1、进程描述符(task_struct)某些字段含义,假设进程为P。
state:P进程状态,用set_task_state和set_current_state宏更改之,或直接赋值。
thread_info:指向thread_info结构的指针。
run_list:假设P状态为TASK_RUNNING,优先级为k,run_list将P连接到优先级为k的可运行进程链表中。
tasks:将P连接到进程链表中。
ptrace_children:链表头,链表中的所有元素是被调试器程序跟踪的P的子进程。
ptrace_list:P被调试时,链表中的所有元素是被调试器程序跟踪的P的子进程。
pid:P进程标识(PID)。
tgid:P所在的线程组的领头进程的PID。
real_parent:P的真实的父进程的进程描述符指针。
parent:P的父进程的进程描述符指针,当被调试时就是调试器进程的描述符指针。
children:P的子进程链表。
sibling:将P连接到P的兄弟进程链表。
group_leader:P所在的线程组的领头进程的描述符指针。
2、PID导出进程描述符
有些情况需要从PID得到响应的进程描述符指针,比如kill()系统调用。由于顺序扫描进程链表并检查进程描述符的pid字段是比较低效的,因此引入了4个哈希表:
PIDTYPE_PID
PIDTYPE_TGID
PIDTYPE_PGID
PIDTYPE_SID
这四个哈希表在内核初始化时动态地分配空间,它们的地址被存入pid_hash数组,其长度依赖于RAM容量。利用pid_hashfn可以将PID转化为表索引。
为了防止出现哈希运算带来的冲突,Linux采用拉链法来解决,即引入具有链表的哈希表来处理。
1、执行进程切换
从本质上说,每个进程切换由两步组成:发生在schedule()函数
-
切换页全局录以安装一个新的地址空间;
-
切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。
进程切换的第二节由switch_to宏执行。
2、进程的创建
传统的Unix操作系统以统一的方式对待所有的进程;子进程复制父进程所拥有的资源。这种方法使进程的创建非常慢且效率低。因为子进程需要拷贝父进程的整个地址空间。实际上,子进程几乎不必读或修改父进程拥有的所有资源,在很多情况下,子进程立即调用execve(),并清除父进程仔细拷贝过来的地址空间。
现代Unix内核通过引入三种不同的机制解决了这个问题:
-
写时复制技术允许父子进程读相同的物理页。只要两者中有一个试图写一个物理页。内核就把这个页的内容拷贝到一个新的物理页,并把这个新物理页分配给正在写的进程。
-
轻量级进程允许父子进程共享每进程在内核的很多数据结构,如页表,打开文件表及信号处理。
-
vfork()系统调用创建的进程共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出或执行一个新程序为止。
不管是clone、fork还是vfork系统调用,他们的实现函数sys_clone、sys_fork和sys_vfork都指向了位于/kernel/Fork.c中的do_fork函数,唯一的不同就是clone_flags的不同,具体含义请参考前一博文。
这里,我们就来详细分析进程创建的实务函数 —— do_fork()
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)
先介绍一下它执行时使用的参数:
clone_flags:与clone()参数flags相同。
stack_start:与clone()参数stack_start相同。
regs:指向内核态堆栈通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的。
stack_size:未使用,总被设置为0。
parent_tidptr,child_tidptr:与clone系统调用中对应参数ptid和ctid相同。
do_fork()函数利用辅助函数copy_process()来创建进程描述符以及子进程所需要的其他所有数据结构。下面是do_fork()函数执行的主要步骤:
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标志,或者必须跟踪子进程,即在p->ptrace 中设置 PT_PTRACED标志,那么子进程的状态被设置成TASK_STOPPED状态,并且为子进程增加挂起的SIGSTOP信号。在另一个进程把子进程状态恢复成TASK_RUNNING之前,一直保持该状态。
5. 如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task(p, clone_flags)函数以执行以下操作:
a.调整父进程和子进程的调度参数
b.如果子进程和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表(CLONE_VM标志被清0),那么,就把子进程插入到父进程的运行队列,插入时让子进程恰好在父进程前面,因此迫使子进程优于父进程先运行。如果子进程刷新其地址空间,并且在创建之后执行新程序,那么这种简单的处理会产生较好的性能。而如果我们让父进程先运行,那么写时复制机制将会执行一些不必要的页面复制。
c.否则,如果子进程与父进程运行在不同CPU上,或者父进程和子进程共享同一组页表(CLONE_VM标志被设置),就把子进程插入父进程所在运行队列的队尾。
6. 如果设置了CLONE_STOPPED标志,则子进程的状态被设置成TASK_STOPPED状态。
7. 如果父进程被跟踪,则把子进程的PID存入current的ptrace_message字段并调用ptrace_notify函数使当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号。子进程的祖父进程是跟踪父进程的debugger进程。SIGCHLD信号通知debugger进程:当前进程current已经创建了一个子进程,可以通过current->ptrace_message字段获得该子进程的PID。
8. 如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间(也就是说,直到子进程结束或执行新的程序)。
9. 结束并返回子进程的PID。
3、进程终止do_exit()
1、将task_struct中的标志成员设置为PF_EXITING,表明该进程正在被删除
|--------------------------------|
| tsk->flags |= PF_EXITING; |
|--------------------------------|
2、从进程描述符中分离出于分页、信号量、文件系统、打开的文件描述符、命名空间等数据结构
放弃进程进程占用的mm_struct,如果没有别的进程使用它们(也就是说,它们没被共享),就彻底释放它们。
|------------------|
| exit_mm(tsk); -|
|------------------|
如果进程排队等候IPC信号,它则离开队列
|-------------------|
| exit_sem(tsk); -|
|-------------------|
分别递减本件描述符、文件系统数据、进程名字空间的引用计数。如果其中某些引用计数的数值降为0,那么就代表没有进程在使用相应的资源,此时可以释放
|------------------------|
| __exit_files(tsk); |
| __exit_fs(tsk); |
| exit_namespace(tsk); |
|------------------------|
exit_thread();
cpuset_exit(tsk);
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);
3、把task_struct.exit_code(任务退出代码)置为exit()提供的代码code(终止代码)(退出代码存放在task_struct.exit_code中以供父进程随时检索)
|--------------------------|
| tsk->exit_code = code; |
|--------------------------|
4、向父进程发送信号;将当前进程的子进程的父进程重新设置为线程组中的其他线程或者init进程,并把进程状态设成TASK_ZOMBIE
|--------------------------|
| exit_notify(tsk); |
|--------------------------|
#ifdef CONFIG_NUMA
mpol_free(tsk->mempolicy);
tsk->mempolicy = NULL;
#endif
BUG_ON(!(current->flags & PF_DEAD));
5、切换到其他进程。因为处于TASK_ZOMBIE状态的进程不会再被调用,所以这是进程所执行的最后一段代码。此时进程占用的资源就是内核堆栈、 thread_info结构、 task_struct结构。进程此时存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是不关的信息后,由进程所持有的剩余 内存被释放,归还给系统使用。
|------------------|
| schedule(); |
|------------------|