首页 > 代码库 > 进程创建与销毁
进程创建与销毁
Unix操作系统紧紧依赖进程创建来满足用户需求
创建进程
Unix创建进程的三种机制
1、写时复制技术运行父子进程读相同的物理页。只要两者中有一个试图写一个物理页,内核就把这个页的内容拷贝到 一个新的物理页,并把这个新的物理页分配给正在写的进程。
2、轻量级进程允许父子进程共享每个进程在内核的很多数据结构,如页表(整个用户态的地址空间)、打开文件表及信 号处理。
3、vfork()系统调用创建的进程能共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进 程的执行,一直到子进程退出或执行一个新的程序为止。
进程创建中两个主要的调用do_fork、copy_process()
do_fork()
do_fork()函数负责处理clone()、fork()、vfork()系统调用,
linux-2.6.11.1/kernel/fork.c
/* * Ok, this is the main fork-routine. * * It copies the process, and if successful kick-starts * it and waits for it to finish using the VM if required. */ long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; int trace = 0; long pid = alloc_pidmap(); if (pid < 0) return -EAGAIN; if (unlikely(current->ptrace)) { trace = fork_traceflag (clone_flags); if (trace) clone_flags |= CLONE_PTRACE; } p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid); /* * Do this prior waking up the new thread - the thread pointer * might get invalid after that point, if the thread exits quickly. */ if (!IS_ERR(p)) { struct completion vfork; if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); } if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) { /* * We‘ll start up with an immediate SIGSTOP. */ sigaddset(&p->pending.signal, SIGSTOP); set_tsk_thread_flag(p, TIF_SIGPENDING); } if (!(clone_flags & CLONE_STOPPED)) wake_up_new_task(p, clone_flags); else p->state = TASK_STOPPED; if (unlikely (trace)) { current->ptrace_message = pid; ptrace_notify ((trace << 8) | SIGTRAP); } if (clone_flags & CLONE_VFORK) { wait_for_completion(&vfork); if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP); } } else { free_pidmap(pid); pid = PTR_ERR(p); } return pid; }clone_flags 进程复制的标记
stack_start 表示把用户态堆栈指针赋给子进程的esp寄存器。调用进程应该总是为子进程分配新的堆栈
regs 指向通用寄存器值的指针,通用寄存器的值是从用户态切换到内核态时被保存到内核态堆栈中的
stack_size 未使用(总是被设置为0)
主要步骤:
1、通过查找pidmap_array位图,为子进程分配新的PID
2、检查父进程的ptrace字段:如果它的值不等于0,说明有另外一个进程正在跟踪父进程,因而,do_fork()检查debugger程序是否想跟踪子进程。在这种情况下,如果子进程不是内核线程(CLONE_UNTRACED标志被清0),那么do_fork()函数设置CLONE_PTRACE标志
3、调用copy_process()复制进程描述符。如果所有必须的资源都是可以用的,该函数返回刚创建的task_struct描述符的地址。这是创建过程的关键步骤。
4、如果设置了CLONE_STOPPED标志或者必须跟踪子进程,那么子进程的状态被设置成TASK_STOPPED,并为子进程增加挂起的SIGSTOP信号。在另外一个进程把子进程的状态恢复为TASK_RUNNING之前,子进程将一直保持TASK_STOPPED状态。
5、如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task()执行下述操作:
a、调整父进程和子进程的调度参数
b、如果子进程将和父进程运行在同一CPU上,而且父进程和子进程不能共享同一组页表,那么,就把子进程插入父进程运行队列,插入时让子进程恰好在 父进程前面,因此而迫使子进程先与父进程运行。如果子进程刷新其地址空间,并在创建之后执行新程序,那么这种简单的处理会产生较好的性能。
c、否则,如果子进程与父进程运行在不同的CPU上,或者父进程和子进程共享一组页表,就把子进程插入父进程运行队列的队尾。
6、如果父进程被跟踪,则把子进程的PID存入current->ptrace_message字段并调用ptrace_notify(),ptrace_notify()使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号。通知跟踪父进程的debugger进程:current已经创建了一个子进程,可以通过查询current->ptrace_message字段获得子进程的PID。
7、如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间(也就是说,直到子进程结束或执行新的程序)
8、结束并返回子进程的PID。
内核线程
在Linux中,内核线程在以下几方面不同于普通进程:
1、内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。
2、因为内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是内核态,普通进程可以用4GB的线性地址空间。
kernel_thread()函数创建一个新的内核线程。该函数本质上以下面的方式调用do_fork():
do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
CLONE_VM标志避免复制调用进程的页表:由于新内核线程无论如何都不会访问用户态地址空间,所以这种复制会无疑会造成时间和空间的浪费。CLONE_UNTRACED标志保证不会有任何进程跟踪新内核线程,即使调用进程被跟踪了。
进程0
所有进程的祖先叫做进程0,idle进程或因为历史原因叫做swapper进程,它是在Linux的初始化阶段从无到有创建的一个内核线程。这个祖先的数据结构式静态分配的(所有其他进程的数据结构式动态分配的)
start_kernel()函数初始化内核需要的数据结构,激活中断,创建另一个进程1的内核线程(一般叫做init进程)
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
创建init进程后,进程0执行cpu_idle()函数,该函数本质是在开中断的情况下重复执行hlt汇编指令。只有当没有其他进程处于TASK_RUNNING状态时,调度程序才选择进程0。
其他内核线程
keventd 执行keventd_wq工作队列中的函数
kapmd 处理与高级电源管理的相关事件
kswapd 执行内存回收
pdflush 刷新“脏”缓冲区中的内容到磁盘以回收内存。
kblockd 执行kblockd_workqueue工作队列中的函数。实际上,它周期性的激活块设备驱动程序。
ksoftirqd 运行tasklet,系统中每个CPU都有这样一个内核线程。
撤销进程
进程终止的一般方式是调用exit()库函数,该函数释放C函数库所分配的资源,执行编程者所注册的每个函数,并结束从系统回收进程的那个系统调用。exit()函数可能由编程者显示插入。另外,C编译程序总是把exit()函数插入到main()函数的最后一条语句之后。
内核可以有选择地强迫整个线程组死掉,这发生在以下两种情况下:当进程接收到一个不能处理或者忽视的信号时,或者内核正在代表进程运行时在内核态产生一个不可恢复的CPU异常时。
在Linux 2.6中有两个终止用户态应用的系统调用:
exit_group()系统调用,它终止整个线程组,即整个基于多线程的应用。do_group_exit()是实现这个系统调用的主要内核函数。这是C库函数exit()应该调用的系统调用。
exit()系统调用,它终止某一个线程,而不管该线程所属线程组中的所有其他进程,do_exit()是实现这个系统调用的主要内核函数。这是被诸如pthread_exit()的linux线程库的函数所调用的系统调用。
/* * Take down every thread in the group. This is called by fatal signals * as well as by sys_exit_group (below). */ NORET_TYPE void do_group_exit(int exit_code) { BUG_ON(exit_code & 0x80); /* core dumps don‘t get here */ if (current->signal->flags & SIGNAL_GROUP_EXIT) exit_code = current->signal->group_exit_code; else if (!thread_group_empty(current)) { struct signal_struct *const sig = current->signal; struct sighand_struct *const sighand = current->sighand; read_lock(&tasklist_lock); spin_lock_irq(&sighand->siglock); if (sig->flags & SIGNAL_GROUP_EXIT) /* Another thread got here before we took the lock. */ exit_code = sig->group_exit_code; else { sig->flags = SIGNAL_GROUP_EXIT; sig->group_exit_code = exit_code; zap_other_threads(current); } spin_unlock_irq(&sighand->siglock); read_unlock(&tasklist_lock); } do_exit(exit_code); /* NOTREACHED */ }
该函数执行下述操作:
1、检查退出进程的SIGNAL_GROUP_EXIT标志是否不为0,如果不为0,说明内核已经开始为线程组执行退出的过程。在这种情况下,就把current->signal->group_exit_code中的值当做退出码,然后跳转到第4步。
2、否则,设置进程的SIGNAL_GROUP_EXIT标志并把终止代号存放到current->signal->group_exit_code字段。
3、调用zap_other_threads()函数杀死current线程组中的其他进程。为了完成这个步骤,函数扫描与current->tgid对应的PIDTYPE_TGID类型
的散列表中的每个PID链表,向表中所有其他进程发送SIGKILL信号,结果,所有这样的进程都将执行do_exit()函数,从而被杀死
4、调用do_exit()函数,把进程的终止代号传递给它。
do_exit()函数
所有进程的终止都是由do_exit()函数来处理,这个函数从内核数据结构中删除对终止进程的大部分引用。
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(); if (unlikely(current->ptrace & PT_TRACE_EXIT)) { current->ptrace_message = code; ptrace_notify((PTRACE_EVENT_EXIT << 8) | SIGTRAP); } 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()); acct_update_integrals(); update_mem_hiwater(); 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_NUMA mpol_free(tsk->mempolicy); tsk->mempolicy = NULL; #endif BUG_ON(!(current->flags & PF_DEAD)); schedule(); BUG(); /* Avoid "noreturn function does return". */ for (;;) ; }该函数执行下述操作:
1、把进程描述符的flag字段设置为PF_EXITING标志,以表示进程正在被删除。
2、如果需要,通过函数del_timer_sync()从动态定时器队列中删除进程描述符。
3、分别调用exit_mm()、exit_sem()、__exit_files()、__exit_fs()、exit_namespace()和exit_thread()函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及I/O权限位图相关的数据结构。如果没有其他进程共享这些数据结构,那么这些函数删除这些数据结构。
4、如果被杀死进程实现了执行域和可执行格式的内核函数包含在内核模块中,则函数递减它们的使用计数器。
5、把进程描述符的exit_code字段设置成进程终止代号
6、调用exit_notify()函数执行下面的操作:
a、更新父进程和子进程的亲属关系。如果同一线程组中有正在运行的进程,就让终止进程所创建的所有子进程都变成同一个线程组中另外一个进程的子进 程,否则让它们成为init的子进程。
b、检查被终止进程其进程描述符的exit_signal字段是否不等于-1,并检查进程是否是其所属进程组的最后一个成员。在这种情况下,函数通过给正被 终止进程的父进程发送一个信号(通常是SIGCHLD),以通知父进程子进程死亡。
c、否则,也就是exit_signal字段等于-1,或者线程组中还有其他进程,那么只要进程正在被跟踪,就向父进程发送一个SIGCHLD信号。
d、如果exit_signal字段等于-1,而且进程没有被跟踪,就把进程描述符的exit_status字段置为EXIT_DEAD,然后调用release_task()回收进 程的其他数据结构占用的内存,并递减进程描述符的使用计数器。
e、否则,如果进程描述符的exit_signal字段不等于-1,或者进程在被跟踪,就把exit_state字段置为EXIT_ZOMBIE。
f、把进程描述符的flags字段设置为PF_DEAD标志。
7、调用schedule()函数选择一个新进程运行。调度程序忽略处于EXIT_ZOMBIE状态的进程,所以这种进程正好在schedule()中的宏switch_to被调用 之后停止执行。
进程删除
Unix运行进程查询内核以获得其父进程的PID,或者其任何子进程的执行状态。例如,进程可以创建一个子进程来执行特定的任务,然后调用诸如wait()这样的一些库函数检查子进程是否终止。如果子进程已经终止,那么,它的终止代号将告诉父进程这个任务是否已经成功完成。
为了遵循这些设计选择,不允许Unix内核在进程一终止后就丢弃包含在进程描述符字段中的数据。只有父进程发出了与被终止的进程相关的wait()类系统调用之后,才允许这样做。这就是引入僵死状态的原因:尽管从技术上来说进程已经死了,但必须保存它的描述符,直到父进程得到通知。
如果父进程在子进程结束之前结束,必须强迫所有的孤儿进程成为init进程的子进程来解决这个问题。