linux内核设计与实现---进程管理

进程管理

  • 1 进程描述符及任务结构
    • 分配进程描述符
    • 进程描述符的存放
    • 进程状态
    • 设置当前进程状态
    • 进程上下文
    • 进程家族树
  • 2 进程创建
    • 写时拷贝
    • fork()
    • vfork()
  • 3 线程在Linux中的实现
      • 内核线程
  • 4 进程终结
    • 删除进程描述符
    • 孤儿进程造成的进退微谷
  • 5 小结

进程的另一个名字叫做任务(task)。Linux内核通常把进程也叫做任务。下面我们会交替使用任务和进程两个术语。

1 进程描述符及任务结构

内核把进程存放在叫作任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符的结构,该结构定义在include/linux/sched.h文件中。进程描述符包含一个具体进程的所有信息。

struct task_struct {volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */struct thread_info *thread_info;atomic_t usage;unsigned long flags;	/* per process flags, defined below */unsigned long ptrace;int lock_depth;		/* Lock depth */int prio, static_prio;struct list_head run_list;prio_array_t *array;unsigned long sleep_avg;long interactive_credit;unsigned long long timestamp, last_ran;int activated;unsigned long policy;cpumask_t cpus_allowed;unsigned int time_slice, first_time_slice;#ifdef CONFIG_SCHEDSTATSstruct sched_info sched_info;
#endifstruct list_head tasks;/** ptrace_list/ptrace_children forms the list of my children* that were stolen by a ptracer.*/struct list_head ptrace_children;struct list_head ptrace_list;struct mm_struct *mm, *active_mm;/* task state */struct linux_binfmt *binfmt;long exit_state;int exit_code, exit_signal;int pdeath_signal;  /*  The signal sent when the parent dies  *//* ??? */unsigned long personality;unsigned did_exec:1;pid_t pid;pid_t tgid;/* * pointers to (original) parent process, youngest child, younger sibling,* older sibling, respectively.  (p->father can be replaced with * p->parent->pid)*/struct task_struct *real_parent; /* real parent process (when being debugged) */struct task_struct *parent;	/* parent process *//** children/sibling forms the list of my children plus the* tasks I'm ptracing.*/struct list_head children;	/* list of my children */struct list_head sibling;	/* linkage in my parent's children list */struct task_struct *group_leader;	/* threadgroup leader *//* PID/PID hash table linkage. */struct pid pids[PIDTYPE_MAX];wait_queue_head_t wait_chldexit;	/* for wait4() */struct completion *vfork_done;		/* for vfork() */int __user *set_child_tid;		/* CLONE_CHILD_SETTID */int __user *clear_child_tid;		/* CLONE_CHILD_CLEARTID */unsigned long rt_priority;unsigned long it_real_value, it_prof_value, it_virt_value;unsigned long it_real_incr, it_prof_incr, it_virt_incr;struct timer_list real_timer;unsigned long utime, stime;unsigned long nvcsw, nivcsw; /* context switch counts */struct timespec start_time;
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */unsigned long min_flt, maj_flt;
/* process credentials */uid_t uid,euid,suid,fsuid;gid_t gid,egid,sgid,fsgid;struct group_info *group_info;kernel_cap_t   cap_effective, cap_inheritable, cap_permitted;unsigned keep_capabilities:1;struct user_struct *user;
#ifdef CONFIG_KEYSstruct key *session_keyring;	/* keyring inherited over fork */struct key *process_keyring;	/* keyring private to this process (CLONE_THREAD) */struct key *thread_keyring;	/* keyring private to this thread */
#endifunsigned short used_math;char comm[16];
/* file system info */int link_count, total_link_count;
/* ipc stuff */struct sysv_sem sysvsem;
/* CPU-specific state of this task */struct thread_struct thread;
/* filesystem information */struct fs_struct *fs;
/* open file information */struct files_struct *files;
/* namespace */struct namespace *namespace;
/* signal handlers */struct signal_struct *signal;struct sighand_struct *sighand;sigset_t blocked, real_blocked;struct sigpending pending;unsigned long sas_ss_sp;size_t sas_ss_size;int (*notifier)(void *priv);void *notifier_data;sigset_t *notifier_mask;void *security;struct audit_context *audit_context;/* Thread group tracking */u32 parent_exec_id;u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings */spinlock_t alloc_lock;
/* Protection of proc_dentry: nesting proc_lock, dcache_lock, write_lock_irq(&tasklist_lock); */spinlock_t proc_lock;
/* context-switch lock */spinlock_t switch_lock;/* journalling filesystem info */void *journal_info;/* VM state */struct reclaim_state *reclaim_state;struct dentry *proc_dentry;struct backing_dev_info *backing_dev_info;struct io_context *io_context;unsigned long ptrace_message;siginfo_t *last_siginfo; /* For ptrace use.  */
/** current io wait handle: wait queue entry to use for io waits* If this thread is processing aio, this points at the waitqueue* inside the currently handled kiocb. It may be NULL (i.e. default* to a stack based synchronous wait) if its doing sync IO.*/wait_queue_t *io_wait;
#ifdef CONFIG_NUMAstruct mempolicy *mempolicy;short il_next;		/* could be shared with used_math */
#endif
};

分配进程描述符

Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的(通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗)。在2.6以前的内核中,各个进程的task_struct存放在它们内核栈的尾端。这样做是为了让那些x86这样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置。由于现在用slab分配器动态生成task_struct,所以只需在栈底(对于向下增长的栈来说)或栈顶(对于向上增长的的栈来说)创建一个新的结构struct thread_info,thread_info有一个指向进程描述符的指针。
在这里插入图片描述

在x86_64上,thread_info结构在文件include/asm-x86_64/thread_info.h中

struct thread_info {struct task_struct	*task;		/* main task structure */struct exec_domain	*exec_domain;	/* execution domain */__u32			flags;		/* low level flags */__u32			status;		/* thread synchronous flags */__u32			cpu;		/* current CPU */int 			preempt_count;mm_segment_t		addr_limit;	struct restart_block    restart_block;
};

每个任务的thread_info结构在它的内核栈的尾部分配。结构中task域存放的是指向该任务实际task_struct的指针。每个进程都有一个thread_info结构,指向自己的task_struct进程描述符。

进程描述符的存放

内核通过一个唯一的进程标识值或PID来标识每个进程。PID是pid_t类型,是一个int类型。为了与老版本的Unix和Linux兼容,PID的最大值默认设置为32768,内核把每个进程的PID存放在它们各自的进程描述符中。
这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。如果确实需要的话,由系统管理员通过修改/proc/sys/kernel/pid_max来提高上线。
在内核中,访问任务通常需要获得指向其task_struct指针。可以通过current宏查找到当前正在运行进程的进程描述符。有的硬件体系结构可以拿出一个专门寄存器来存放当前进程task_struct的地址。
在x86系统上,current宏定义在include/asm-m32r/current.h文件中。current把栈指针的后13个有效数字位屏蔽掉,用来计算出thread_info的偏移,因为thread_info结构存放在它的内核栈的尾端

static __inline__ struct task_struct *get_current(void)
{return current_thread_info()->task;
}#define current	(get_current())static inline struct thread_info *current_thread_info(void)
{struct thread_info *ti;__asm__ __volatile__ ("ldi	%0, #0xffffe000;	\n\t""and	%0, sp;			\n\t": "=r" (ti));return ti;
}

这里内核栈的大小是8KB,两页,13位可以标识8kb内存地址,屏蔽13位刚刚好指向栈的尾端。

进程状态

进程描述符中的state域描述了进程的当前状态

volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */

系统中的每个进程都必然处于五种进程状态中的一种。该域的值也必然为下列五种状态标志之一:

  • TASK_RUNNING(运行):进程是可执行的;它或者正在执行,或者在运行队列中等待执行
  • TASK_INTERRUPTIBLE(可中断):进程正在睡眠(也就是被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。
  • TASK_UNINTERRUPTIBLE(不可中断):除了不会因为接受到信号而被唤醒从而投入运行外,这个状态与可打断状态相同。
  • TASK_ZOMBIE(僵死):该进程已经结束了,但是符父进程还没有调用wait4()系统调用,释放相关资源。
  • TASK_STOPPED(停止):进程停止运行。

请添加图片描述

设置当前进程状态

内核经常需要调整某个进程的状态。这时最好使用set_task_state函数:

set_task_state(task,state)//将任务task的状态设置为state
#define set_task_state(tsk, state_value)		\set_mb((tsk)->state, (state_value))

该函数将指定的进程设置为指定的状态。位置在/include/linux/sched.h文件中。
方法set_current_state(state)和set_task_state(current, state)含义是相同的。

进程上下文

一般程序在用户空间执行。当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核"代表进程执行"并处于进程上下文中。在此上下文中的current宏是有效的。除非在此间隙有更高优先级的进程需要执行并由调度器作出了相应调整,否则在内核退出的时候,程序恢复在用户空间继续执行。

进程家族树

Unix系统的进程之间存在明显的继承关系,在Linux系统中也是如此。所有的进程都是PID为1的init进程的后代。系统中的每个进程必有一个父进程。相应的,每个进程也可以拥有零个或多个子进程。进程间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程进程描述符的parent指向,还包含一个称为children的子进程链表。所以对于当前进程,可以通过下面的代码获得其父进程的进程描述符:

struct task_struct *my_parent = current->parent;	//current是指向当前进程的进程描述符指针

同样,也可以按以下方式依次访问子进程

struct task_struct *task;
struct list_head *list;
list_for_each(list,&current->children){task = list_entry(list,struct task_struct,sibling);/* task现在指向当前的某个子进程 */
}

2 进程创建

Unix的进程创建很特别。许多其他的操作系统都提供了产生进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。然后exec()函数负责读取可执行文件并将其载入地址空间开始执行。

写时拷贝

Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。当调用fork时,内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。

fork()

Linux通过clone()系统调用实现 fork()。这个调用通过一系列的参数标志来指明父子进程需要共享的资源。fork()、vfork()和__clone()库函数都根据各自需要的参数标志去调用clone()。然后由clone()去调用do_fork()。
do_fork()完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。该函数调用copy_process函数,然后让进程开始运行。copy_process函数完成的工作很有意思:

static task_t *copy_process(unsigned long clone_flags,unsigned long stack_start,struct pt_regs *regs,unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr,int pid)
{int retval;struct task_struct *p = NULL;if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))return ERR_PTR(-EINVAL);/** Thread groups must share signals as well, and detached threads* can only be started up within the thread group.*/if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))return ERR_PTR(-EINVAL);/** Shared signal handlers imply shared VM. By way of the above,* thread groups also imply shared VM. Blocking this case allows* for various simplifications in other code.*/if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))return ERR_PTR(-EINVAL);retval = security_task_create(clone_flags);if (retval)goto fork_out;retval = -ENOMEM;p = dup_task_struct(current);if (!p)goto fork_out;retval = -EAGAIN;if (atomic_read(&p->user->processes) >=p->signal->rlim[RLIMIT_NPROC].rlim_cur) {if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&p->user != &root_user)goto bad_fork_free;}atomic_inc(&p->user->__count);atomic_inc(&p->user->processes);get_group_info(p->group_info);/** If multiple threads are within copy_process(), then this check* triggers too late. This doesn't hurt, the check is only there* to stop root fork bombs.*/if (nr_threads >= max_threads)goto bad_fork_cleanup_count;if (!try_module_get(p->thread_info->exec_domain->module))goto bad_fork_cleanup_count;if (p->binfmt && !try_module_get(p->binfmt->module))goto bad_fork_cleanup_put_domain;p->did_exec = 0;copy_flags(clone_flags, p);p->pid = pid;retval = -EFAULT;if (clone_flags & CLONE_PARENT_SETTID)if (put_user(p->pid, parent_tidptr))goto bad_fork_cleanup;p->proc_dentry = NULL;INIT_LIST_HEAD(&p->children);INIT_LIST_HEAD(&p->sibling);init_waitqueue_head(&p->wait_chldexit);p->vfork_done = NULL;spin_lock_init(&p->alloc_lock);spin_lock_init(&p->proc_lock);clear_tsk_thread_flag(p, TIF_SIGPENDING);init_sigpending(&p->pending);p->it_real_value = p->it_virt_value = p->it_prof_value = 0;p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0;init_timer(&p->real_timer);p->real_timer.data = (unsigned long) p;p->utime = p->stime = 0;p->lock_depth = -1;		/* -1 = no lock */do_posix_clock_monotonic_gettime(&p->start_time);p->security = NULL;p->io_context = NULL;p->io_wait = NULL;p->audit_context = NULL;
#ifdef CONFIG_NUMAp->mempolicy = mpol_copy(p->mempolicy);if (IS_ERR(p->mempolicy)) {retval = PTR_ERR(p->mempolicy);p->mempolicy = NULL;goto bad_fork_cleanup;}
#endifp->tgid = p->pid;if (clone_flags & CLONE_THREAD)p->tgid = current->tgid;if ((retval = security_task_alloc(p)))goto bad_fork_cleanup_policy;if ((retval = audit_alloc(p)))goto bad_fork_cleanup_security;/* copy all the process information */if ((retval = copy_semundo(clone_flags, p)))goto bad_fork_cleanup_audit;if ((retval = copy_files(clone_flags, p)))goto bad_fork_cleanup_semundo;if ((retval = copy_fs(clone_flags, p)))goto bad_fork_cleanup_files;if ((retval = copy_sighand(clone_flags, p)))goto bad_fork_cleanup_fs;if ((retval = copy_signal(clone_flags, p)))goto bad_fork_cleanup_sighand;if ((retval = copy_mm(clone_flags, p)))goto bad_fork_cleanup_signal;if ((retval = copy_keys(clone_flags, p)))goto bad_fork_cleanup_mm;if ((retval = copy_namespace(clone_flags, p)))goto bad_fork_cleanup_keys;retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);if (retval)goto bad_fork_cleanup_namespace;p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;/** Clear TID on mm_release()?*/p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;/** Syscall tracing should be turned off in the child regardless* of CLONE_PTRACE.*/clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);/* Our parent execution domain becomes current domainThese must match for thread signalling to apply */p->parent_exec_id = p->self_exec_id;/* ok, now we should be set up.. */p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);p->pdeath_signal = 0;p->exit_state = 0;/* Perform scheduler related setup */sched_fork(p);/** Ok, make it visible to the rest of the system.* We dont wake it up yet.*/p->group_leader = p;INIT_LIST_HEAD(&p->ptrace_children);INIT_LIST_HEAD(&p->ptrace_list);/* Need tasklist lock for parent etc handling! */write_lock_irq(&tasklist_lock);/** The task hasn't been attached yet, so cpus_allowed mask cannot* have changed. The cpus_allowed mask of the parent may have* changed after it was copied first time, and it may then move to* another CPU - so we re-copy it here and set the child's CPU to* the parent's CPU. This avoids alot of nasty races.*/p->cpus_allowed = current->cpus_allowed;set_task_cpu(p, smp_processor_id());/** Check for pending SIGKILL! The new thread should not be allowed* to slip out of an OOM kill. (or normal SIGKILL.)*/if (sigismember(&current->pending.signal, SIGKILL)) {write_unlock_irq(&tasklist_lock);retval = -EINTR;goto bad_fork_cleanup_namespace;}/* CLONE_PARENT re-uses the old parent */if (clone_flags & (CLONE_PARENT|CLONE_THREAD))p->real_parent = current->real_parent;elsep->real_parent = current;p->parent = p->real_parent;if (clone_flags & CLONE_THREAD) {spin_lock(&current->sighand->siglock);/** Important: if an exit-all has been started then* do not create this new thread - the whole thread* group is supposed to exit anyway.*/if (current->signal->group_exit) {spin_unlock(&current->sighand->siglock);write_unlock_irq(&tasklist_lock);retval = -EAGAIN;goto bad_fork_cleanup_namespace;}p->group_leader = current->group_leader;if (current->signal->group_stop_count > 0) {/** There is an all-stop in progress for the group.* We ourselves will stop as soon as we check signals.* Make the new thread part of that group stop too.*/current->signal->group_stop_count++;set_tsk_thread_flag(p, TIF_SIGPENDING);}spin_unlock(&current->sighand->siglock);}SET_LINKS(p);if (unlikely(p->ptrace & PT_PTRACED))__ptrace_link(p, current->parent);attach_pid(p, PIDTYPE_PID, p->pid);attach_pid(p, PIDTYPE_TGID, p->tgid);if (thread_group_leader(p)) {attach_pid(p, PIDTYPE_PGID, process_group(p));attach_pid(p, PIDTYPE_SID, p->signal->session);if (p->pid)__get_cpu_var(process_counts)++;}nr_threads++;write_unlock_irq(&tasklist_lock);retval = 0;fork_out:if (retval)return ERR_PTR(retval);return p;bad_fork_cleanup_namespace:exit_namespace(p);
bad_fork_cleanup_keys:exit_keys(p);
bad_fork_cleanup_mm:if (p->mm)mmput(p->mm);
bad_fork_cleanup_signal:exit_signal(p);
bad_fork_cleanup_sighand:exit_sighand(p);
bad_fork_cleanup_fs:exit_fs(p); /* blocking */
bad_fork_cleanup_files:exit_files(p); /* blocking */
bad_fork_cleanup_semundo:exit_sem(p);
bad_fork_cleanup_audit:audit_free(p);
bad_fork_cleanup_security:security_task_free(p);
bad_fork_cleanup_policy:
#ifdef CONFIG_NUMAmpol_free(p->mempolicy);
#endif
bad_fork_cleanup:if (p->binfmt)module_put(p->binfmt->module);
bad_fork_cleanup_put_domain:module_put(p->thread_info->exec_domain->module);
bad_fork_cleanup_count:put_group_info(p->group_info);atomic_dec(&p->user->processes);free_uid(p->user);
bad_fork_free:free_task(p);goto fork_out;
}
  • 调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
  • 检查新创建的这个子进程后,当前用户所拥有的进程数目没有超过给他分配的资源的限制
  • 现在,子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。
  • 接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行
  • copy_process()调用copy_flags()以更新task_struct的flags成员。
  • 调用get_pid()为新进程获取一个有效的PID
  • 根据传递给clone的参数标志,copy_process拷贝或共享打开的文件、文件系统消息等。
  • 让父进程和子进程平分剩余的时间片
  • 最后,copy_process做扫尾工作并放回一个指向子进程的指针。
    再回到do_fork()函数,如果copy_process()成功返回,新创建的子进程被唤醒并让其投入运行。

vfork()

vfork系统调用和fork()的功能相同,除了不拷贝父进程的页表项。子进程作为父进程的一个单独的线程在它的地址空间运行,父进程被阻塞,直到子进程退出或执行exec()。
vfork系统调用的实现是通过向clone系统调用传递一个特殊标志进行的。

  • 在调用copy_process()时,task_struct的vfork_done成员被设置为NULL
  • 在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特殊地址
  • 子进程开始执行后,父进程不是立马恢复执行,而是一直等待,直到子进程通过vfork_done指针向它发送信号
  • 在调用mm_release()时,该函数用于进程退出内存地址空间,并且检查vfork_done是否为空,如果不为空,则会向父进程发送信号。
  • 回到do_fork(),父进程醒来并返回。

3 线程在Linux中的实现

Linux实现线程的机制非常独特。从内核角度来说,它并没有线程这个概念。Linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表示线程。相反,线程仅仅被视为一个与其他进程共享某些资源的**进程。**每个进程都有唯一自己的task_struct。所以在内核中,它看起来像一个普通的线程。
线程的创建和普通进程的创建类似,只不过在调用clone的时候需要传递一些参数标志来指明需要的共享资源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0)

上面的代码产生的结构和调用fork差不多,只是父子进程共享地址空间、文件系统资源、文件描述符和信号处理程序。
一个普通的fork实现是:

clone(CLONE_VFORK | CLONE_VM  | CLONE_SIGHAND,0)

传递给clone的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。这些参数标志定义在include/linux/sched.h文件中。

/** cloning flags:*/
#define CSIGNAL		0x000000ff	/* signal mask to be sent at exit */
#define CLONE_VM	0x00000100	/* set if VM shared between processes */
#define CLONE_FS	0x00000200	/* set if fs info shared between processes */
#define CLONE_FILES	0x00000400	/* set if open files shared between processes */
#define CLONE_SIGHAND	0x00000800	/* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE	0x00002000	/* set if we want to let tracing continue on the child too */
#define CLONE_VFORK	0x00004000	/* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT	0x00008000	/* set if we want to have the same parent as the cloner */
#define CLONE_THREAD	0x00010000	/* Same thread group? */
#define CLONE_NEWNS	0x00020000	/* New namespace group? */
#define CLONE_SYSVSEM	0x00040000	/* share system V SEM_UNDO semantics */
#define CLONE_SETTLS	0x00080000	/* create a new TLS for the child */
#define CLONE_PARENT_SETTID	0x00100000	/* set the TID in the parent */
#define CLONE_CHILD_CLEARTID	0x00200000	/* clear the TID in the child */
#define CLONE_DETACHED		0x00400000	/* Unused, ignored */
#define CLONE_UNTRACED		0x00800000	/* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID	0x01000000	/* set the TID in the child */
#define CLONE_STOPPED		0x02000000	/* Start in stopped state */

内核线程

内核线程是独立运行在内核空间的标准进程,内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(mm指针被设置为NULL)。它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。
内核线程只能由其他内核线程创建,在现有内核线程创建一个新的内核线程的方法如下:

int kernel_thread(int (*fn)(void*),void *arg,unsigned long flags);

新的任务也是通过向普通的clone()系统调用传递特定的flags参数而创建的。在上面的函数返回时,父线程退出,并返回一个指向子线程task_struct的指针。

4 进程终结

当一个进程终结时,内核必须释放它所占有的资源并把这一消息传给父进程。不论进程是怎样终结的,该任务大部分都要靠do_exit()来完成,do_exit()定义在kernel/exit.c文件中

fastcall NORET_TYPE void do_exit(long code)
{struct task_struct *tsk = current;int group_dead;profile_task_exit(tsk);if (unlikely(in_interrupt()))panic("Aiee, killing interrupt handler!");if (unlikely(!tsk->pid))panic("Attempted to kill the idle task!");if (unlikely(tsk->pid == 1))panic("Attempted to kill init!");if (tsk->io_context)exit_io_context();tsk->flags |= PF_EXITING;del_timer_sync(&tsk->real_timer);if (unlikely(in_atomic()))printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",current->comm, current->pid,preempt_count());if (unlikely(current->ptrace & PT_TRACE_EXIT)) {current->ptrace_message = code;ptrace_notify((PTRACE_EVENT_EXIT << 8) | SIGTRAP);}group_dead = atomic_dec_and_test(&tsk->signal->live);if (group_dead)acct_process(code);__exit_mm(tsk);exit_sem(tsk);__exit_files(tsk);__exit_fs(tsk);exit_namespace(tsk);exit_thread();exit_keys(tsk);if (group_dead && tsk->signal->leader)disassociate_ctty(1);module_put(tsk->thread_info->exec_domain->module);if (tsk->binfmt)module_put(tsk->binfmt->module);tsk->exit_code = code;exit_notify(tsk);
#ifdef CONFIG_NUMAmpol_free(tsk->mempolicy);tsk->mempolicy = NULL;
#endifBUG_ON(!(current->flags & PF_DEAD));schedule();BUG();/* Avoid "noreturn function does return".  */for (;;) ;
}
  • 首先,将task_struct中的标志成员设置为PF_EXITING
  • 其次,调用del_timer_sync()喊出任一内核定时器
  • 如果BSD的进程记账功能是开启的,do_exit()调用acct_process()来输出记账信息。
  • 然后调用_exit_mm()函数放弃进程占用的mm_struct,如果没有别的进程使用它们,就彻底释放它们
  • 接下来调用exit_sem()函数。如果进程排队等候IPC信号,则离开队列
  • 调用_exit_files()、_exit_fs()、_exit_namespce()和exit_sighand(),分别递减文件描述符、文件系统数据、进程名字空间和信号处理函数的引用计数。如果其中某些引用计数的值降为0,那么久代表没有进程在使用相应的资源,此时可以释放。
  • 接着把存放在task_struct的exit_code成员中的任务退出码置为exit()提供的代码中,或者去完成任何其他由内核机制规定的退出动作
  • 调用exit_notify()向父进程发送信号,将子进程的父进程重新设置为线程组中的其他线程或init进程,并把进程状态设置为TASK_ZOMBLE
  • 最后,do_exit()调用schedule()切换到其他进程。

至此,与进程相关联的所有资源都被释放掉了,只是相关联的资源被释放了,进程还有资源没有被释放,它剩下所占用的所有资源就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用。

删除进程描述符

在调用do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。这样做可以让系统有办法在子进程终结后仍能获得它的信息。
当最终需要释放进程描述符时,release_task()会被调用,用于完成以下工作:

  • 首先,它调用free_uid()来减少该进程拥有者的进程使用计数。
  • 然后,release_task()调用unhash_process()从pidhash上删除该进程,同时也要从task_list中删除该进程
  • 接下来,如果这个进程正在被普通race跟踪,release_task()将跟踪进程的父进程重设为最初的父进程并将它从ptrace list上删除。
  • 最后,release_task()调用put_task_struct()释放该进程内核栈和thread_info结构所占的页,并释放task_struct所占的深蓝高速缓存、

至此,进程描述符和所有进程独享的资源就全部释放掉了。

孤儿进程造成的进退微谷

如果父进程在子进程之前退出,那么必须有机制来保证子进程能找到一个新的父亲,否则的话这些成为孤儿的进程就会在退出时永远处于僵死状态,白白的耗费内存。解决方案:子进程在当前线程组(父进程所在的线程组)内找一个线程作为父亲,如果不行,就让init作为它们的父进程。在do_exit()中会调用notify_parent(),该函数会通过forget_original_parent()来执行寻父过程。

static inline void forget_original_parent(struct task_struct * father,struct list_head *to_release)
{struct task_struct *p, *reaper = father;struct list_head *_p, *_n;do {reaper = next_thread(reaper);if (reaper == father) {reaper = child_reaper;break;}} while (reaper->exit_state >= EXIT_ZOMBIE);/** There are only two places where our children can be:** - in our child list* - in our ptraced child list** Search them and reparent children.*/list_for_each_safe(_p, _n, &father->children) {int ptrace;p = list_entry(_p,struct task_struct,sibling);ptrace = p->ptrace;/* if father isn't the real parent, then ptrace must be enabled */BUG_ON(father != p->real_parent && !ptrace);if (father == p->real_parent) {/* reparent with a reaper, real father it's us */choose_new_parent(p, reaper, child_reaper);reparent_thread(p, father, 0);} else {/* reparent ptraced task to its real parent */__ptrace_unlink (p);if (p->exit_state == EXIT_ZOMBIE && p->exit_signal != -1 &&thread_group_empty(p))do_notify_parent(p, p->exit_signal);}/** if the ptraced child is a zombie with exit_signal == -1* we must collect it before we exit, or it will remain* zombie forever since we prevented it from self-reap itself* while it was being traced by us, to be able to see it in wait4.*/if (unlikely(ptrace && p->exit_state == EXIT_ZOMBIE && p->exit_signal == -1))list_add(&p->ptrace_list, to_release);}list_for_each_safe(_p, _n, &father->ptrace_children) {p = list_entry(_p,struct task_struct,ptrace_list);choose_new_parent(p, reaper, child_reaper);reparent_thread(p, father, 1);}
}

先在线程组里找一个线程作为父进程,如果线程组内没有其他的进程,就将init设为父进程。当合适的父进程找到后,只需要遍历所有子进程并为它们设置新的父进程。

后面遍历了两个链表:子进程链表和ptrace子进程链表。给每个子进程设置新的父进程。当一个进程被跟踪时,它被暂时设定为调试进程的子进程。此时如果它的父进程退出了,系统会为它和它所有的兄弟重新找一个父进程。在以前的内核中,这就需要遍历系统所有的进程来找这些子进程,现在的解决办法是在一个单独的ptrace跟踪的子进程链表中搜索相关的兄弟进程,用两个相关链表减轻了遍历带来的消耗。

5 小结

在本文中,我们讨论进程的一般特效,它为何如此重要,以及进程与线程之间的关系。然后,讨论了Linux如何存放和表示进程(用task_struct和thread_info),如果创建进程(通过clone()和fork()),如何把新的执行映像装入到地址空间(通过exec()调用族),如何表示进程的层次关系,父进程又是如何收集后代的信息(通过wait()系统调用族),以及进程最终如何死亡(强制或自愿调用exit())。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/379680.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

生日蜡烛(蓝桥杯)

某君从某年开始每年都举办一次生日party&#xff0c;并且每次都要吹熄与年龄相同根数的蜡烛。 现在算起来&#xff0c;他一共吹熄了236根蜡烛。 请问&#xff0c;他从多少岁开始过生日party的&#xff1f; 请填写他开始过生日party的年龄数。 注意&#xff1a;你提交的应该是…

Linux内核设计与实现---进程调度

进程调度1 策略I/O消耗型和处理器消耗型的进程进程优先级时间片进程抢占2 Linux调度算法可执行队列优先级数组重新计算时间片schedule()计算优先级和时间片睡眠和唤醒负载平衡程序3 抢占和上下文切换用户抢占内核抢占4 实时5 与调度相关的系统调用与调度策略和优先级相关的系统…

ServletContext(核心内容)

什么是ServletContext对象 ServletContext代表是一个web应用的环境&#xff08;上下文&#xff09;对象&#xff0c;ServletContext对象 内部封装是该web应用的信息&#xff0c;ServletContext对象一个web应用只有一个 一个web应用有多个servlet对象 ServletContext对象的生…

【转载】[TC]飞船动画例子--《C高级实用程序设计》

【声明和备注】本例子属于转载来源于《C高级实用程序设计》&#xff08;王士元&#xff0c;清华大学出版社&#xff09;第11章&#xff0c;菜单设计与动画技术&#xff0c;第11.5节&#xff0c;一个动画例子。 本例讲解的是在一个繁星背景下&#xff0c;一个由经纬线组成的蓝色…

Linux内核设计与实现---系统调用

系统调用1 API、POSIX和C库2 系统调用系统调用号3 系统调用处理程序指定恰当的系统调用参数传递4 系统调用的实现参数验证5 系统调用上下文绑定一个系统调用的最后步骤从用户空间访问系统调用为什么不通过系统调用的方式实现1 API、POSIX和C库 API&#xff1a;应用编程接口。一…

手动去设置HTTP响应行、响应头、响应体

①手动去设置HTTP响应行中的状态码&#xff0c;这里用到了response的setStatus(int sc);这个方法 package com.itheima.line;import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpSer…

基本的二分查找、寻找第一个和最后一个数的二分查找

二分查找1 二分查找的框架2 寻找一个数&#xff08;基本的二分搜索&#xff09;3 寻找左侧边界的二分搜索4 寻找右侧边界的二分查找5 合并二分查找场景&#xff1a;有序数组寻找一个数、寻找左侧边界&#xff08;有序数组第一个等目标数的下标&#xff09;、寻找右侧边界&#…

Linux内核设计与实现---中断和中断处理程序

中断和中断处理程序1 中断异常2 中断处理程序上半部与下半部的对比3 注册中断处理程序释放中断处理程序4 编写中断处理程序重入和中断处理程序共享的中断处理程序中断处理程序实例5 中断上下文6 中断处理机制的实现7 中断控制禁止和激活中断禁止指定中断线中断系统的状态8 总结…

response细节点

一、 1&#xff09;、response获得的流不需要手动关闭&#xff0c;Tomcat容器会帮你自动关闭 2&#xff09;、getWriter和getOutputStream不能同时调用 //error package com.itheima.content;import java.io.IOException; import javax.servlet.ServletException; import java…

linux内核设计与实现---下半部和推后执行的工作

下半部和推后执行的工作1 下半部为什么要用下半部下半部的环境内核定时器2 软中断软中断的实现软中断处理程序执行软中断使用软中断3 tasklettasklet的实现使用taskletksoftirqd4 工作队列工作队列的实现工作、工作队列和工作者线程之间的关系使用工作队列5 下半部机制的选择6 …

Mac VSCode配置C语言环境(可以调试)

Mac VSCode配置C语言环境c_cpp_properties.jsontasks.jsonlaunch.json新建一个文件夹&#xff0c;用vscode&#xff0c;然后再新建一个test.c文件。 #include <stdio.h>int main(void) {int a1,b1;int cab;printf("%d\n",c);return 0; }这篇文章说怎么配置c_c…

vShpere Client在win 7 RC下和2008下 无法正常连接esx主机之解决办法

vShpere Client在win 7 RC下和2008下 无法正常连接esx主机之解决办法 在win7下和2008下打开client后连接esx主机会出现2个错误提示, 第一个是 第二个是 然后就连接失败了,开始以为是CC的esx主机安装有问题,后来找了找,借助了强大google工具,终于找到解决办法.解决办法如下: 1.从…

localhost与127.0.0.1之间的关系更改

其实localhost的默认IP地址为127.0.0.1&#xff0c;因为这是一种映射关系。 更改步骤如下&#xff1a; C:\Windows\System32\drivers\etc 下的hosts 打开hosts可以看到 更改即可

Linux内核设计与实现---内核同步方法

内核同步方法1 原子操作原子整数操作原子性与顺序性的比较原子位操作2 自旋锁自旋锁是不可递归的其他针对自旋锁的操作自旋锁和下半部3 读-写自旋锁4 信号量创建和初始化信号量使用信号量5 读-写信号量6 自旋锁和信号量7 完成变量8 互斥锁互斥锁API9 禁止抢占10 顺序和屏障1 原…

UNIX环境高级编程---进程间通信总结

进程间通信1 管道匿名管道命名管道2 消息队列3 信号量POSIX信号量有名信号量无名信号量有名信号量和无名信号量的公共操作4 共享内存5 信号相关函数6 套接字针对 TCP 协议通信的 socket 编程模型针对 UDP 协议通信的 socket 编程模型针对本地进程间通信的 socket 编程模型总结L…

搜索---广度优先遍历、深度优先遍历、回溯法

参考文章&#xff1a;https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3%20-%20%E6%90%9C%E7%B4%A2.md 广度优先搜索&#xff08;BFS&#xff09; 广度优先搜索是按层来处理顶点的&#xff0c;距离开始点最近的那些顶点首先被访问&#…

如何更改Visual Studio 2008中类文件引用的默认名称空间?

在编写程序的时候&#xff0c;如果某些名称空间经常用到&#xff0c;每次创建一个文件的时候&#xff0c;都需要手工添加名称空间&#xff0c;是不是很烦人呢&#xff1f;多说人会回答&#xff1a;是的。如果新建文件的时候就自动加上自己需要的名称空间该多好啊。&#xff1a;…

Linux内核设计与实现---内存管理

内存管理1 页2 区3 获得页获得填充为0的页释放页4 kmalloc()gfp_mask标志kfree()5 vmalloc()6 slab层slab层的设计7 slab分配器的接口8 在栈上的静态分配9 高端内核的映射永久映射临时映射10 每个CPU的分配11 新的每个CPU的接口编译时的每个CPU数据运行时每个CPU数据12 使用每个…

多语言开发 之 通过基页类及Session 动态响应用户对语言的选择

在用户通过UserLogin.aspx登录系统时 提供其对语言的选择选择后 将所选存入Session 以便登录系统后的其他页面进行按语言显示当然相关页面需要支持多语言具体信息可参看使用 根据语言环境不同 而显示不同的 资源本地化 ASP.NET 网页 App_Code下定义基页类 BasePage.cs Codeusin…

Linux内核设计与实现---虚拟文件系统

虚拟文件系统1 通用文件系统2 文件系统抽象层3 Unix文件系统4 VFS对象及其数据结构其他VFS对象5 超级快对象超级块操作6 索引节点对象索引节点操作7 目录项对象目录项状态目录项缓存目录项操作8 文件对象9 和文件系统相关的数据结构10 和进程相关的数据结构11 Linux中的文件系统…