进程管理
- 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,¤t->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(¤t->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(¤t->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(¤t->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(¤t->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())。