系列文章目录
Linux 内核设计与实现
深入理解 Linux 内核
Linux 设备驱动程序
Linux设备驱动开发详解
深入理解Linux虚拟内存管理
Linux 内核源代码情景分析(一)
Linux 内核源代码情景分析(二)
文章目录
第 4 章 进程与进程调度
1、进程四要素
要给“进程”下一个确切的定义不是件容易的事。不过,一般来说 Linux 系统中的进程都具备下列诸要素:
- 有一段程序供其执行,就好像一场戏要有个剧本一样。这段程序不一定是进程所专有,可以与其他进程共用,就好像不同剧团的许多场演出可以共用一个剧本一样。
- 有起码的 “秘行财产” ,这就是进程专用的系统堆栈空间。
- 有 “户口 ” ,这就是在内核中的一个 task_struct 数据结构,操作系统教科书中常称为“进程控制块”。有了这个数据结构,进程才能成为内核调度的一个基本单位接受内核的调度。同时, 这个结构又是进程的“财产登记卡”,记录着进程所占用的各项资源。
- 有独立的存储空间,意味着拥行专有的用户空间;进一步,还意味着除前述的系统空间堆栈外还有其专用的用户空间堆栈。注意,系统空间是不能独立的,任何进程都不可能直接 (不通过系统调用) 改变系统空间的内容 (除其本身的系统空间堆栈以外) 。
这四条都是必要条件,缺了其中任何一条就不称其为“进程”。如果只具备了前面三条而缺第四条, 那就称为 “线程” 。特别地,如果完全没有用户空间,就称为“内核线程” (kernel thread) ;而如果共享用户空间则就称为 “用户线程” 。在不致引起混淆的场合,二者也都往往简称为“线程”。读者在第 2 章中看到过的 kswapd ,就是一个内核线程。读者要注意,不要把这里的“线程”与有些系统中在用户空间的同一进程内实现的 “线程” 相混淆。那种线程显然不拥有独立、专用的系统堆栈,也不作为一个调度单位直接受内核调度。而且,既然 Linux 内核提供了对线程的支持,一般也就没有必要再在进程内部,即用户空间中自行实现线程。
另一方面,进程与线程的区分也不是十分严格的,一般在讲到进程时常常也包括了线程。事实上, 在 Linux (以及Unix) 系统中,许多进程在“诞生”之初都与其父进程共用同一个存储空间,所以严格说来还是线程;但是子进程可以建立其自己的存储空间,并与父迸程分道扬镳,成为真止意义上的进程。再说,线程也有 “pid",也有 task_struct 结构,所以这两个词在使用中有时候并不严格加以区分, 要根据上下文理解其含意。
还有,在 Linux 系统中“进程” (process) 和“任务” (task) 是同一个意思,在内核的代码中也常常混用这两个名词和概念。例如,每一个进程都要有一个 task_struct 数据结构,而其号码却又是 pid; 唤醒一个睡眠进程的函数名为 wake_up_process() 。之所以有这样的情况是因为 Linux 源自 Unix 和 i386 系统结构,而 Unix 中的进程在 Intel 的技术资料中则称为“任务”(严格说来有点差别,但是对 Linux 和 Unix 的实现来说是一码事)。
Linux 系统运行时的第一个进程是在初始化阶段“捏造”出来的。而此后的进程或线程则都是由一个业已存在的进程像细胞分裂那样通过系统调用复制出来的,称为 “fork"(分叉)或“clone”(克隆)。
除上述最起码的“财产”,即 task_struct 数据结构和系统堆栈之外,一个进程还要有些附加的资源。 例如,上面说过,“独立”的存储空问意味着进程拥有用户空间,因此就要有用于虚存管理的 mm_struct 数据结构以及下属的 vm_area 数据结构,以及相应的页面目录项和页面表。但那些都是第二位的,从属于 task_struct 的资源,而 task_struct 数据结构则在这方面起着登记卡的作用。至于进程的具体实现, 则在相当程度上取决于宿主 CPU 的系统结构。
在转入详细介绍进程的各个要素之前,我们先讲一下 i386 系统结构所提供的进程管理机制以及 Linux 内核对这种机制的特殊运用和处理。读者可以结合第2章中的有关内容阅读。
Intel 在 i386 系统结构的设计中考虑到了进程(任务)的管理和调度,并从硬件上支持任务间的切换。为此目的,Intel 在 i386 系统结构中增设了另一种新的段,叫做“任务状态段” TSS。 一个 TSS 虽说像代码段、数据段等一样,也是一个"段”,实际上却只是一个 104 字节的数据结构、或曰控制块, 用以记录一个任务的关键性的状态信息,包括:
- 任务切换前夕(也就是切入点上)该任务各通用寄存器的内容。
- 任务切换前夕(切入点上)该任务各个段寄存器(包括ES、CS、SS、DS、FS和ES)的内容。
- 任务切换前夕(切入点上)该任务EFLAGS寄存器的内容。
- 任务切换前夕(切入点上)该任务指令地址寄存器EIP的内容。
- 指向前一个任务的 TSS 结构的段选择码。当前任务执行 IRET 指令时,就返回到由这个段选择码所指的(TSS所代表的)任务(返回地址则由堆栈决定)。
- 该任务的 LDT 段选择码,它指向任务的 LDT。
- 控制寄存器 CR3 的内容,它指向任务的页面目录。
- 二个堆栈指针,分别为当任务运行于0级、1级和2级时的堆栈指针,包括堆栈段寄存器SS0、 SS1和SS2,以及ESP0、ESP1和ESP2的内容。注意,在CPU中只有一个SS和一个ESP 寄存器,但是 CPU 在进入新的运行级别时会自动从当前任务的TSS中装入相应SS和ESP 的内容,实现堆栈的切换。
- 一个用于程序跟踪的标志位 T 。当T标志位为 1 时,CPU就会在切入该进程时产生一次 debug 异常,这样就可以在 debug 异常的服务程序中安排所需的操作,如加以记录、显示、等等。
- 在一个 TSS 段中,除了基本的104字节的TSS结构以外,还可以有一些附加的信息。其中之一是表示I/O权限的位图。i386系统结构允许 I/O 指令在比0级为低的状态下执行,也就是说可以将外设驱动实现于一个既非内核(0级)也非用户(3级)的空间中,这个位图就是用于这个目的。另一个是“中断重定向位图”,用于 vm86 模式。
前面讲过,每个进程都有一个 task_struct 数据结构和一片用作系统空间堆栈的存储空间。这二者缺一不可,又有紧密的联系,所以在物理存储空间中也连在一起。内核在为每个进程分配一个 task_struct 结构时,实际上分配两个连续的物理页面 (共8192字节) 。这两个页面的底部用作进程的 task_struct 结构,而在结构的上面就用作进程的系统空间堆栈。下图为进程系统堆栈示意图:
(1)运行状态
// include/linux/sched.h
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
状态 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 均表示进程处于速眠状态。但是, TASK_UNINTERRUPTIBLE 表示进程处于 “深度睡眠” 而不受 “信号” ( signal,也称 “软中断” ) 的打扰,而 TASK_INTERRUPTIBLE 则可以因 “信号” 的到来而被唤醒。内核中提供了不同的函数,让一个进程进入不同深度的睡眠或将进程从睡眠中唤醒。具体地说,函数 sleep_on() 和 wake_up() 用于深度睡眠,而 interruptible_sleep_on() 和wake_up_interruptible() 则用于浅度睡眠。深度睡眠一般只用于临界区和关键性的部位,而 “可中断” 的睡眠那就是通用的了。特别,当进程在 “阻塞性”(blocking) 的系统调用中等将某一事件发生时,应该进入“可中断” 睡眠间不应深度睡眠。例如,当进程等待操作人员按某个键的时候,就不应该进入深度睡眠,否则就不能对别的事件作出反应,别的进程就不能通过发一个信号来 “杀” 掉这个进程了。还应该注意,这里的 INTERRUPTIBLE 或 UNINTERRUPTIBLE 跟 “中断” 毫无关系,而只是说睡眠能否因其他事件而中断,即唤醒。不过,所谓其他事件主要是 “信号”,而信号的概念实际上与中断的概念是相同的,所以这里所谓 INTERRUPTIBLE 也是指这种 “软中断” 而言。
TASK_RUNNING 状态并不是表示一个进程止在执行中,或者说这个进程就是 “当前进程”,而是表示这个进程可以被调度执行而成为当前进程。当进程处于这样的可执行(或就绪)状态时,内核就将该进程的 task_struct 结构通过其队列头 run_list (见309行)挂入一个 “运行队列”。
TASK_ZOMBIE 状态表示进程已经 “去世”(exit)而 “户口” 尚未注销。
TASK_STOPPED 主要用于调试目的。进程接收到一个 SIGSTOP 信号后就将运行状态改成 TASK_STOPPED 而进入“挂起”状态,然后在接收到一个 SIGCONT 信号时又恢复继续运行。
2、进程三部曲:创建、执行与消亡
就像世上万物都有产生、发展与消亡的过程一样,每个进程也有被创建、执行某段程序以及最后消亡的过程。在 Linux 系统中,第一个进程是系统固有的、与生俱来的或者说是由内核的设计者安排好了的。内核在引导并完成了基本的初始化以后,就有了系统的第一进程(实际上是内核线程)。除此之外,所有其他的进程和内核线程都这个原始进程或其子孙进程所创建,都是这个原始进程的 “后代” 。在 Linux 系统中,一个新的进程一定要由一个已经存在的进程“复制”出来,而不是“创造”出来(而所谓“创建”实际就是复制)。所以,Linux 系统(Unix也一样)并不向用户(即进程)提供类似这样的系统调用:
int creat_proc(int (*fn)(void*), void *arg, unsigned long options);
可是在很多操作系统(包括一些Unix的变种)中都采用了 “一揽子” 的方法。它“创造”出一个进程,并使该进程从函数指针 fn 所指的地方开始执行。根据不同的情况和设计,参数 fn 也可以换成一个可执行程序的文件名。这里所谓“创造”,包括为进程分配所需的资源、包括属于最低限度的 task_struct 数据结构和系统空间堆栈,并初始化这些资源;还要设置其系统空间堆栈,使得这个新进程看起来就好像是一个本来就已经存在而正在唾眠的进程。当这个进程被调度运行的时候,其“返回地址”,也就是“恢复”运行时的下一条指令,则就在 fn 所指的地方。这个“子进程” 生下来时两手空空,却可以完全独立,并不与其父进程共享资源。
但是,Linux (以及Unix)采用的方法却不同。
Linux 将进程的创建与目标程序的执行分成两步。第一步是从已经存在的“父进程”中像细胞分裂一样地复制出一个 “子进程” 。这里所谓像“细胞分裂一样”,只是打个比方,实际上,复制出来的子进程有自己的 task_struct 结构和系统空间堆栈,但与父进程共享其他所有的资源。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。 所以,这一步所做的是“复制”。Linux 为此提供了两个系统调用,一个是 fork(),另一个是 clone() 。 两者的区别在于 fork() 是全部复制,父进程所有的资源全都通过数据结构的复制“遗传”给步进程。 而 clone() 则可以将资源有选择地复制给厂进程,而没有复制的数据结构则通过指针的复制让子进程共享。在极端的情况下,一个进程可以 clone() 出一个线程。所以,系统调用 fork() 是无参数的,而 clone() 则带有参数。读者也许已经意识到,fork() 其实比clone() 更接近本来意义上的“克隆”。确实是这样, 原因在于 fork() 从 Unix 的初期即已存在,那时候“克隆”这个词还不像现在这么流行,而既然业已存在,就不宜更改了。否则,也许应该互换一下名字。后来,又增设了一个系统调用 vfork() ,也不带参数,但是除 task_struct 结构和系统空间堆栈以外的资源全都通过数据结构指针的复制“遗传”,所以 vfork() 出来的是线程而不是进程。读者将会看到,vfork() 主要是出于效率的考虑而设计并提供的。
第二步是目标程序的执行。一般来说,创建一个新的进程是因为有不同的目标程序要让新的程序去执行 (但也不一定),所以,复制完成以后,子进程通常要与父进程分道扬镳,走自己的路。Linux 为此提供了一个系统调用 execve(),让一个进程执行以文件形式存在的一个可执行程序的映象。
读者也许要问:这两种方案到底哪一种好?应该说是各有利弊。但是更应该说,Linux从Unix继承下来的这种分两步走,并且在第一步中采取复制方式的方案,利远大于弊。从效率的角度看,分两步走很行好处。所谓复制,只是进程的基本资源的复制,如 task_struct 数据结构、系统空间堆栈、页面表等等,对父进程的代码及全局变量则并不需要复制,而只是通过只读访问的形式实现共享,仅在需要写的时候才通过 copy_on_write 的手段为所涉及的页面建立个新的副本。所以,总的来说复制的代价是很低的,但是通过复制而继承下来的资源则往往对子进程很有用。读者以后会看到,在计算机网络的实现中,以及在 client/server 系统中的 server 一方的实现中,fork() 或 clone() 常常是最自然、最有效、最适宜的手段。笔者有时候简直怀疑,到底足先有 fork() 还是先有 client/server,因为 fork() 似乎就是专门为此而设计的。更重要的好处是,这样有利于父、子进程间通过 pipe 来建立起一种简单有效的进程间通信管道,并且从而产生了操作系统的用户界面即 shell 的 “管道” 机制。这一点,对于 Unix 的发展和推广应用,对于Unix程序设计环境的形成,对于Unix程序设计风格的形成,都有着非常深远的影响。可以说,这是一项天才的发明,它在很大程度上改变了操作系统的发展方向。
当然,从另一角度,也就是从程序设计界面的角度来看,则“一揽子”的方案更为简洁。不过 fork() 加 execve() 的方案也并不复杂很多。进一步说,这也像练武或演戏一样有个固定的“招式”,一一掌握了以后就不觉得复杂,也很少变化了。再说,如果有必要也可以通过程序库提供一个 “一揽子” 的库函数,将这两步包装在一起。
创建了子进程以后,父进程有二个选择。第一是继续走自己的路,与父进程分道扬镳。只是如果子进程先于父进程“去世”,则由内核给父进程发一个报丧的信号。第二是停下来,也就是进入睡眠状态,等待了一进程完成其使命而最终去世,然后父进程再继续运行。Linux为此提供了两个系统调用, wait4() 和 wait3()。两个系统调用基本相同,wait4() 等行某个特定的子进程去世,而 wait3() 则等待任何一个子进程去世。第三个选择是“自行退出历史舞台”,结束自己的生命。Linux 为此设置了一个系统调用 exit() 。这里的第三个选择其实不过是第一个选择的一种特例,所以从本质上说是两种选择:一种是父进程不受阻的 (non_blocking) 方式,也称为“异步”的方式;另一种是父进程受阻的 (blocking) 方式,或者也称为 “同步” 的方式。
3、系统调用fork()、vfork()、与clone()
前面已经简要地介绍过 fork() 与 clone() 二者的作用和区别。这里先来看一下二者在程序设计接口上的不同:
#include <unistd.h>
pid_t fork(void);
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
系统调用 __clone() 的主要用途是创建一个线程,这个线程可以是内核线程,也可以是用户线程。 创建用户空间线程时,可以给定子线程用户空间堆栈的位置,还可以指定子进程运行的起点。同时, 也可以用 __clone() 创建进程,有选择地复制父进程的资源。而 fork() ,则是全面地复制。还有一个系统调用 vfork() ,其作用也是创建一个线程,但主要只是作为创建进程的中间步骤,目的在于提高创建时的效率,减少系统开销,其程序设计接口则与fork相同。
这几个系统调用的代码都在 arch/i386/kernel/process.c 中:
// arch/i386/kernel/process.c
asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0);
}
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx;
newsp = regs.ecx;
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s, 0);
}
/*
* This is trivial, and on the face of it looks like it
* could equally well be done in user mode.
*
* Not so, for quite unobvious reasons - register pressure.
* In user mode vfork() cannot have a stack frame, and if
* done by calling the "clone()" system call directly, you
* do not have enough call-clobbered registers to hold all
* the information you need.
*/
asmlinkage int sys_vfork(struct pt_regs regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0);
}
可见,二个系统调用的实现都是通过 do_fork() 来完成的,不同的只是对 do_fork() 的调用参数。关于这些参数所起的作用,读了 do_fork() 的代码以后就会清楚。注意 sys_clone() 中的regs.ecx,就是调用 __clone() 时的参数 child_stack,读者如果还不清楚,可以回到第3章“系统调用” 节顺着代码再走一遍。调用 __clone() 时可以为子进程设置一个独立的用户空间堆栈 (在同一个用户空间中),如果 child_stack 为 0,就表示使用父进程的用户空间堆栈。这三个系统调用的主体部分 do_fork() 是在 kernel/fork.c 中定义的。这个函数比较大,计我们逐段往下看:
(1)do_fork()
// kernel/fork.c
/*
* Ok, this is the main fork-routine. It copies the system process
* information (task[nr]) and sets up the necessary registers. It also
* copies the data segment in its entirety. The "stack_start" and
* "stack_top" arguments are simply passed along to the platform
* specific copy_thread() routine. Most platforms ignore stack_top.
* For an example that's using stack_top, see
* arch/ia64/kernel/process.c.
*/
int do_fork(unsigned long clone_flags, unsigned long stack_start,
struct pt_regs *regs, unsigned long stack_size)
{
int retval = -ENOMEM;
struct task_struct *p;
// 第560行的宏操作DECLARE_MUTEX_LOCKED()定义和创建了一个用于进程间互斥和同步的信
// 号量,其定义和实现见第6章“进程间通信”。
DECLARE_MUTEX_LOCKED(sem);
// 参数 clone_flags 由两部分组成,其最低的字行为信号类型,用以规定子进程去世时应该向父进程
// 发出的信号。我们也经看到,对于fork()和vfork()这个信号就是SIGCHLD,而对__clone()则该位段
// 可由调用者决定。第二部分是一些表示资源和特性的标志位,这些标志位是在
// include/linux/sched.h 中定义的:
// 对于fork(),这一部分为全0,表现对有关的资源都要复制而不是通过指针共享。向对vfbrk(),
// 则为 CLONE_VFORK | CLONE_VM ,表示父、子进程共用(用户)虚存区间,并且当子进程释放其虚
// 存区间时要唤醒父进程。至于__clone(),则这一部分完全由调用者设定而作为参数传递下来。其中标
// 志位CLONE_PID有特殊的作用,当这个标志位为1时,父、子进程(线程)共用同一个进程号,也
// 就是说,子进程虽然有其自己的task_struct数据结构,却使用父进程的pid。但是,只有0号进程,也
// 就是系统中的原始进程(实际上是线程),才允许这样来调用__clone(),所以564行对此加以检查。
if (clone_flags & CLONE_PID) {
/* This is only allowed from the boot up thread */
if (current->pid)
return -EPERM;
}
current->vfork_sem = &sem;
// 接着,通过alloc_task_struct()为子进程分配两个连续的物理页而,低端用作子进程的task_struct
// 结构,高端则用作其系统空间堆栈.
p = alloc_task_struct();
if (!p)
goto fork_out;
// 注意574行的赋值为整个数据结构的赋值。这样,父进程的整个 task_struct 就被复制到了子进程
// 的数据结构中。经编译以后,这样的赋值是用memcpyS实现的,所以效率很高。
*p = *current;
retval = -EAGAIN;
// 在task_struct结构中有个指针user,用来指向一个user_struct结构。一个用户常常有许多个进程,
// 所以有关用户的一些信息并不专属于某一个进程。这样,属于同一用户的进程就可以通过指针user共
// 享这些信息。显然,每个用户有且只有一个user_struct结构。结构中有个计数器count,对属于该用户
// 的进程数量计数。可想而知,内核线程并不属于某个用户,所以其task_struct中的user指针为0。这个
// 数据结构的定义在include/lmux/sched.h中:
//
// 各进程的 task_struct 结构中还有个数组 rlim,对该进程占用各种资源的数量作出限制,而
// rlim[RLIMIT_NPROC] 就规定了该进程所属的用户可以拥有的进程数量。所以,如果当前进程是
// 一个用户进程,并且该用户拥有的进程数量己经达到了规定的限制值,就再不允许它 fork()
// 了。
if (atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur)
goto bad_fork_free;
atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
/*
* Counter increases are protected by
* the kernel lock so nr_threads can't
* increase under us (but it may decrease).
*/
// 那么,对于不属于任何用户的内核线程怎么办呢? 587行中的两个计数器就是为进程的总量而设的。
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
// 一个进程除了属于某一个用户之外,还属于某个“执行域”。总的来说,Linux是Unix的一个变种,
// 并且符合POSIX的规定。但是,有很多版本的操作系统同样是Unix变种,同样符合POSIX规定,互
// 相之间在实现细节上却仍然有明显的不同。例如,AT&T的Sys V和BSD4.2就有相当的不同,而Sun
// 的Solaris又有区别,这就形成了不同的执行域。如果一个进程所执行的程序是为Solaris开发的,那么
// 这个进程就属于Solaris执行域PER_SOLARIS。当然,在Linux上运行的绝大多数程序都属于Linux
// 执行域。在task_struct结构中有一个指针exec_domain,可以指向一个exec_domain数据结构。
// 那是在 include/linux/personality.h 中定义的
get_exec_domain(p->exec_domain);
// 同样的道理,每个进程所执行的程序属于某种可执行映象格式,如a.out格式、elf格式、甚至java
// 虚拟机格式。对这些不同格式的支持通常是通过动态安装的驱动模块来实现的。所以task_struct结构
// 中还有一个指向 linux_binfmt 数据结构的指针binfmt , 而 do_fork()中593行的
// __MOD_INC_USE_COUNT() 就是对有关模块的使用计数器进行操作。
if (p->binfmt && p->binfmt->module)
__MOD_INC_USE_COUNT(p->binfmt->module);
p->did_exec = 0;
p->swappable = 0;
// 为什么要在597行把状态设成TASK_UNINTERRUPTIBLE呢?这是因为在 get_pid()中产生一个
// 新pid的操作必须是独占的,当前进程可能会因为一时进不了临界区而只好暂时进入睡眠状态等待,
// 所以才事先把状态设成 UNINTERRUPTIBLE。
p->state = TASK_UNINTERRUPTIBLE;
// 函数 copy_flags() 将参数clone_flags中的标志位略加补充和变换,然后写入p->flags,
// 这个函数的代码也在fork.c中。读者可以自己阅读。
copy_flags(clone_flags, p);
// 至于600行的 get_pid(),则根据clone_flags中标志位CLONE_PID的值,或返回父进程(当前进
// 程)的pid,或返回一个新的pid放在了进程的task_struct中。函数get_pid()的代码也在fork.c 中
p->pid = get_pid(clone_flags);
p->run_list.next = NULL;
p->run_list.prev = NULL;
if ((clone_flags & CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) {
p->p_opptr = current;
if (!(p->ptrace & PT_PTRACED))
p->p_pptr = current;
}
p->p_cptr = NULL;
// 我们在前一节中提到过 wait4()和wait3(), 一个进程可以停下来等待其子进程完成使命。为此,在
// task_struct 中设置了一个队列头部 wait_chldexit,前面在复制task_struct结构时把这也照抄了
// 过来,而子进程此时尚未“出生”,当然谈不上子进程的等待队列,所以要在611行中加以初始化。
init_waitqueue_head(&p->wait_chldexit);
p->vfork_sem = NULL;
spin_lock_init(&p->alloc_lock);
// 类似地,对各种信息量也要加以初始化。这里615和616行是对子进程的待处理信号队列以及有
// 关结构成分的初始化。对这些与信号有关的结构成分我们将在“进程间通信”的信号一节中详细介绍。
// 接下来是对 task_struct 结构中各种计时变量的初始化,我们将在“进程调度” 一节中介绍这些变量。
// 在这里我们不关心对多处理器SMP结构的特殊考虑,所以也跳过627一637行。
p->sigpending = 0;
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->leader = 0; /* session leadership doesn't inherit */
p->tty_old_pgrp = 0;
p->times.tms_utime = p->times.tms_stime = 0;
p->times.tms_cutime = p->times.tms_cstime = 0;
#ifdef CONFIG_SMP
{
int i;
p->has_cpu = 0;
p->processor = current->processor;
/* ?? should we just memset this ?? */
for(i = 0; i < smp_num_cpus; i++)
p->per_cpu_utime[i] = p->per_cpu_stime[i] = 0;
spin_lock_init(&p->sigmask_lock);
}
#endif
p->lock_depth = -1; /* -1 = no lock */
// 最后,task_struct 结构中的 start_time 表示进程创建的时间,向全局变量jiffies的数值就是以时钟
// 中断周期为单位的从系统初始化开始至此时的时间。
// 至此,对 task_struct 数据结构的复制与初始化就基本完成了。下面就轮到其他的资源了:
p->start_time = jiffies;
retval = -ENOMEM;
/* copy all the process information */
// 函数copy_files()有条件地复制已打开文件的控制结构,这种复制只有在clone_flags中
// CLONE_FILES 标志位为 0 时才真正进行,否则就只是共享父进程的已打开文件。当一个进程有己打开
// 文件时,task_struct 结构中的指针files指向一个files_struct数据结构,否则为0。
// 所有与终端设备tty相联系的用户进程的头三个文件,即stdin、stdout、及stderr,都是预先打开的,
// 所以指针一般不会是 0。数据结构 files_struct 是在include/linux/sched.h中定义的
//(详见“文件系统”一章),copy_files() 的代码则还是在fork.c中:
if (copy_files(clone_flags, p))
goto bad_fork_cleanup;
// 除 files_struct 数据结构外,还有个fs_struct数据结构也是与文件系统有关的,也要通过共享或复
// 制遗传给子进程。类似地,copy_fs()也是只有在clone_flags中CLONE_FS标志位为 0 时才加以复制。
// task_struct 结构中的指针指向一个fs_struct数据结构,结构中记录的是进程的根目录root、当前工作目
// 录pwd、一个用于文件操作权限管理的umask,还有一个计数器,其定义在include/linux/fs_struct.h中
// (详见“文件系统”一章)。函数copy_fs()连同几个有关低层函数的代码也在kernel/fork.c中。我们把这些代
// 码留给读者:
if (copy_fs(clone_flags, p))
goto bad_fork_cleanup_files;
// 接着是关于对信号的处理方式。是否复制父进程对信号的处理是由标志位CLONE_SIGHAND控
// 制的。信号基本上是一种进程间通信手段,信号之于一个进程就好像中断之于一个处理器。进程可以
// 为各种信号设置用于该信号的处理程序,就好像系统可以为各个中断源设置相应的中断服务程序一样.
// 如果一个进程设置了信号处理程序,其task_struct结构中的指针sig就指向一个signal_struct数据结构。
// 这种结构是在include/linux/sched.h中定义的:
if (copy_sighand(clone_flags, p))
goto bad_fork_cleanup_fs;
// 然后是用户空间的继承。进程的task_struct结构中有个指针mm,读者已经相当熟悉了,它指向一
// 个代表着进程的用户空间的mm_struct数据结构。由于内核线程并不拥有用户空间,所以在内核线程
// 的task_struct结构中该指针为0。有关mm_struct及其下属的vm_area_struct等数据结构已经在第2章
// 中介绍过,这里不再重复。
if (copy_mm(clone_flags, p))
goto bad_fork_cleanup_sighand;
// 当 CPU 从 copy_mm() 回到 do_fork() 中时,所有需要有条件复制的资源都已经处理完了。
// 读者不妨回顾一下,当系统调用fork()通过sys_fork()进入do_fork()时,其clone_flags为SIGCHLD,
// 也就是说,所有的标志位均为0,所以copy_files() 、copy_fs() 、copy_sighand()以及copy_mm()
// 全部真正执行了,这四项资源全都复制了。而当vfork()经过sys_vfork进入do_fork()时,
// 则其clone_flags为 VFORK|CLONE_VM|SIGHLD,所以只执行了 copy_files() 、copy_fs()以及
// copy_sighand();而 copy_mm(),则因标志位 CLONE_VM 为1,只是通过指针共享其父进程的mm_struct,
// 并没有一份自己的副本。这也就是说,经vfork()复制的是个线程,只能靠共享其父进程的存储空间度日,
// 包括用户空间堆栈在内。至于 __clone(),则取决于调用时的参数。当然,最终还得取决于父进程具有
// 什么资源, 要是父进程没有已打开文件,那么即使执行了 copy_files(),也还是空的。
//
// 回到do_fork()的代码111。前面已通过alloc_task_struct()分配了两个连续的页间,其低端用作
// task_struct结构,已经基本上复制好了;而用作系统空间堆栈的高端,却还没有复制。现在就由
// copy_thread()来做这件事了。这个函数的代码在 arch/i386/kernel/process.c 中:
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_sighand;
p->semundo = NULL;
/* Our parent execution domain becomes current domain
These must match for thread signalling to apply */
// 代码中的 parent_exec_id 表示父进程的执行域,self_exec_id 为本进程的执行域,swappable
// 表示本进程的存储页面可以被换出,exit_signal 为本进程执行exit()时应向父进程发出的信号,
// pdeath_signal 为要求父进程在执行exit()时向本进程发出的信号。
p->parent_exec_id = p->self_exec_id;
/* ok, now we should be set up.. */
p->swappable = 1;
p->exit_signal = clone_flags & CSIGNAL;
p->pdeath_signal = 0;
/*
* "share" dynamic priority between parent and child, thus the
* total amount of dynamic priorities in the system doesnt change,
* more scheduling fairness. This is only important in the first
* timeslice, on the long run the scheduling behaviour is unchanged.
*/
// 此外,task_struct 结构中 counter 字段的值就是进程的运行时间配额,这里将父进程的时间配额分成
// 两半,让父、子进程各有原值的一半。
p->counter = (current->counter + 1) >> 1;
current->counter >>= 1;
if (!current->counter)
current->need_resched = 1;
/*
* Ok, add it to the run-queues and make it
* visible to the rest of the system.
*
* Let it rip!
*/
retval = p->pid;
p->tgid = retval;
INIT_LIST_HEAD(&p->thread_group);
write_lock_irq(&tasklist_lock);
// 如果创建的是线程,则还要通过task_struct结构中的队列头thread_group与父进程链接起来,
// 形成一个 “线程组”。
if (clone_flags & CLONE_THREAD) {
p->tgid = current->tgid;
list_add(&p->thread_group, ¤t->thread_group);
}
// 接着,就要让子进程进入它的关系网了。先通过SET_LTNKS(p)将子进程的task_struct结构链入内核的
// 进程队列,然后又通过hash_pid()将其链入按其pid计算得的杂凑队列。有关这些队列的详情可参看
// “进程”以及“进程的调度与切换”两节中的有关叙述。
SET_LINKS(p);
hash_pid(p);
nr_threads++;
write_unlock_irq(&tasklist_lock);
if (p->ptrace & PT_PTRACED)
send_sig(SIGSTOP, p, 1);
// 最后,通过 wake_up_process() 将子进程“唤醒”, 也就是将其挂入可执行进程队列等待调度。
// 有关详情可参看“过程的睡眠与唤醒” 一节。
wake_up_process(p); /* do this last */
++total_forks;
// 至此,新进程的创建已经完成了,并且已经挂入了可运行进程的队列接受调度。子进程与父进程
// 在用户空间中具有相同的返回地址,然后才会因用户空间中程序的安排而分开。同时,由于当父进程
// (当前进程)从系统调用返回的前夕可能会接受调度,所以,到底谁会先返回到用户空间是不确定的。
// 不过,一般而言,由于父、子进程适用相同的调度政策,而父进程在可执行进程队列中排在子进程前
// 面,所以父进程先运行的可能较大。
fork_out:
// 还有一种特殊情况要考虑。当调用do_fork()的参数中CLONE_VFORK标志位为1时,一定要保
// 证让子进程先运行,一直到了进程通过系统调用execve()执行一个新的可执行程序或者通过
// 系统调用 exit() 退出系统时,才可以恢复父进程的运行。为什么呢?这要从用户空间的复制
// 或共享这个问题说起。 前面读者已经看到,在创建子进程时,对于父进程的用户空间可以通过
// 复制父进程的 mm_struct 及其下属的各个vm_area_struct数据结构,再加上父进程的页面目录和页面表
// 来继承;也可以简单地复制父进程的task_struct结构中指向其mm_struct结构的指针来共享,具体取决于
// CLONE_VM标志位的值。 当CLONE_VM标志位为1,因而父、子进程通过指针共享用户空间时,
// 父、子进程是在真正的意义上共享用户空间,父进程写入其用户空间的内容同时也“写入”了进程的用户空间,
// 反之亦然。如果说, 在这种情况下父、子进程各自对其数据区的写入可能会引起问题的话,
// 那么对堆栈区的写入可就是致命的了。而每次对子程序的调用都是对堆栈区的写入!由此可见,
// 在这样的情况下绝不能让两个进程都回到用户空间并发地运行;否则,必然是两个进程最终都乱来
// 一气或者因非法越界访问而死亡。解决的办法只能是“扣留”其中一个进程,而只让一个进程回到用户空间,
// 直到两个进程不再共享它们的用户空间或其中一个进程(必然是回到用户空间运行的那个进程)消亡为止。
// 所以,do_fork()中的703行利704行在CLONE_VFORK标志为 1 并且 fork 子进程成功的情况下,
// 通过让当前进程(父进程)在一个信号量上执行一次down()操作,以达到扣留父进程的目的。我们来
// 看看具体是怎样实现的。
// 首先,信号量sem是在函数开头时的560行定义的一个局部量(名曰DECLARE,实际上为之分配了空间):
if ((clone_flags & CLONE_VFORK) && (retval > 0))
// DECLARE_MUTEX_LOCKED 定义这个信号量中资源的数量为 0 。当资源数量为1时,第一个执行down()操作
// 的进程进入临界区,而使资源数量变成了 0,以后执行 down() 操作的进程便会因为资源为0而被拒之
// 门外进入睡眠,直到第一个进程归还资源离开临界区时才被唤醒。而现在这个信号量的资源从一开始
// 就是0,所以第一个对此执行down()操作的进程就会进入睡眠,一直要到某个进程往这个信号量中投
// 入资源,也就是执行一次up()操作时才会被唤醒。
//
// 那么,谁来投入资源呢?在“系统调用execve()”一节中读者将看到,子进程在通过execve()执
// 行一个新的可执行程序时会做这件事。此外,子进程在通过exit()退出系统时也会做这件事。这里还要
// 指出,这个信号量是do_fork()的一个局部变量,所以在父进程的系统空间堆栈中,而子进程在其
// task_struct结构中有指向这个信号量的指针(即 vfork_sem,见do_fork()的第554行和560行)。既然
// 父进程一直要睡眠到子进程使用这个信号量以后,信号量所在的空间就不会受到打扰。还应指出,
// CLONE_VM要与CLONE_VFORK结合使用,否则就会发生前述的问题,除非在用户程序中采取了特
// 殊的预防措施。
// 不管怎样,子进程的创建终于完成了,让我们祝福这新的生命!可是,如果子进程只具有与父进
// 程相同的可执行程序和数据,只是父进程的“影子”,那又有什么意义呢?子进程必须走自己的路,这
// 就是下一节“系统调用execve()”所要讲述的内容了。
down(&sem);
return retval;
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:
put_exec_domain(p->exec_domain);
if (p->binfmt && p->binfmt->module)
__MOD_DEC_USE_COUNT(p->binfmt->module);
bad_fork_cleanup_count:
atomic_dec(&p->user->processes);
free_uid(p->user);
bad_fork_free:
free_task_struct(p);
goto fork_out;
}
(2)clone_flags
// 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_PID 0x00001000 /* set if pid 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_SIGNAL (CLONE_SIGHAND | CLONE_THREAD)
(3)user_struct
// include/linux/sched.h
/*
* Some day this will be a full-fledged user tracking system..
*/
struct user_struct {
atomic_t __count; /* reference count */
atomic_t processes; /* How many processes does this user have? */
atomic_t files; /* How many open files does this user have? */
/* Hash table maintenance information */
struct user_struct *next, **pprev;
uid_t uid;
};
熟悉Unix内核的读者要注意,不要把Unix的进程控制结构中的 user 区与这里的 user_struct 结构相混淆,二者是截然不同的概念。在 kernel/user.c 中还定义了一个 user_struct 结构指针的数组 uidhash_table:
// kernel/user.c
#define UIDHASH_BITS 8
#define UIDHASH_SZ (1 << UIDHASH_BITS)
static struct user_struct *uidhash_table[UIDHASH_SZ];
这是一个杂凑(hash)表。对用户名施以杂凑运算,就可以计算出一个下标而找到该用户的 user_struct 结构。
(4)exec_domain
// include/linux/personality.h
/* Description of an execution domain - personality range supported,
* lcall7 syscall handler, start up / shut down functions etc.
* N.B. The name and lcall7 handler must be where they are since the
* offset of the handler is hard coded in kernel/sys_call.S.
*/
struct exec_domain {
const char *name;
lcall7_func handler;
unsigned char pers_low, pers_high;
unsigned long * signal_map;
unsigned long * signal_invmap;
struct module * module;
struct exec_domain *next;
};
函数指针 handler,用于通过调用门实现系统调用,我们并不关心。字节 pers_low 为某种域的代码, 有 PER_LINUX、PER_SVR4、PER_BSD 和 PER_SOLARIS 等等。
我们在这里主要关心的结构成分是 module,这是指向某个 module 数据结构的指针。读者在有关文件系统和设备驱动的章节中将会看到,在 Linux 系统中设备驱动程序可以设计并实现成 “动态安装模块” module,使其在运行时动态地安装和拆除。这些“动态安装模块”与运行中的进程的执行域有密切的关系。例如,一个属于 Solaris 执行域的进程算很可能要用到专门为 Solaris 设置的一些模块,只要还有一个这样的进程在运行,这些为 Solaris 所需的模块就不能拆除。所以,在描述每个一安装模块的数据结构中都有一个计数器,表明有几个进程需要使用这个模块。因此,do_fork() 中通过590行的 get_exec_domain() 递增具体模块的数据结构中的计数器 (定义在include/linux/personality.h 中)。
// include/linux/personality.h
#define get_exec_domain(it) \
if (it && it->module) __MOD_INC_USE_COUNT(it->module);
(5)get_pid
// kernel/fork.c
static int get_pid(unsigned long flags)
{
static int next_safe = PID_MAX;
struct task_struct *p;
if (flags & CLONE_PID)
return current->pid;
spin_lock(&lastpid_lock);
if((++last_pid) & 0xffff8000) {
last_pid = 300; /* Skip daemons etc. */
goto inside;
}
if(last_pid >= next_safe) {
inside:
next_safe = PID_MAX;
read_lock(&tasklist_lock);
repeat:
for_each_task(p) {
if(p->pid == last_pid ||
p->pgrp == last_pid ||
p->session == last_pid) {
if(++last_pid >= next_safe) {
if(last_pid & 0xffff8000)
last_pid = 300;
next_safe = PID_MAX;
}
goto repeat;
}
if(p->pid > last_pid && next_safe > p->pid)
next_safe = p->pid;
if(p->pgrp > last_pid && next_safe > p->pgrp)
next_safe = p->pgrp;
if(p->session > last_pid && next_safe > p->session)
next_safe = p->session;
}
read_unlock(&tasklist_lock);
}
spin_unlock(&lastpid_lock);
return last_pid;
}
这里的常数 PID_MAX 定义为 0x8000。可见,进程号的最大值是 0x7fff,即 32767。进程号 0~299 是为系统进程 (包括内核线程) 保留的,主要用于各种 “保护神” 进程。以上这段代码的逻辑并不复杂,我们就不多加解释了。
(6)copy_files
// kernel/fork.c
static int copy_files(unsigned long clone_flags, struct task_struct * tsk)
{
struct files_struct *oldf, *newf;
struct file **old_fds, **new_fds;
int open_files, nfds, size, i, error = 0;
/*
* A background process may not have any files ...
*/
// 读者可以在学习了 “文件系统” 一章以后再回过头来仔细阅读这段代码,我们在这里先作一些解释。
// 先看复制的方向。因为是当前进程在创建子进程,是从当前进程复制到子进程,所以把当前进程的
// task_struct 结构中的 files_struct 结构指针作为 oldf 。
oldf = current->files;
if (!oldf)
goto out;
// 再看复制的条件。如果参数 clone_flags 中的CLONE_FILES标志位为1,就只是通过 atomic_inc()
// 递增当前进程的files_struct结构中的共享计数,表示这个数据结构现在多了一个“用户”,就返回了。
// 由于在此之前已通过数据结构赋值将当前进程的整个task_struct结构都复制给了子进程,结构中的指
// 针files自然也复制到了子进程的task_struct结构中,使子进程通过这个指针共享当前进程的
// files_struct 数据结构。
if (clone_flags & CLONE_FILES) {
atomic_inc(&oldf->count);
goto out;
}
tsk->files = NULL;
error = -ENOMEM;
// 否则,如果CLONE_FILES标志位为0,那就要复制了。首先通过kmem_cache_alloc() 为子进程分配一个
// files_struct 数据结构作为newf,然后从oldf把内容复制到newf。
newf = kmem_cache_alloc(files_cachep, SLAB_KERNEL);
if (!newf)
goto out;
atomic_set(&newf->count, 1);
// 在files_struct数据结构
// 中有三个主要的“部件”。其一是个位图,名为close_on_exec_init;其二也是位图,名为open_fds_init;
// 其三则是 file 结构数组 fd_array[]。这七个部件都是固定大小的,如果打开的文件数量超过其容量,就
// 得通过expand_fdset()和expand_fd_array()在files_struct数据结构以外另行分配空间作为替换。
// 不管是采用files_struct数据结构内部的这三个部件或是采用外部的替换空间,指针close_on_exec、
// open_fds 和 fd 总是分别指向这二组信息。所以,如何复制取决于已打开文件的数量。
newf->file_lock = RW_LOCK_UNLOCKED;
newf->next_fd = 0;
newf->max_fds = NR_OPEN_DEFAULT;
newf->max_fdset = __FD_SETSIZE;
newf->close_on_exec = &newf->close_on_exec_init;
newf->open_fds = &newf->open_fds_init;
newf->fd = &newf->fd_array[0];
/* We don't yet have the oldf readlock, but even if the old
fdset gets grown now, we'll only copy up to "size" fds */
size = oldf->max_fdset;
if (size > __FD_SETSIZE) {
newf->max_fdset = 0;
write_lock(&newf->file_lock);
error = expand_fdset(newf, size);
write_unlock(&newf->file_lock);
if (error)
goto out_release;
}
read_lock(&oldf->file_lock);
open_files = count_open_files(oldf, size);
/*
* Check whether we need to allocate a larger fd array.
* Note: we're not a clone task, so the open count won't
* change.
*/
nfds = NR_OPEN_DEFAULT;
if (open_files > nfds) {
read_unlock(&oldf->file_lock);
newf->max_fds = 0;
write_lock(&newf->file_lock);
error = expand_fd_array(newf, open_files);
write_unlock(&newf->file_lock);
if (error)
goto out_release;
nfds = newf->max_fds;
read_lock(&oldf->file_lock);
}
old_fds = oldf->fd;
new_fds = newf->fd;
memcpy(newf->open_fds->fds_bits, oldf->open_fds->fds_bits, open_files/8);
memcpy(newf->close_on_exec->fds_bits, oldf->close_on_exec->fds_bits, open_files/8);
for (i = open_files; i != 0; i--) {
struct file *f = *old_fds++;
if (f)
get_file(f);
*new_fds++ = f;
}
read_unlock(&oldf->file_lock);
/* compute the remainder to be cleared */
size = (newf->max_fds - open_files) * sizeof(struct file *);
/* This is long word aligned thus could use a optimized version */
memset(new_fds, 0, size);
if (newf->max_fdset > open_files) {
int left = (newf->max_fdset-open_files)/8;
int start = open_files / (8 * sizeof(unsigned long));
memset(&newf->open_fds->fds_bits[start], 0, left);
memset(&newf->close_on_exec->fds_bits[start], 0, left);
}
tsk->files = newf;
error = 0;
out:
return error;
out_release:
free_fdset (newf->close_on_exec, newf->max_fdset);
free_fdset (newf->open_fds, newf->max_fdset);
kmem_cache_free(files_cachep, newf);
goto out;
}
显而易见,共享比复制要简单得多。那么这二者在效果上到底有什么区别呢?如果共享就可以达到目的,为什么还要不辞辛劳地复制呢?区别在于子进程 (以及父进程本身) 是否能 “独立自主” 。当复制完成之初,子进程有了一份副本,它的内容与父进程的“正本”在内容上基本是相同的,在这一点上似乎与共享没有什么区别。可是,随后区别就来了。在共享的情况下,两个进程是互相牵制的。如果子进程对某个已打开文件调用了一次 lseek(),则父进程对这个文件的读写位置也随着改变了,因为两个进程共享着对文件的同一个读写上下文。而在复制的情况下就不一样了,由于子进程有自己的副本,就有了对同一文件的另一个读写上下文,以后就可以各走各的路,互不干扰了。
(7)copy_fs
// include/linux/fs_struct.h
struct fs_struct {
atomic_t count;
rwlock_t lock;
int umask;
struct dentry * root, * pwd, * altroot;
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};
// ===============================================================================
// kernel/fork.c
static inline int copy_fs(unsigned long clone_flags, struct task_struct * tsk)
{
if (clone_flags & CLONE_FS) {
atomic_inc(¤t->fs->count);
return 0;
}
tsk->fs = __copy_fs_struct(current->fs);
if (!tsk->fs)
return -1;
return 0;
}
static inline struct fs_struct *__copy_fs_struct(struct fs_struct *old)
{
struct fs_struct *fs = kmem_cache_alloc(fs_cachep, GFP_KERNEL);
/* We don't need to lock fs - think why ;-) */
if (fs) {
atomic_set(&fs->count, 1);
fs->lock = RW_LOCK_UNLOCKED;
fs->umask = old->umask;
read_lock(&old->lock);
fs->rootmnt = mntget(old->rootmnt);
fs->root = dget(old->root);
fs->pwdmnt = mntget(old->pwdmnt);
fs->pwd = dget(old->pwd);
if (old->altroot) {
fs->altrootmnt = mntget(old->altrootmnt);
fs->altroot = dget(old->altroot);
} else {
fs->altrootmnt = NULL;
fs->altroot = NULL;
}
read_unlock(&old->lock);
}
return fs;
}
代码中的 mntget() 和 dget() 都是用来递增相应数据结构中共享计数的,因为这些数据结构现在多了一个用户。注意,在这里要复制的是 fs_struct 数据结构,而并不复制更深层的数据结构。复制了 fs_struct 数据结构,就在这一层上有了自主性,至于对更深层的数据结构则还是共享,所以要递增它们的共享计数。
(8)copy_sighand
// include/asm-i386/signal.h
struct sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void);
sigset_t sa_mask; /* mask last for extensibility */
};
struct k_sigaction {
struct sigaction sa;
};
// ===============================================================================
// include/linux/sched.h
struct signal_struct {
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
};
// 其中的数组 action[] 确定了一个进程对各种信号(以信号的数值为下标)的反应和处理,子进程可
// 以通过复制或共享把它从父进程继承下来。
// ===============================================================================
static inline int copy_sighand(unsigned long clone_flags, struct task_struct * tsk)
{
struct signal_struct *sig;
if (clone_flags & CLONE_SIGHAND) {
atomic_inc(¤t->sig->count);
return 0;
}
sig = kmem_cache_alloc(sigact_cachep, GFP_KERNEL);
tsk->sig = sig;
if (!sig)
return -1;
spin_lock_init(&sig->siglock);
atomic_set(&sig->count, 1);
memcpy(tsk->sig->action, current->sig->action, sizeof(tsk->sig->action));
return 0;
}
像 copy_files() 和 copy_fs() 一样,copy_sighand() 也是只有在 CLONE_SIGHAND 为 0 时才真正进行;否则就共亨父进程的 sig 指针,并将父进程的 signal_struct 中的共享计数加 1。
(9)copy_mm
// kernel/fork.c
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
tsk->min_flt = tsk->maj_flt = 0;
tsk->cmin_flt = tsk->cmaj_flt = 0;
tsk->nswap = tsk->cnswap = 0;
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
if (!oldmm)
return 0;
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
mm = allocate_mm();
if (!mm)
goto fail_nomem;
/* Copy the current MM stuff.. */
memcpy(mm, oldmm, sizeof(*mm));
if (!mm_init(mm))
goto fail_nomem;
down(&oldmm->mmap_sem);
retval = dup_mmap(mm);
up(&oldmm->mmap_sem);
/*
* Add it to the mmlist after the parent.
*
* Doing it this way means that we can order
* the list, and fork() won't mess up the
* ordering significantly.
*/
spin_lock(&mmlist_lock);
list_add(&mm->mmlist, &oldmm->mmlist);
spin_unlock(&mmlist_lock);
if (retval)
goto free_pt;
/*
* child gets a private LDT (if there was an LDT in the parent)
*/
copy_segments(tsk, mm);
if (init_new_context(tsk,mm))
goto free_pt;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
free_pt:
mmput(mm);
fail_nomem:
return retval;
}
显然,对 mm_struct 的复制也是只在 clone_flags 中 CLONE_VM 标志为 0 时才真正进行,否则就只是通过已经复制的指针共享父进程的用户空间。对 mm_struct 的复制就不只是局限于这个数据结构本身了,也包括了对更深层数据结构的复制。其中最重要的是 vm_area_struct 数据结构和页面映射表, 这是由 dup_mmap() 复制的。函数 dup_mmap() 的代码也在 fork.c 中。读者在认真读过本书第2章以后, 阅读这段程序时应该不会感到困难,同时也是一次很好的练习。
(10)dup_mmap
// kernel/fork.c
static inline int dup_mmap(struct mm_struct * mm)
{
struct vm_area_struct * mpnt, *tmp, **pprev;
int retval;
flush_cache_mm(current->mm);
mm->locked_vm = 0;
mm->mmap = NULL;
mm->mmap_avl = NULL;
mm->mmap_cache = NULL;
mm->map_count = 0;
mm->cpu_vm_mask = 0;
mm->swap_cnt = 0;
mm->swap_address = 0;
pprev = &mm->mmap;
// 这里通过140〜185行的for循环对同一用户空间中的各个区间进行复制。
for (mpnt = current->mm->mmap ; mpnt ; mpnt = mpnt->vm_next) {
struct file *file;
retval = -ENOMEM;
if(mpnt->vm_flags & VM_DONTCOPY)
continue;
tmp = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!tmp)
goto fail_nomem;
*tmp = *mpnt;
tmp->vm_flags &= ~VM_LOCKED;
tmp->vm_mm = mm;
mm->map_count++;
tmp->vm_next = NULL;
file = tmp->vm_file;
// 对于通过mmap()映射到某个文件的区间,155〜169行是一些特殊的附加处理。
if (file) {
struct inode *inode = file->f_dentry->d_inode;
get_file(file);
if (tmp->vm_flags & VM_DENYWRITE)
atomic_dec(&inode->i_writecount);
/* insert tmp into the share list, just after mpnt */
spin_lock(&inode->i_mapping->i_shared_lock);
if((tmp->vm_next_share = mpnt->vm_next_share) != NULL)
mpnt->vm_next_share->vm_pprev_share =
&tmp->vm_next_share;
mpnt->vm_next_share = tmp;
tmp->vm_pprev_share = &mpnt->vm_next_share;
spin_unlock(&inode->i_mapping->i_shared_lock);
}
/* Copy the pages, but defer checking for errors */
// 172行的copy_page_range()是关键所在,这个函数逐层处理页面目录项和页面表项,
// 其代码在mm/memory.c 中
retval = copy_page_range(mm, current->mm, tmp);
if (!retval && tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp);
/*
* Link in the new vma even if an error occurred,
* so that exit_mmap() can clean up the mess.
*/
*pprev = tmp;
pprev = &tmp->vm_next;
if (retval)
goto fail_nomem;
}
retval = 0;
if (mm->map_count >= AVL_MIN_MAP_COUNT)
build_mmap_avl(mm);
fail_nomem:
flush_tlb_mm(current->mm);
return retval;
}
(11)copy_page_range
// mm/memory.c
/*
* copy one vm_area from one task to the other. Assumes the page tables
* already present in the new task to be cleared in the whole range
* covered by this vma.
*
* 08Jan98 Merged into one routine from several inline routines to reduce
* variable count and make things faster. -jj
*/
int copy_page_range(struct mm_struct *dst, struct mm_struct *src,
struct vm_area_struct *vma)
{
pgd_t * src_pgd, * dst_pgd;
unsigned long address = vma->vm_start;
unsigned long end = vma->vm_end;
unsigned long cow = (vma->vm_flags & (VM_SHARED | VM_MAYWRITE)) == VM_MAYWRITE;
src_pgd = pgd_offset(src, address)-1;
dst_pgd = pgd_offset(dst, address)-1;
// 代码中163行的for循环是对页面目录项的循环,
for (;;) {
pmd_t * src_pmd, * dst_pmd;
src_pgd++; dst_pgd++;
/* copy_pmd_range */
if (pgd_none(*src_pgd))
goto skip_copy_pmd_range;
if (pgd_bad(*src_pgd)) {
pgd_ERROR(*src_pgd);
pgd_clear(src_pgd);
skip_copy_pmd_range:
address = (address + PGDIR_SIZE) & PGDIR_MASK;
if (!address || (address >= end))
goto out;
continue;
}
if (pgd_none(*dst_pgd)) {
if (!pmd_alloc(dst_pgd, 0))
goto nomem;
}
src_pmd = pmd_offset(src_pgd, address);
dst_pmd = pmd_offset(dst_pgd, address);
// 188行的do循环是对中间目录项的循环
do {
pte_t * src_pte, * dst_pte;
/* copy_pte_range */
if (pmd_none(*src_pmd))
goto skip_copy_pte_range;
if (pmd_bad(*src_pmd)) {
pmd_ERROR(*src_pmd);
pmd_clear(src_pmd);
skip_copy_pte_range:
address = (address + PMD_SIZE) & PMD_MASK;
if (address >= end)
goto out;
goto cont_copy_pmd_range;
}
if (pmd_none(*dst_pmd)) {
if (!pte_alloc(dst_pmd, 0))
goto nomem;
}
src_pte = pte_offset(src_pmd, address);
dst_pte = pte_offset(dst_pmd, address);
// 211行的do循环则是对页面表项的循环。
do {
pte_t pte = *src_pte;
struct page *ptepage;
/* copy_one_pte */
if (pte_none(pte))
goto cont_copy_pte_range_noset;
if (!pte_present(pte)) {
swap_duplicate(pte_to_swp_entry(pte));
goto cont_copy_pte_range;
}
ptepage = pte_page(pte);
if ((!VALID_PAGE(ptepage)) ||
PageReserved(ptepage))
goto cont_copy_pte_range;
/* If it's a COW mapping, write protect it both in the parent and the child */
if (cow) {
ptep_set_wrprotect(src_pte);
pte = *src_pte;
}
/* If it's a shared mapping, mark it clean in the child */
if (vma->vm_flags & VM_SHARED)
pte = pte_mkclean(pte);
pte = pte_mkold(pte);
get_page(ptepage);
cont_copy_pte_range:
set_pte(dst_pte, pte);
cont_copy_pte_range_noset:
address += PAGE_SIZE;
if (address >= end)
goto out;
src_pte++;
dst_pte++;
} while ((unsigned long)src_pte & PTE_TABLE_MASK);
cont_copy_pmd_range:
src_pmd++;
dst_pmd++;
} while ((unsigned long)src_pmd & PMD_TABLE_MASK);
}
out:
return 0;
nomem:
return -ENOMEM;
}
代码中163行的for循环是对页面目录项的循环,188行的do循环是对中间目录项的循环,211行 的do循环则是对页面表项的循环。我们把注意力集中在211一246行对页面表项的do_while循环。
循环中检查父进程一个页面表中的每个表项,根据表项的内容决定具体的操作。而表项的内容, 则无非是下面这么一些可能:
- 表项的内容为全 0,所以 pte_none() 返回1。说明该页面的映射尚未建立,或者说是个“空洞”, 因此不需要做任何事。
- 表项的最低位,即 _PAGE_PRESENT 标志位为 0,所以 pte_present() 返回 1 。说明映射已建 立,但是该页面目前不在内存中,已经被调出到交换设备上。此时表项的内容指明“盘上页面”的地点,而现在该盘上页面多了一个“用户”,所以要通过 swap_duplicate() 递增它的共享计数。然后,就转到 cont_copy_pte_range 将此表项复制到了进程的页面去中。
- 映射已建立,但是物理页面不是一个有效的内存页面,所以 VALID_PAGE() 返回 0 。读者可以回顾一下,我们以前讲过有些物理页面在外设接口卡上,相应的地址称为“总线地址”, 而并不是内存页面。这样的页面、以及虽是内存页面但由内核保留的页面,是不属于页面换入/换出机制管辖的,实际上也不消耗动态分配的内存页面,所以也转到cont_copy_pte_range 将此表项复制到子进程的页面表中。
- 需要从父进程复制的可写页面。本来,此时应该分配一个空闲的内存页面,再从父进程的页面把内容复制过来,并为之建立映射。显然,这个操作的代价是不小的。然而,对这么辛辛苦苦复制下来的页面,子进程是否一定会用呢?特别是会有写访问呢?如果只是读访问,则只要父进程从此不再写这个页面,就完全可以通过复制指针来共亨这个页面,那不知要省事多少了。所以,Linux 内核采用了一种称为 “copy on write” 的技术,先通过复制页面表项暂时共享这个页面,到子进程(或父进程)真的要写这个页面时再来分配页面和复制。代码中的局部变量 cow 是在前面158行定义的,变量名 cow 是 “copy on write” 的缩写。只要一个虚存区间的性质是可写 ( VM_MAYWRITE 为 1 ) 而又不是共享 ( VM_SHARED 为 0 ) ,就属于 copy_on_write 区间。实际上,对于绝大多数的可写虚存区间,cow 都是 1 。在通过复制页面表项暂时共享一个页面表项时要做两件重要的事情,首先要在230和231行将父进程的页面表项改成写保护,然后在236行把已经改成写保护的表项设置到子进程的页面表中。这样一来,相应的页面在两个进程中都变成“只读” 了,当不管是父进程或是子进程企图写入该页面时,都会引起一次页面异常。而页面异常处理程序对此的反应则是另行分配个物理页面,并把内容真正地“复制”到新的物理页面中,让父、子进程各自拥有自己的物理页面, 然后将两个页面表中相应的表项改成可写。所以,Linux 内核之所以可以很迅速地“复制” 一个进程,完全依赖于 “copy on write" (否则,在 fork 一个进程时就得要复制每一个物理页面了) 。可是,copy_on_write 只存在父、子进程各自拥有自己的页面表时才能实现。当 CLONE_VM 标志位为 1,因为父、子进程通过指针共享用户空间时,copy_on_write 就用不上了。此时,父、子进程是在真正的意义上共享用户空间,父进程写入其用户空间的内容同时也 “写入” 子进程的用户空间。
- 父进程的只读页面。这种页面本来就不需要复制。因而可以复制页面表项共享物理页面。
可见,名为 copy_page_range(),实际上却连一个页面也没有真正地 “复制”,这就是为什么 Linux 内核能够很迅速地 fork() 或 clone() 一个进程的秘密。
回到 copy_mm() 的代码中。函数 copy_segments() 处理的是进程可能具有的局部段描述表 LDT 。我们在第 2 章中讲过,只有在 VM86 模式中运行的进程才会有 LDT 。虽然我们并不关心VM86模式,但 是有兴趣的读者也不妨自己看看它是怎样复制的。copy_segments() 的代码在 arch/i386/kernel/process.c 中。
(12)copy_segments
// arch/i386/kernel/process.c
/*
* we do not have to muck with descriptors here, that is
* done in switch_mm() as needed.
*/
void copy_segments(struct task_struct *p, struct mm_struct *new_mm)
{
struct mm_struct * old_mm;
void *old_ldt, *ldt;
ldt = NULL;
old_mm = current->mm;
if (old_mm && (old_ldt = old_mm->context.segments) != NULL) {
/*
* Completely new LDT, we initialize it from the parent:
*/
ldt = vmalloc(LDT_ENTRIES*LDT_ENTRY_SIZE);
if (!ldt)
printk(KERN_WARNING "ldt allocation failed\n");
else
memcpy(ldt, old_ldt, LDT_ENTRIES*LDT_ENTRY_SIZE);
}
new_mm->context.segments = ldt;
}
回到 copy_mm() 的代码。对于 i386 CPU 来说,copy_mm() 中 339 行处的 init_new_context() 是个空语句。
(13)copy_thread
// arch/i386/kernel/process.c
/*
* Save a segment.
*/
#define savesegment(seg,value) \
asm volatile("movl %%" #seg ",%0":"=m" (*(int *)&(value)))
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs;
childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;
struct_cpy(childregs, regs);
childregs->eax = 0;
childregs->esp = esp;
p->thread.esp = (unsigned long) childregs;
p->thread.esp0 = (unsigned long) (childregs+1);
p->thread.eip = (unsigned long) ret_from_fork;
savesegment(fs,p->thread.fs);
savesegment(gs,p->thread.gs);
unlazy_fpu(current);
struct_cpy(&p->thread.i387, ¤t->thread.i387);
return 0;
}
名为 copy_thread(),实际上即只是复制父进程的系统空间堆栈。堆栈中的内容说明了父进程从通过系统调用进入系统空间开始到进入 copy_thread() 的来历,子进程将要循相同的路线返回,所以要把它复制给子进程。但是,如果子进程的系统空间堆栈与父进程的完全相同,那返回以后就无从区分谁是子进程了,所以复制以后还要略作调整。这是一段很有趣的程序,我们先来看535行。在第3章中,读者已经看到当一个进程因系统调用或中断而进入内核时,其系统空间堆栈的顶部保存着CPU进入内核前夕各个寄存器的内容,并形成一个 pt_regs 数据结构。这里535行中的 p 为子进程的 task_struct 指针,指向两个连续物理页面的起始地址;而 THREAD_SIZE + (unsigned long)p 则指向这两个页面的顶端。将其变换成 structpt_regs*,再从中减 1,就指向了了进程系统空间堆栈中的 pt_regs 结构,如图 4.3 所示。
得到了指向子进程系统空间堆栈中 pt_regs 结构的指针 childregs 以后,就先将当前进程系统空间堆栈中的 pt_regs 结构复制过去,再来作少量的调整。什么样的调整呢?首先,将该结构中的 eax 置成 0。 当子进程受调度而“恢复”运行,从系统调用“返回”时,这就是返回值。如前所述,子进程的返回值为 0。其次,还要将结构中的 esp 置成这里的参数 esp,它决定了进程在用户空间的堆栈位置。在 __clone() 调用中,这个参数是由调用者给定的。而在 fork() 和 vfork() 中,则来自调用 do_fork() 前夕的 regs.esp,所以实际上并没有改变,还是指向父进程原来在用户空间的堆栈。
在进程的 task_struct 结构中有个重要的成分 thread,它本身是一个数据结构 thread_struct,里面记录着进程在切换时的 (系统空间) 堆栈指针,取指令地址 (也就是“返回地址”) 等关键性的信息。在复制 task_struct 数据结构的时候,这些信息也原封不动地复制了过来。可是,子进程有自己的系统空间堆栈,所以也要相应加以调整。具体地说,540行将 p->thread.esp 设置成子进程系统空间堆栈中 pt_regs 结构的起始地址,就好像这个子进程以前曾经运行过,而在进入内核以后正要返回用户空间时被切换了一样。而 p->thread.esp0 则应该指向子进程的系统空间堆栈的顶端。当一个进程被调度运行时,内核会将这个变量的值写入 TSS 的 esp0 字段,表示当这个进程进入 0 级运行时其堆栈的位置。此外, p->thread.eip 的值表示当进程下一次被切换进入运行时的切入点,类似于函数调用或中断的返回地址。 将此地址设置成 ret_from_fork,使创建的子进程在首次被调度运行时就从那儿开始,这一点以后在阅读有关进程切换的代码时还要讲到。545行和546行的 savesegment 是个宏操作,其定义就在526行。 所以,545行在 gcc 预处理以后就会变成
asm volatile("movl %%fs, %0":"=m" (*(int *)&p->thread.fs))
也就是把当前的段寄存器 fs 的值保存在 p->thread.fs 中。546行与此类似。548行和549行是为 i387 浮点处理器而设的,那就不是我们所关心的了。
(14)DECLARE_MLTEX_LOCKED
// include/asm-i386/semaphore.h
#define DECLARE_MUTEX(name) __DECLARE_SEMAPHORE_GENERIC(name,1)
#define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0)
将 DECLARE_MUTEX_LOCKED 与 DECLARE_MUTEX 作一比较,可以看出正常情况下信号量中资源的数量为 1,而现在这个信号量中资源的数量为 0。
4、系统调用execve
读者在前一节中已经看到,进程通常是按其父进程的原样复制出来的,在多数情况下,如果复制出来的子进程不能与父进程分道扬镳,“走自已的路”,那就没有多大意义。所以,执行一个新的可执行程序是进程生命历程中关键性的一步。Linux 为此提供了一个系统调用 execve(),而在C语言的程序库中则又在此基础上向应用程序提供一整套的库函数,包括 execl() 、execlp() 、execle() 、execle() 、execv() 和 execvp() 。 此外,还有库函数 system(),也与 exccve() 有关,不过 system() 是 fork() 、execve() 、wait4() 的组合。我们己经在本章第2节介绍过应用程序怎样调用 execve(),现在我们就来介绍 execvc() 的实现。
系统调用 execve() 内核入口 是 sys_execve(),代码见 arch/i386/kernel/process.c:
(1)sys_execve
// arch/i386/kernel/process.c
/*
* sys_execve() executes a new program.
*/
asmlinkage int sys_execve(struct pt_regs regs)
{
int error;
char * filename;
// 以前讲过,系统调用进入内核时,regs.ebx 中的内容为应用程序中调用相应库函数时的第一个参数。
// 在本章第2节所举的例子中,这个参数为指向字符串 “/bin/echo" 的指针。现在,指针存放在
// regs.ebx 中,但字符串本身还在用户空间中,所以730行的 getname() 要把这个字符串从
// 用户空间拷贝到系统空间,在系统空间中建立起一个副本。让我们看看具体是怎么做的。
// 函数 getname() 的代码在 fs/namei.c 中
filename = getname((char *) regs.ebx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
// 在系统空间中建立起一份可执行文件的路径名副本以后,sys_execve()就调用do_execve(),以完
// 成其主体部分的工作。当然,完成以后还要通过 putname() 将所分配的物理页面释放。
// 函数 do_execvc() 的代码在 fs/exec.c 中
error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s);
if (error == 0)
current->ptrace &= ~PT_DTRACE;
putname(filename);
out:
return error;
}
⑴ getname
// fs/namei.c
char * getname(const char * filename)
{
char *tmp, *result;
result = ERR_PTR(-ENOMEM);
tmp = __getname();
if (tmp) {
int retval = do_getname(filename, tmp);
result = tmp;
if (retval < 0) {
putname(tmp);
result = ERR_PTR(retval);
}
}
return result;
}
// =================================================================
// include/linux/fs.h
#define __getname() kmem_cache_alloc(names_cachep, SLAB_KERNEL)
先通过 __getname() 分配一个物理页面作为缓冲区,然后调用 do_getname() 从用户空间拷贝字符串。那么,为什么要专门为此分配一个物理页面作为缓冲区呢?首先,这个字符串确有可能相当长, 因为这是个绝对路径名。其次,我们以前讲过,进程系统空间堆栈的大小是大约 7KB,不能滥用, 不宜在 getname() 中定义一个局部的 4KB 的字符数组 (注意,局部变量所占据的空间是在堆栈中分配的) 。函数 do_getname() 的代码也在文件 fs/namei.c 中。
⑵ do_getname
// fs/namei.c
/* In order to reduce some races, while at the same time doing additional
* checking and hopefully speeding things up, we copy filenames to the
* kernel data space before using them..
*
* POSIX.1 2.4: an empty pathname is invalid (ENOENT).
*/
static inline int do_getname(const char *filename, char *page)
{
int retval;
unsigned long len = PATH_MAX + 1;
if ((unsigned long) filename >= TASK_SIZE) {
if (!segment_eq(get_fs(), KERNEL_DS))
return -EFAULT;
} else if (TASK_SIZE - (unsigned long) filename < PAGE_SIZE)
len = TASK_SIZE - (unsigned long) filename;
retval = strncpy_from_user((char *)page, filename, len);
if (retval > 0) {
if (retval < len)
return 0;
return -ENAMETOOLONG;
} else if (!retval)
retval = -ENOENT;
return retval;
}
如果指针 filename 的值大于等于 TASK_SIZE,就表示 filename 实际上在系统空间中。读者应该还记得 TASK_SIZE 的值是 3GB 。具体的拷贝是通过 strncpy_from_user() 进行的,代码见 arch/i386/lib/usercopy.c 。
// arch/i386/lib/usercopy.c
long strncpy_from_user(char *dst, const char *src, long count)
{
long res = -EFAULT;
if (access_ok(VERIFY_READ, src, 1))
__do_strncpy_from_user(dst, src, count, res);
return res;
}
#define __do_strncpy_from_user(dst,src,count,res) \
do { \
int __d0, __d1, __d2; \
__asm__ __volatile__( \
" testl %1,%1\n" \
" jz 2f\n" \
"0: lodsb\n" \
" stosb\n" \
" testb %%al,%%al\n" \
" jz 1f\n" \
" decl %1\n" \
" jnz 0b\n" \
"1: subl %1,%0\n" \
"2:\n" \
".section .fixup,\"ax\"\n" \
"3: movl %5,%0\n" \
" jmp 2b\n" \
".previous\n" \
".section __ex_table,\"a\"\n" \
" .align 4\n" \
" .long 0b,3b\n" \
".previous" \
: "=d"(res), "=c"(count), "=&a" (__d0), "=&S" (__d1), \
"=&D" (__d2) \
: "i"(-EFAULT), "0"(count), "1"(count), "3"(src), "4"(dst) \
: "memory"); \
} while (0)
这个函数的主体 __do_strncpy_from_user() 是一个宏操作,也在同一源文件 usercopy.c 中,与第3章中介绍过的 __generic_copy_from_user() 很相似,读者可以自行对照阅读。
(2)do_execve
// fs/exec.c
/*
* sys_execve() executes a new program.
*/
int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs)
{
struct linux_binprm bprm;
struct file *file;
int retval;
int i;
// 显然,先要将给定的可执行程序文件找到并打开,open_exec()就是为此而调用的,其代码也在
// fs/exec.c 中,读者可结合“文件系统” 一章中有关打开文件操作的内容,特别是 path_walk()
// 的代码自行阅读。
file = open_exec(filename);
retval = PTR_ERR(file);
if (IS_ERR(file))
return retval;
// 假定目标文件已经打开,下一步就要从文件中装入可执行程序了。内核中为可执行程序的装入定
// 义了一个数据结构 linux_binprm ,以便将运行一个可执行文件时所需的信息组织在一起,
// 这是在 include/linux/binfmts.h 定义的
bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
memset(bprm.page, 0, MAX_ARG_PAGES*sizeof(bprm.page[0]));
// 代码中的 linux_binprm 数据结构 bprm 是个局部量。函数 open_exec() 返回一个 file 结构指针,
// 代表着读入可执行文件的上下文,所以将其保存在数据结构 bprm 中。变量 bprm.sh_bang 的值说明
// 可执行文件的性质,当可执行文件是一个Shell过程(Shell Script,用 Shell 语言编写的命令文件,
// 由 shell 解释执行) 时置为 1。而现在还不知道,所以暂且将其置为 0,也就是先假定为二进制文件。
// 数据结构中的其他两个变量也暂时设置成 0。接着就处理可执行文件的参数和环境变量。
bprm.file = file;
bprm.filename = filename;
bprm.sh_bang = 0;
bprm.loader = 0;
bprm.exec = 0;
// 与可执行文件路径名的处理办法一样,每个参数的最大长度也定为一个物理页面,所以 bprm 中有
// 一个页面指针数组,数组的大小为允许的最大参数个数 MAX_AGE_PAGES,目前这个常数定义为32。
// 前面已通过memset()将这个指针数组初始化成全0。现在将 bprm.p 设置成这些页面的总和减长一个
// 指针的大小,因为第 0 个参数也就是 argv[0] 是可执行程序本身的路径名。函数 count() 是在
// exec.c 中定义的,这里用它对字符串指针数组 argv[] 中参数的个数进行计数,而
// bprm.p/sizeof(void*)表示允许的最大值。同样,对作为参数传过来的环境变量也要通过 count()
// 计数。注意这里的数组 argv[] 和 envp[] 是在用户空间而不在系统空间,所以计数的操作并不那么
// 简单。函数 count() 的代码在 fs/exec.c 中,它本身的代码很简单,但是引用的宏定义
// get_user() 却颇有些挑战性,值得一读。它也与第3章中介绍过的 __generic_copy_from_user()
// 相似,我们把它留给读者作为练习。相关的代码在 include/asm-i386/uaccess.h 和
// arch/i386/lib/getuser.S 中,调用的路径为
// [count() > getuser() > _get_user() > _get_user_4()] 。 如果 count() 失败,即返回负值,
// 则要对目标文件执行一次 allow_write_access()。这个函数是与 deny_write_access() 配对使用的,
// 目的在于防止其他进程(可能在另一个CPU上运行)在读入可执行文件期间通过内存映射改变它的内容
// (详见“文件系统”以及系统调用mmap())。与其配对的 deny_write_access() 是在打开可执行文件时在
// open_exec() 中调用的。
if ((bprm.argc = count(argv, bprm.p / sizeof(void *))) < 0) {
allow_write_access(file);
fput(file);
return bprm.argc;
}
if ((bprm.envc = count(envp, bprm.p / sizeof(void *))) < 0) {
allow_write_access(file);
fput(file);
return bprm.envc;
}
// 完成了对参数和环境变量的计数以后,do_execve()又调用prepare_binprm(),进一步做数据结构
// bprm的准备工作,从可执行文件读入开头的128个字节到 linux_binprm 结构 bprm 中的缓冲区。
// 当然,在读之前还要先检验当前进程是否有这个权力,以及该文件是否有可执行属性。如果可执行文件
// 具有"setuid”特性则要作相应的设置。这个函数的代码也在exec.c中。由于涉及文件操作的细节,我
// 们建议读者在学习了 “文件系统”以后再回过来自行阅读。此处先说明为什么只是先读128个字节。
// 这是因为,不管目标文件是elf格式还是a.out格式,或者别的格式,在开头128个字节中都包括了关
// 于可执行文件属性的必要而充分的信息。等一下读者就会看到这些信息的用途。
retval = prepare_binprm(&bprm);
if (retval < 0)
goto out;
// 最后的准备工作就是把执行的参数,也就是argv[],以及运行的环境,也就是envp[],从用户空
// 间拷贝到数据结构bprm中。其中的第1个参数argv[0]就是可执行文件的路径名,已经在
// bprm.filename 中了,所以用copy_strings_kernel()从系统空间中拷贝,其他的就要用
// copy_strings()从用户空间拷贝。
retval = copy_strings_kernel(1, &bprm.filename, &bprm);
if (retval < 0)
goto out;
bprm.exec = bprm.p;
retval = copy_strings(bprm.envc, envp, &bprm);
if (retval < 0)
goto out;
retval = copy_strings(bprm.argc, argv, &bprm);
if (retval < 0)
goto out;
// 至此,所有的准备工作都己完成,所有必要的信息都已经搜集到了 linux_binprm 结构bprm中,
// 接下来就要装入并运行目标程序了 (exec.c) 。
//
// 显然,这里的关键是search_binary_handler(),在深入到这个函数内部之前,先介绍一个大概。内
// 核中有一个队列,叫 formats,挂在此队列中的成员是代表着各种可执行文件格式的“代理人”,每个
// 成员只认识并且处理一种特定格式的可执行文件的运行。在前面的准备阶段中,己经从可执行文件头
// 部读入了 128 个字节存放在bprm的缓冲区,而且运行所需的参数和环境变量也已经收集在bprm中。
// 现在就由formats队列中的成员逐个来认领,谁要是辨认到了它所代表的可执行文件格式,运行的事就
// 交给它。要是都不认识呢?那就根据文件头部的信息再找找看,是否有为此种格式设计,但是作为可
// 动态安装模块实现的“代理人”存在于文件系统中。如果有的话就把这模块安装进来并且将其挂入到
// formats队列中,然后让formats队列中的各个“代理人”再来试一次。
// 函数search_binary_handler()的代码也在exec.c中,其中有一段是专门针对alpha处理器的条件编
// 译,在下列的代码中跳过了这段条件编译语句:
retval = search_binary_handler(&bprm,regs);
if (retval >= 0)
/* execve success */
return retval;
out:
/* Something went wrong, return the inode and free the argument pages*/
allow_write_access(bprm.file);
if (bprm.file)
fput(bprm.file);
for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
struct page * page = bprm.page[i];
if (page)
__free_page(page);
}
return retval;
}
⑴ linux_binprm
// include/linux/binfmts.h
/*
* This structure is used to hold the arguments that are used when loading binaries.
*/
struct linux_binprm{
char buf[BINPRM_BUF_SIZE];
struct page *page[MAX_ARG_PAGES];
unsigned long p; /* current top of mem */
int sh_bang;
struct file * file;
int e_uid, e_gid;
kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
int argc, envc;
char * filename; /* Name of binary */
unsigned long loader, exec;
};
⑵ search_binary_handler
// fs/exec.c
/*
* cycle the list of binary formats handler, until one recognizes the image
*/
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{
int try,retval=0;
struct linux_binfmt *fmt;
#ifdef __alpha__
/* handle /sbin/loader.. */
{
struct exec * eh = (struct exec *) bprm->buf;
if (!bprm->loader && eh->fh.f_magic == 0x183 &&
(eh->fh.f_flags & 0x3000) == 0x3000)
{
char * dynloader[] = { "/sbin/loader" };
struct file * file;
unsigned long loader;
allow_write_access(bprm->file);
fput(bprm->file);
bprm->file = NULL;
loader = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
file = open_exec(dynloader[0]);
retval = PTR_ERR(file);
if (IS_ERR(file))
return retval;
bprm->file = file;
bprm->loader = loader;
retval = prepare_binprm(bprm);
if (retval<0)
return retval;
/* should call search_binary_handler recursively here,
but it does not matter */
}
}
#endif
for (try=0; try<2; try++) {
read_lock(&binfmt_lock);
for (fmt = formats ; fmt ; fmt = fmt->next) {
int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
if (!fn)
continue;
if (!try_inc_mod_count(fmt->module))
continue;
read_unlock(&binfmt_lock);
retval = fn(bprm, regs);
if (retval >= 0) {
put_binfmt(fmt);
allow_write_access(bprm->file);
if (bprm->file)
fput(bprm->file);
bprm->file = NULL;
current->did_exec = 1;
return retval;
}
read_lock(&binfmt_lock);
put_binfmt(fmt);
if (retval != -ENOEXEC)
break;
if (!bprm->file) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
if (retval != -ENOEXEC) {
break;
#ifdef CONFIG_KMOD
}else{
#define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e))
char modname[20];
if (printable(bprm->buf[0]) &&
printable(bprm->buf[1]) &&
printable(bprm->buf[2]) &&
printable(bprm->buf[3]))
break; /* -ENOEXEC */
sprintf(modname, "binfmt-%04x", *(unsigned short *)(&bprm->buf[2]));
request_module(modname);
#endif
}
}
return retval;
}
程序中有两层嵌套的 for 循环。内层是对 formats 队列中的每个成员循环,让队列中的成员逐个试试它们的 load_binary() 函数,看看能否对上号。如果对上了号,那就把目标文件装入并将其投入运行, 再返回一个正数或 0 。当 CPU 从系统调用返回时,该目标文件的执行就真正开始了。否则,如果不能辨识,或者在处理的过程中也了错,就返回一个负数。出错代码 -ENOEXEC 表示只是对不上号,而并没有发生其他的错误,所以循环回去,让队列中的下个成员再来试试。但是如果出了错而又并不是 -ENOEXEC,那就表示对上了号但出了其他的错,这就不用再让其他的成员来试了。
内层循环结束以后,如果失败的原因是 -ENOEXEC,就说明队列中所有的成员都不认识目标文件的格式。这时候,如果内核支持动态安装模块 (取决于编译选择项 CONFIG_KMOD) ,就根据目标文件的第2和第3个字节生成一个 binfmt 模块名,通过 request_module() 试着将相应的模块装入 (见本书 “文件系统”和“设备驱动”两章中的有关内容) 。外层的 for 循环共进行两次,正是为了在安装了模块以后再来试一次。
能在 Linux 系统上运行的可执行程序的开头几个字节,特别是开头 4 个字节,往往构成一个所谓的 magic number,如果把它拆开成字节,则往往又是说明文件格式的字符。例如,elf 格式的可执行文件的头四个字节为 “0x7F” 、“e”、“l” 和 “f";而 java 的可执行文件头部四个字节则为 “c”、“a”、“f” 和 “e”。如果可执行文件为 Shell 过程或 perl 文件,即笫一行的格式为 #!/bin/sh 或 #!/usr/bin/perl ,此时第一个字符为 “#”,第二个字符为 “!”,后面是相应解释程序的路径名。
数据结构 linux_binfmt 定义于 include/linux/binfmts.h 中,前面已经看到过了。结构中有三个函数指针,load_binary 用来装入可执行程序,load_shlib 用来装入动态安装的公用库程序,而 core_dump 的作用则不言自明。显然,这里最根本的是 load_binary 。同时,如果不搞清具体的装载程序怎样工作,就很难对 execve()、进而对 Linux 进程的运行有深刻的理解。下面我们以 a.out 格式为例,讲述装入并启动执行目标程序的过程。其实,a.out 格式的可执行文件已经渐渐被淘汰了,取而代之的是 elf 格式。 但是,a.out 格式要简单得多,并且方便我们通过它来讲述目标程序的装载和投入运行的过程,所以从篇幅考虑我们选择了 a.out 。读者在搞清了 a.out 格式的装载和投运过程以后,可以自行阅读有关 elf 格式的相关代码。
⑶ a.out格式目标文件的装载和投运行
// fs/binfmt_aout.c
static struct linux_binfmt aout_format = {
NULL, THIS_MODULE, load_aout_binary, load_aout_library, aout_core_dump, PAGE_SIZE
};
/*
* These are the functions used to load a.out style executables and shared
* libraries. There is no binary dependent code anywhere else.
*/
static int load_aout_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
struct exec ex;
unsigned long error;
unsigned long fd_offset;
unsigned long rlim;
int retval;
// 首先是检查目标文件的格式,看看是否对上号。所有a.out格式可执行文件(二进制代码)的开头
// 都应该是一个exec数据结构,这是在include/asm-i386/a.out.h 中定义的:
ex = *((struct exec *) bprm->buf); /* exec-header */
if ((N_MAGIC(ex) != ZMAGIC && N_MAGIC(ex) != OMAGIC &&
N_MAGIC(ex) != QMAGIC && N_MAGIC(ex) != NMAGIC) ||
N_TRSIZE(ex) || N_DRSIZE(ex) ||
bprm->file->f_dentry->d_inode->i_size < ex.a_text+ex.a_data+N_SYMSIZE(ex)+N_TXTOFF(ex)) {
return -ENOEXEC;
}
// 各种a.out格式的文件因目标代码的特性不同,其正文的起始伟置也就不同。为此提供了一个宏操
// 作N_TXTOFF(),以便根据代码的特性取得正文在目标文件中的起始位置,这是在includc/linux/a.out.h
// 中定义的:
fd_offset = N_TXTOFF(ex);
/* Check initial limits. This avoids letting people circumvent
* size limits imposed on them by creating programs with large
* arrays in the data or bss.
*/
// 以前曾经讲过,每个进程的task_struct结构中有个数组rlim,规定了该进程使用各种资源的限制,
// 其中也包括对用于数据的内存空间的限制。所以,目标文件所确定的data和bss两个“段”的总和不
// 能超出这个限制。
rlim = current->rlim[RLIMIT_DATA].rlim_cur;
if (rlim >= RLIM_INFINITY)
rlim = ~0;
if (ex.a_data + ex.a_bss > rlim)
return -ENOMEM;
/* Flush all traces of the currently running executable */
// 顺利通过了这些检验就表示具备了执行该目标文件的条件,所以就到了 “与过去告别”的时候。
// 这种“告别过去”意味着放弃从父进程“继承”下来的全部用户空间,不管是通过复制还是通过共享
// 继承下来的。不过,下面读者会看到,这种告别也并非彻底的决裂。
// 函数 flush_old_exec() 的代码也在 exec.c 中
retval = flush_old_exec(bprm);
if (retval)
return retval;
/* OK, This is the point of no return */
#if !defined(__sparc__)
set_personality(PER_LINUX);
#else
set_personality(PER_SUNOS);
#if !defined(__sparc_v9__)
memcpy(¤t->thread.core_exec, &ex, sizeof(struct exec));
#endif
#endif
// 这里是对新的mm_struct数据结构中的一些变量进行初始化,为以后分配存储空间并读入可执行
// 代码的映象作好准备。目标代码的映象分成text、data 以及 bss 三段,mm_struct结构中为每个段都设
// 置了 start 和 end 两个指针。每段的起始地址定义于include/linux/a.out.h:
current->mm->end_code = ex.a_text +
(current->mm->start_code = N_TXTADDR(ex));
current->mm->end_data = ex.a_data +
(current->mm->start_data = N_DATADDR(ex));
current->mm->brk = ex.a_bss +
(current->mm->start_brk = N_BSSADDR(ex));
current->mm->rss = 0;
current->mm->mmap = NULL;
// 然后,通过compute_creds()确定进程在开始执行新的目标代码以后所具有的权限,这是根据bprm
// 中的内容和当前的权限确定的。其代码在 exec.c 中,读者可自行阅读。
compute_creds(bprm);
current->flags &= ~PF_FORKNOEXEC;
#ifdef __sparc__
if (N_MAGIC(ex) == NMAGIC) {
loff_t pos = fd_offset;
/* Fuck me plenty... */
/* <AOL></AOL> */
error = do_brk(N_TXTADDR(ex), ex.a_text);
bprm->file->f_op->read(bprm->file, (char *) N_TXTADDR(ex),
ex.a_text, &pos);
error = do_brk(N_DATADDR(ex), ex.a_data);
bprm->file->f_op->read(bprm->file, (char *) N_DATADDR(ex),
ex.a_data, &pos);
goto beyond_if;
}
#endif
// 前面讲过,a.out 格式目标代码中的 magic number 表示着代码的特性,或者说类型。当 magic number
// 为 OMAGIC 时,表示该文件中的可执行代码并非“纯代码”。对于这样的代码,先通过do_brk()为正文
// 段和数据段合在一起分配空间,然后就把这两部分从文件中读进来。函数do_brk()我们已经在笫2章中
// 介绍过,而从文件读入则在“文件系统”和“块设备驱动”两章中有详细叙述,读者可以参阅,这里
// 就不重复了。不过要指出,读入代码时是从文件中位移为32的地方开始,读入到进程用户空间中从地
// 址0开始的地方,读入的总长度为ex.a_text+ex.a_data。对于 i386 CPU 而言,
// flush_icache_range() 为一空语句。至于bss段,则无需从文件读入,只要分配空间就可以了,
// 所以放在后面再处理。对于 OMAGIC 类型的a.out可执行文件而言,装入程序的工作就基本完成了。
if (N_MAGIC(ex) == OMAGIC) {
unsigned long text_addr, map_size;
loff_t pos;
text_addr = N_TXTADDR(ex);
#if defined(__alpha__) || defined(__sparc__)
pos = fd_offset;
map_size = ex.a_text+ex.a_data + PAGE_SIZE - 1;
#else
pos = 32;
map_size = ex.a_text+ex.a_data;
#endif
error = do_brk(text_addr & PAGE_MASK, map_size);
if (error != (text_addr & PAGE_MASK)) {
send_sig(SIGKILL, current, 0);
return error;
}
error = bprm->file->f_op->read(bprm->file, (char *)text_addr,
ex.a_text+ex.a_data, &pos);
if (error < 0) {
send_sig(SIGKILL, current, 0);
return error;
}
flush_icache_range(text_addr, text_addr+ex.a_text+ex.a_data);
} else {
// 在a.out格式的可执行文件中,除OMAGIC以外其他二种均为纯代码,也就是所谓的“可重入”代
// 码。此类代码中,不但其正文段的执行代码在运行时不会改变,其数据段的内容也不会在运行时改变。
// 凡是要在运行过程中改变内容的东西都在堆栈中(局部变量),要不然就在动态分配的缓冲区。所以,
// 内核干脆将可执行文件映射到子进程的用户空间中,这样连通常swap所需的盘上空间也省去了。在这
// 二种类型的可执行文件中,除NMGIC以外都要求正文段及数据段的长度与页面大小对齐,如发现没有
// 对齐就要通过printk()发出警告信息。但是,发出警告信息太频繁也不好,所以就设置了一个静态变量
// error_time2,使警告信息之间的间隔不小于5秒。接下来的操作取决于具体的文件系统是否提供mmap、
// 就是将一个已打开文件映射到虚在空间的操作,以及正文段及数据段的长度是否与页面大小对齐。如
// 果不满足映射的条件,就分配空间并且将正文段和数据段一起读入至进程的用户空间。这次是从文件
// 中位移为fd_offset ,即N_TXTOFF(ex)的地方开始,读入到由文件的头部所指定的地址
// N_TXTADDR(ex),长度为两段的总和。如果满足映射的条件,那就更好了,那就通过do_mmap()分别
// 将文件中的正文段和数据段映射到进程的用户空间中,映射的地址则与装入的地址一致。
// 调用mmap()之前无需分配空间, 那已经包含在mmap()之中了。
// 至此,正文段和数据段都已经装入就绪了,接下来就是bss段和堆栈段了(fs/binfmt_aout.c)
static unsigned long error_time, error_time2;
if ((ex.a_text & 0xfff || ex.a_data & 0xfff) &&
(N_MAGIC(ex) != NMAGIC) && (jiffies-error_time2) > 5*HZ)
{
printk(KERN_NOTICE "executable not page aligned\n");
error_time2 = jiffies;
}
if ((fd_offset & ~PAGE_MASK) != 0 &&
(jiffies-error_time) > 5*HZ)
{
printk(KERN_WARNING
"fd_offset is not page aligned. Please convert program: %s\n",
bprm->file->f_dentry->d_name.name);
error_time = jiffies;
}
if (!bprm->file->f_op->mmap||((fd_offset & ~PAGE_MASK) != 0)) {
loff_t pos = fd_offset;
do_brk(N_TXTADDR(ex), ex.a_text+ex.a_data);
bprm->file->f_op->read(bprm->file,(char *)N_TXTADDR(ex),
ex.a_text+ex.a_data, &pos);
flush_icache_range((unsigned long) N_TXTADDR(ex),
(unsigned long) N_TXTADDR(ex) +
ex.a_text+ex.a_data);
goto beyond_if;
}
down(¤t->mm->mmap_sem);
error = do_mmap(bprm->file, N_TXTADDR(ex), ex.a_text,
PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE,
fd_offset);
up(¤t->mm->mmap_sem);
if (error != N_TXTADDR(ex)) {
send_sig(SIGKILL, current, 0);
return error;
}
down(¤t->mm->mmap_sem);
error = do_mmap(bprm->file, N_DATADDR(ex), ex.a_data,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE,
fd_offset + ex.a_text);
up(¤t->mm->mmap_sem);
if (error != N_DATADDR(ex)) {
send_sig(SIGKILL, current, 0);
return error;
}
}
beyond_if:
set_binfmt(&aout_format);
set_brk(current->mm->start_brk, current->mm->brk);
// 接着,还要在用户空间的堆栈区顶部为进程建立起一个虚存区间,并将执行参数以及环境变量所
// 占的物理页面与此虚存区间建立起映射。这是由setup_arg_pages()完成的,其代码在exec.c中:
//
// 进程的用户空间中地址最高处为堆栈区,这里的常数STACK_TOP就是TASK_SIZE,也就是3GB
// (0xC0000 0000)。堆栈区的顶部为一个数组,数组中的每一个元素都是一个页面。数组的大小为
// MAX_ARG_PAGES,而实际映射的页面数量则取决于这些执行参数和环境变量的数量。
// 然后,在这些页面的下方,就是过程的用户空间堆栈了。另一方面,大家知道任何用户程序的入
// 口都是main(),而main有两个参数argc和argv[]。其中参数argv[]是字符指针数组,
// argc则为数组的大小。 但是实际上还有个隐藏着的字符指针数组envp[]用来传递环境变量,
// 只是不在用户程序的“视野”之内而已。所以,用户空间堆栈中从一开始就要设置好三项数据,
// 即envp[]、argv[] 以及argc。
retval = setup_arg_pages(bprm);
if (retval < 0) {
/* Someone check-me: is this error path enough? */
send_sig(SIGKILL, current, 0);
return retval;
}
// 此外,还要将保存着的(字符串形式的)参数和环境变量复制到用户空间的顶端。这都是由
// create_aout_tables() 完成的,其代码也在同一文件(binfmt.aout.c)中
//
// 读者应该能看明白,这是在堆栈的顶端构筑envp[]、argv[]和argc。请读者注意看一下这段代码中
// 的228至234行(以及237至243行),然后回答一个问题:为什么是get_user(c, ptt),后不是
// get_user(&c, ptt)? 以前我们曾经讲过,get_user()是一段颇具挑战性的代码,
// 并建议读者自行阅读。现在简单地介绍一下,看看你是否读懂了。
// 这是在include/asm-i386/uaccess.h 中定义的一个宏定义
current->mm->start_stack =
(unsigned long) create_aout_tables((char *) bprm->p, bprm);
#ifdef __alpha__
regs->gp = ex.a_gpvalue;
#endif
// 这里只剩下最后一个关键性的操作了,那就是start_thread()。这是个宏操作,定义于
// include/asm-i386/process.h 中
//
// 读者对这里的regs指针已经很熟悉,它指向保留在当前进程系统空间堆栈中的各个寄存器副本。
// 当进程从系统调用返回时,这些数值就会被“恢复”到CPU的各个寄存器中。所以,那时候的堆栈指
// 针将是current->mm->start_stack;而返回地址,也就是EIP的内容,则将是ex.a_entry。
// 显然,这正是我们所需要的。
start_thread(regs, ex.a_entry, current->mm->start_stack);
if (current->ptrace & PT_PTRACED)
send_sig(SIGTRAP, current, 0);
return 0;
// 至此,可执行代码的装入和投运已经完成。而 do_execve() 在调用了 search_binary_handler()
// 以后也就结束了。当CPU从系统调用返回到用户空间时,就会从由 ex.a_entry 确定的地址开始执行。
}
Ⓐ exec
// include/asm-i386/a.out.h
struct exec
{
unsigned long a_info; /* Use macros N_MAGIC, etc for access */
unsigned a_text; /* length of text, in bytes */
unsigned a_data; /* length of data, in bytes */
unsigned a_bss; /* length of uninitialized data area for file, in bytes */
unsigned a_syms; /* length of symbol table data in file, in bytes */
unsigned a_entry; /* start address */
unsigned a_trsize; /* length of relocation info for text, in bytes */
unsigned a_drsize; /* length of relocation info for data, in bytes */
};
#define N_TRSIZE(a) ((a).a_trsize)
#define N_DRSIZE(a) ((a).a_drsize)
#define N_SYMSIZE(a) ((a).a_syms)
结构中的第一个无符号长整数 a_info 在逻辑上分成两部分:其高16位是一个代表目标 CPU 类型的代码,对于 i386 CPU 这部分的值为 100 (0x64);而低 16 位就是 magic number。不过,a.out 文件的 magic number 并不像在有的格式中那样是可打印字符,而是表示某些属性的编码,一共有四种,即 ZMAGIC、OMAGIC、QMAGIC 以及 NMAGIC,这是在 include/linux/a.out.h 中定义的。
Ⓑ flush_old_exec
// fs/exec.c
int flush_old_exec(struct linux_binprm * bprm)
{
char * name;
int i, ch, retval;
struct signal_struct * oldsig;
/*
* Make sure we have a private signal table
*/
oldsig = current->sig;
// 首先是进程的信号(软中断)处理表。我们讲过,一个进程的信号处理表就好像一个系统中的中断向量表,
// 虽然运用的层次不同,其概念是相似的。当子进程被创建出来时,父进程的信号处理表可以己经复制过来,
// 但也有可能只是把父进程的信号处理表指针复制了过来,而通过这指针来共享父进程的信号处理表。现在,
// 子进程最终要“自立门户” 了,所以要看一下,如果还在共享父进程的信号处理表的话,就要把它复制过来。
// 正因为这样,make_private_signals() 的代码与do_fork()中调用的 copy_sighand()基本相同。
retval = make_private_signals();
if (retval) goto flush_failed;
/*
* Release all of the old mmap stuff
*/
// 同样,子进程的用户空间可能是父进程用户空间的复制品,也可能只是通过一个指针来共享父进
// 程的用户空间,这一点只要检查一下对用户空间、也就是current->mm的共享计数就可清楚。当共享
// 计数为 1 时,表明对此空间的使用是独占的,也就是说这是从父进程复制过来的,那就要先释放
// mm_struct 数据结构以下的所有 vm_area_struct 数据结构(但是不包括mm_struct结构本身),并且将页
// 面表中的表项都设置成0。具体地这是由exit_mmap()完成的,其代码在mm/mmap.c中,读者可自行
// 阅读。在调用exit_mmap()之前还调用了一个函数mm_release(),对此我们将在稍后加以讨论,因为在
// 后面也调用了这个函数。至于flush_cache_mm() 和 flush_tlb_mm(),那只是使高速缓存与内存相一致,
// 不在我们现在关心之列,而且前者对i386处理器而言根本就是空语句。这里倒是要问一句,在父进程
// fork()子进程的时候,辛辛苦苦地复制了代及用户空间的所有数据结构,难道目的就在于稍后在执行
// execve()时又辛辛苦苦把它们全部释放?既有今日,何必当初?是的,这确实不合理。这就是在有了
// fork()系统调用以后又增加了一个vfork()系统调用(从BSD Unix开始)的原因。让我们回顾一下
// sys_fork() 与 sys_vfork() 在调用 do_fork() 时的不同。
//
// 可见,sys_vfork() 在调用 do_fork()时比 sys_fork()多了两个标志位,一个是 CLONE_VFORK,另
// 一个是CLONE_VM。当CLONE_VM标志位为1时,内核并不将父进程的用户空间(数据结构)复制
// 给子进程,向只是将指向mm_struct数据结构的指针复制给子进程,让子进程通过这个指针来共享父
// 进程的用户空间。这样,创建了进程时可以免去复制用户空间的麻烦。向当子进程调用execve()时就
// 可以跳过释放用户空间这一步,直接就为子进程分配新的用户空间。但是,这样一来省事是省事了,
// 却可能带来新的问题。以前讲过,fork()以后,execve()之前,子进程虽然有它自己的一整套代表用户
// 空间的数据结构,但是最终在物理上还是与父进程共用相同的页面。不过,由于子进程有其独立的页
// 面目录与页面表,可以在子进程的页面表里把对所有页面的访问权限都设置成“只读”。这样,当子进
// 程企图改变某个页面的内容时,就会因权限不符而导致页面异常",在页面异常的处理程序中为子进程
// 复制所需的物理页面,这就叫"copy_on_write”。相比之下,如果子进程与父进程共享用户空间,也就
// 是共享包括页面表在内的所有数据结构,那就无法实施"copy_on_write”了。此时子进程所写入的内容
// 就真正进入了父进程的空间中。我们知道,当一个进程在用户空间运行时,其堆栈也在用户空间。这
// 意味着在这种情况下子进程可以改变父进程的堆栈,反过来父进程也可以改变子进程的堆栈!因为这
// 个原因,vfork()的使用是很危险的,在子进程尚未放弃对父进程用户空间的共享之前,绝不能让两个
// 进程都进入系统空间运行。所以,在sys_vfork()调用do_fork()时结合使用了另一个标志位
// CLONE_VFORK。当这个标志位为1时,父进程在创建了子进程以后就进入睡眠状态,等候子进程通
// 过execve()执行另一个目标程序,或者通过exit()寿终正寝。在这两种情况下子进程都会释放其共享的
// 用户空间,使父进程可以安全地继续运行。即便如此,也还是有危险,子进程绝对不能从调用vfork()
// 的那个函数中返回,否则还是可能破坏父进程的返回地址。所以,vfork()实际上是建立在子进程在创
// 建以后立即就会调用execve()这个前提之上的。
// 那么,怎样使父进程进入睡眠而等待子进程调用execve()或exit()呢?当然可以有不同的实现。读
// 者已经在do_fork()的代码中看到了内核让父进程在一个0资源的“信号量”上执行一次down()操作而
// 进入睡眠的安排,这里的mm_release()则让子进程在此信号量上执行1次up()操作将父进程唤醒。
// 函数 mm_release()的代码在 fork.c 中
retval = exec_mmap();
if (retval) goto mmap_failed;
// 从exec_mmap()返回到flush_old_exec()时,子进程从父进程继承的用户空间已经释放,其用户空
// 间变成了一个独立的“空壳”,也就是一个大小为0的独立的用户空间。这时候的进程已经是“义无反
// 顾” 了,回不到原来的用户空间中去了(见代码中的注解)。前面讲过,当前进程(子进程)原来可能
// 是通过指针共享父进程的信号处理表的,而现在有了自己的独立的信号处理表,所以也要递减父进程
// 信号处理表的共享计数,并且如果递减后为0就要将其所占的空间释放,这就是
// release_old_signals() 所做的事情。
/* This is the point of no return */
release_old_signals(oldsig);
current->sas_ss_sp = current->sas_ss_size = 0;
if (current->euid == current->uid && current->egid == current->gid)
current->dumpable = 1;
// 此外,进程的task_struct结构中有一个字符数组用来保存进程所执行的程序名,
// 所以还要把bprm->filename的目标程序路径名中的最后一段抄过去。
name = bprm->filename;
for (i=0; (ch = *(name++)) != '\0';) {
if (ch == '/')
i = 0;
else
if (i < 15)
current->comm[i++] = ch;
}
current->comm[i] = '\0';
// 接着的flush_thread()只是处理与 debug和i387协处理器有关的内容,不是我们所关心的。
flush_thread();
// 如果“当前进程”原来只是一个线程,那么它的task_struct结构通过结构中的队列头thread_group
// 挂入由其父进程为首的“线程组”队列。现在,它已经在通过execve()升级为进程,放弃了对父进程
// 用户空间的共享,所以就要通过de_thread()从这个线程组中脱离出来。这个函数的代码在fs/exec.c中
de_thread(current);
if (bprm->e_uid != current->euid || bprm->e_gid != current->egid ||
permission(bprm->file->f_dentry->d_inode,MAY_READ))
current->dumpable = 0;
/* An exec changes our domain. We are no longer part of the thread
group */
current->self_exec_id++;
// 前面说过,进程的信号处理表就好像是个中断向量表。但是,这里还有个重要的不同,就是中断
// 向量表中的表项要么指向一个服务程序,要么就没有;而信号处理式中则还可以有对各种信号预设的
// (default)响应,并不定非要指向一个服务程序。当把信号处理表从父进程复制过来时,其中每个
// 表项的值有三种可能:一种可能是SIG_IGN,表示不理睬;第二种是SIG_DFL,表示采取预设的响应
// 方式(例如收到SIGQUIT就exit()):笫二种就是指向一个用户空间的子程行。可是,现在整个用户空
// 间都已经放弃了,怎么还能让信号处理表的表项指向用户空间的子程序呢?所以还得检查一遍,将指
// 向服务程序的表项改成SIG_DFL。这是由flush_signal_handler()完成的,代码在kernel/signal.c 中
flush_signal_handlers(current);
// 最后,是对原有已打开文件的处理,这是由flush_old_files()完成的。进程的task_struct结构中有
// 个指向一个file_struct结构的指针“files",所指向的数据结构中保存着己打开文件的信息。在
// file_struct 结构中有个位图close_on_exec,里面存储着表示哪些文件在执行一个新目标程序时应予
// 关闭的信息。 而flush_old_files()要做的就是根据这个位图的指示将这型文件关闭,并且将此位图
// 清成全0。其代码在 exec.c 中
flush_old_files(current->files);
return 0;
mmap_failed:
flush_failed:
spin_lock_irq(¤t->sigmask_lock);
if (current->sig != oldsig)
kfree(current->sig);
current->sig = oldsig;
spin_unlock_irq(¤t->sigmask_lock);
return retval;
}
⑷ 文字形式可执行文件的执行
前面介绍了 a.out 格式可执行文件的装入和投运过程,我们把这作为二进制可执行文件的代表。现在,再来简要地看一下字符形式的可执行文件 (为 shell 过程或 perl 文件) 的执行。有关的代码都在 binfmt_script.c 中。山于已经比较详细地阅读了二进制可执行文件的处理,读者在阅读下面的代码时应该比较轻松了,所以我们只作一些简要的提示。
// fs/binfmt_script.c
struct linux_binfmt script_format = {
NULL, THIS_MODULE, load_script, NULL, NULL, 0
};
static int load_script(struct linux_binprm *bprm,struct pt_regs *regs)
{
char *cp, *i_name, *i_arg;
struct file *file;
char interp[BINPRM_BUF_SIZE];
int retval;
if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!') || (bprm->sh_bang))
return -ENOEXEC;
/*
* This section does the #! interpretation.
* Sorta complicated, but hopefully it will work. -TYT
*/
bprm->sh_bang++;
allow_write_access(bprm->file);
fput(bprm->file);
bprm->file = NULL;
bprm->buf[BINPRM_BUF_SIZE - 1] = '\0';
if ((cp = strchr(bprm->buf, '\n')) == NULL)
cp = bprm->buf+BINPRM_BUF_SIZE-1;
*cp = '\0';
while (cp > bprm->buf) {
cp--;
if ((*cp == ' ') || (*cp == '\t'))
*cp = '\0';
else
break;
}
for (cp = bprm->buf+2; (*cp == ' ') || (*cp == '\t'); cp++);
if (*cp == '\0')
return -ENOEXEC; /* No interpreter name found */
i_name = cp;
i_arg = 0;
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
/* nothing */ ;
while ((*cp == ' ') || (*cp == '\t'))
*cp++ = '\0';
if (*cp)
i_arg = cp;
strcpy (interp, i_name);
// 得到了解释程序的路径名以后,问题就转化成了对解释程序的装入,而script文件本身则转化成了
// 解释程序的运行参数。虽然script文件本身并不是二进制格式的可执行文件,解释程序的映象却是一个
// 二进制的可执行文件。
/*
* OK, we've parsed out the interpreter name and
* (optional) argument.
* Splice in (1) the interpreter's name for argv[0]
* (2) (optional) argument to interpreter
* (3) filename of shell script (replace argv[0])
*
* This is done in reverse order, because of how the
* user environment and arguments are stored.
*/
remove_arg_zero(bprm);
retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval < 0) return retval;
bprm->argc++;
if (i_arg) {
retval = copy_strings_kernel(1, &i_arg, bprm);
if (retval < 0) return retval;
bprm->argc++;
}
retval = copy_strings_kernel(1, &i_name, bprm);
if (retval) return retval;
bprm->argc++;
/*
* OK, now restart the process with the interpreter's dentry.
*/
file = open_exec(interp);
if (IS_ERR(file))
return PTR_ERR(file);
bprm->file = file;
retval = prepare_binprm(bprm);
if (retval < 0)
return retval;
return search_binary_handler(bprm,regs);
// 可见,Script文件的使用在装入运行的过程中引入了递归性,load_script() 最后又调用
// search_binary_handler()不管递归有多深,显终执行的一定是个二进制可执行文件,例如
// /bin/sh、 /usr/bin/perl 等解释程序。在递归的过程中,逐层的可执行文件路径名形成
// 一个参数堆栈,传递给最终的解释程序。
}
5、系统调用exit()与wait4()
系统调用 exit() 与 wait4() 的代码基本上都在 kernel/exit.c 中,下面我们在引用代码时凡不特别说明出处的均来自这个文件。
先来看 exit() 的实现。
(1)sys_exit
// kernel/exit.c
asmlinkage long sys_exit(int error_code)
{
do_exit((error_code&0xff)<<8);
}
// 首先,在函数的类型void前面还行个说明NORET_TYPE。在include/linux/kernel.h中NORET_TYPE 定义为
// “/**/”,所以对编译毫无影响,但起到了提醒读者的作用。CPU在进入do_exit()以后,当前进
// 程就在中途寿终正寝,不会从这个函数返回。所谓不会从这个函数返回到底是怎么回事,又是什么原
// 因,读者在读了下面的代码以后就明白了。
NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;
// 这里只指出,既然CPU不会从do_exit()中返回,也就不会
// 从sys_exit()中返回,从而也就不会从系统调用exit()返回。也只有这样,才能达到“exit”,即从系统
// 退出的目的。另一方面,所谓 exit,只有进程(或线程)才谈得上。中断服务程序根本就不应该调用
// do_exit(),不管是直接还是间接调用。所以,这里首先通过in_interrupt()对此加以检杏,如发现这是
// 在某个中断服务程序中调用的,那就一定是出了问题。
// 那么,怎么知道是否在中断服务程序中呢?让我们来看看在
// include/asm-i386/hardirq.h中定义的 in_interrupt()
if (in_interrupt())
panic("Aiee, killing interrupt handler!");
if (!tsk->pid)
panic("Attempted to kill the idle task!");
if (tsk->pid == 1)
panic("Attempted to kill init!");
tsk->flags |= PF_EXITING;
// 进程在决定退出之前可能已经设置了实时定时器,也就是将其 task_struct 结构中的成员 real_timer
// 挂入了内核中的定时器队列。现在进程即将退出系统,一来是这个定时器己经没有了存在的必要,
// 一来进程的task_struct 结构行将撤销,作为其成员的real_timer 也将“皮之不存,毛将焉附”,当然要先
// 将它从队列中脱离。所以,要通过del_timer_sync()将当前进程从定时器队列中脱离出来。
del_timer_sync(&tsk->real_timer);
fake_volatile:
#ifdef CONFIG_BSD_PROCESS_ACCT
acct_process(code);
#endif
// 可想而知,进程在结束生命退出系统之前要释放其所有的资源。我们在前一节的do_fork()中看到
// 从父进程“继承”的资源有存储空间、已打开文件、工作目录、信号处理表等等。相应地,这里就有
// __exit_mm()、__exit_files()、__exit_fs() 以及 exit_sighand()。可是,还有一种资源是不
// "继承”的, 所以在do_fork()中不会看到,那就是进程在用户空间建立和使用的“信号量”(semaphore)。
// 这是一种用于进程间通讯的资源,如果在调用exit()之前还有信号量尚未撤销,那就也要把它撤销。
// 这里有一个简单的准则,就是看task_struct数据结构中的各个成分,如果一个成分是个指针,
// 在进程创建时以及运行过程中要为其在内核中分配一个数据结构或缓冲区,并且这个指针又是
// 通向这个数据结构或缓冲区的惟一途径,那就一定要把它释放,否则就会造成内核的存储空间“泄漏”。
// 例如,指针sig指向进程的信号处理表,这个表所占的空间是专为Sig分配的,指针sig就是进入这个表
// 的惟一途径,所以必须释放。而指针p_pptr指向父进程的task_struct结构,可是父进程的task_struct
// 结构却并不是专门为子进程的p_pptr而分配的,这个p_pptr并不是进入其父进程的task_struct
// 的惟一途径,所以不能把这个数据结构也释放掉,否则其他指向这个结构的指针就都 “悬空” 了。
// 具体到用户空间信号量,当进程在用户空间创建和使用信号量时,内核会为进程task_struct结构中
// 的两个指针 semundo 和 semsleeping 分配缓冲区 (semundo 数据结构和sem_queue数据结构,
// 详见“进程间通信”)。而且,这两个指针就是进入这些数据结构的惟一途径,所以必须把它们释放。
// 函数sem_exit()的代码在 ipc/sem.c 中。
__exit_mm(tsk);
lock_kernel();
sem_exit();
__exit_files(tsk);
__exit_fs(tsk);
exit_sighand(tsk);
exit_thread();
// 如果当前进程是一个session中的主进程(current->leader 非 0),那就还要将整个session与其主控终
// 端的联系切断,并将该tty释放(注意,进程的task_struct结构中有个指针tty指向其主控终端)。
// 函数 disassociate_ctty() 的代码在 drivers/char/tty_io.c 中
if (current->leader)
disassociate_ctty(1);
put_exec_domain(tsk->exec_domain);
if (tsk->binfmt && tsk->binfmt->module)
__MOD_DEC_USE_COUNT(tsk->binfmt->module);
tsk->exit_code = code;
// 接着,当前进程的状态就改成了 TASK_ZOMBIE,表示进程的生命已经结束,从此不再接受调度。
// 但是当前进程的残骸仍旧占用着最低限度的资源,包括其task_struct数据结构和系统空间堆栈所在的
// 两个页面。什么时候释放这两个页面呢?当前进程自己并不释放这两个页面,就像人们自己并不在临
// 终前注销自己的户口一样,而是调用exit_notify()通知其父进程,让父进程料理后事。
// 为什么要这样安排,而不是让当前进程,也就是子进程自己照料一切呢?有两个原因。首先,在
// 子进程的task_struct数据结构中还有不少有用的统计信息,让父进程来料理后事可以将这些统计信息
// 并入父进程的统计信息中而不会使这些信息丢失。其次,也许更重要的是,系统一但进入多进程状态
// 以后,任何一刻都需要有个“当前进程”存在。读者在第3章中看到了,在中断服务程序以及异常处
// 理程序中都要用到当前进程的系统空间堆栈。如果子进程在系统调度另一个进程投入运行之前就把它
// 的task_struct结构和系统空间堆栈释放,那就会造成一个空隙,如果恰好有一次中断或者异常在此空
// 隙中发生就会造成问题。诚然,中断是可以关闭的,可是异常却不能通过关中断来防止其发生,更何
// 况还有“不可屏蔽中断”哩。所以,子进程的task_struct结构和系统空间堆栈必须要保存到另一个进
// 程开始运行之后才能释放。这样,让父进程料理后事就是一个合理的安排了。此外,这样安排也有利
// 于使程序简化,否则的话调度程序schedule()就得要多考虑一班特殊情况了。让我们来看看exit.c中函
// 数 exit_notify()的源代码
exit_notify();
// 回到do_exit()中时,剩下的大事只有一件了,那就是schedule(),即进程调度。前面讲过,
// do_exit()是不返回的,实际上使do_exit()不返回的正是这里的schedule()。换言之,在这里对
// schedule()的调用是不返回的。当然,在正常条件下对 schedule() 的调用是返回的,
// 只不过返回的时机要延迟到本进程再次被调度而进入运行的时候。函数 schedule() 按照一定的准则
// 从系统中挑选一个最适合的进程进入运行。这个进程有可能就是正在运行的进程本身,也可能是
// 另一个进程。如果不同的话,那就要进行切换。而当前进程虽然被暂时剥夺了运行权,
// 却维持其“运行状态”,即task->state不变,等待下一次又在schedule()中(由另一个进程引起,
// 或者因中断进入内核后从系统空间返回用户空间之前)被选中时再继续运行,从而从schedule()中返回。
// 所以,什么时候从schedule()返回取决于什么时候被进程调度选中而得以继续运行。可是,在这里,当
// 前进程的task一>state已经变成了 TASK_ZOMBIE,这个条件使它在 schedule() 中永远不会再被选中,
// 所以就“黄鹤一去不复返了”。而这里对schedule()的调用,实际上(从CPU的角度看)也是返回的,只
// 不过是返回到另一个进程中去了,只是从当前进程的角度来看没有返回而已。不过,至此为止,当前
// 进程还只是因为不会被选中而不能返回,从理论上说只是无限推迟而已,其task_struct结构还是存在
// 的。到父进程收到子进程发来的信号而来料理后事,将子进程的task_struct结构释放之时,子进程就
// 最终从系统中消失了。在我们这个情景中,父进程正在wait4()中等着哩。
schedule();
BUG();
/*
* In order to get rid of the "volatile function does return" message
* I did this little loop that confuses gcc to think do_exit really
* is volatile. In fact it's schedule() that is volatile in some
* circumstances: when current->state = ZOMBIE, schedule() never
* returns.
*
* In fact the natural way to do all this is to have the label and the
* goto right after each other, but I put the fake_volatile label at
* the start of the function just in case something /really/ bad
* happens, and the schedule returns. This way we can try again. I'm
* not paranoid: it's just that everybody is out to get me.
*/
goto fake_volatile;
}
⑴ in_interrupt
// include/asm-i386/hardirq.h
/*
* Are we in an interrupt context? Either doing bottom half
* or hardware interrupt processing?
*/
#define in_interrupt() ({ int __cpu = smp_processor_id(); \
(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
在单 CPU 的系统中,__cpu 一定是 0。在第3章中讲到过函数 handle_IRQ_event(),在其入口处和出口处各有一个函数调用 irq_enter() 和 irq_exit() ,就分别递增和递减计数器 local_irq_count[ __cpu ] 。 所以,只要这个计数器为非 0,就说明 CPU 在 handle_IRQ_event() 中。类似地,只要计数器 local_bh_count[ __cpu ] 为非 0,就说明 CPU 正在执行某个 bh 函数,这也跟中断服务程序一样。反之, 只要不是在中断服务的上下文中,那就一定是在某个进程 (或线程) 的上下文中了。但是,0 号进程和 1 号进程,也就是 “空转” (idle) 进程和 “初始化” (init) 进程,是不允许退出的,所以接着要对当前进程的 pid 加以检查。
⑵ sem_exit
// ipc/sem.c
/*
* add semadj values to semaphores, free undo structures.
* undo structures are not freed when semaphore arrays are destroyed
* so some of them may be out of date.
* IMPLEMENTATION NOTE: There is some confusion over whether the
* set of adjustments that needs to be done should be done in an atomic
* manner or not. That is, if we are attempting to decrement the semval
* should we queue up and wait until we can do so legally?
* The original implementation attempted to do this (queue and wait).
* The current implementation does not do so. The POSIX standard
* and SVID should be consulted to determine what behavior is mandated.
*/
void sem_exit (void)
{
struct sem_queue *q;
struct sem_undo *u, *un = NULL, **up, **unp;
struct sem_array *sma;
int nsems, i;
/* If the current process was sleeping for a semaphore,
* remove it from the queue.
*/
// 如果当前过程正在(睡眠)等待进入某个临界区,则其task_struct结构中的指针semsleeping指向
// 所在的队列。显然,现在不需要再等待了,所以把当前过程从这个队列中脱链。接着是一个for循环,
// 料理那些正在由当前过程所创建的用户空间信号量(即临界区)上操作的过程,告诉它们:信号量已
// 经撤销,临界区已经要“清场”并“关门大吉”,大家请回吧。建议读者在学习了 “进程间通信”的有
// 关内容后再回过来自己读一下这段代码。
if ((q = current->semsleeping)) {
int semid = q->id;
sma = sem_lock(semid);
current->semsleeping = NULL;
if (q->prev) {
if(sma==NULL)
BUG();
remove_from_queue(q->sma,q);
}
if(sma!=NULL)
sem_unlock(semid);
}
for (up = ¤t->semundo; (u = *up); *up = u->proc_next, kfree(u)) {
int semid = u->semid;
if(semid == -1)
continue;
sma = sem_lock(semid);
if (sma == NULL)
continue;
if (u->semid == -1)
goto next_entry;
if (sem_checkid(sma,u->semid))
goto next_entry;
/* remove u from the sma->undo list */
for (unp = &sma->undo; (un = *unp); unp = &un->id_next) {
if (u == un)
goto found;
}
printk ("sem_exit undo list error id=%d\n", u->semid);
goto next_entry;
found:
*unp = un->id_next;
/* perform adjustments registered in u */
nsems = sma->sem_nsems;
for (i = 0; i < nsems; i++) {
struct sem * sem = &sma->sem_base[i];
sem->semval += u->semadj[i];
if (sem->semval < 0)
sem->semval = 0; /* shouldn't happen */
sem->sempid = current->pid;
}
sma->sem_otime = CURRENT_TIME;
/* maybe some queued-up processes were waiting for this */
update_queue(sma);
next_entry:
sem_unlock(semid);
}
current->semundo = NULL;
}
⑶ __exit_mm
// kernel/exit.c
/*
* Turn us into a lazy TLB process if we
* aren't already..
*/
static inline void __exit_mm(struct task_struct * tsk)
{
struct mm_struct * mm = tsk->mm;
mm_release();
if (mm) {
atomic_inc(&mm->mm_count);
if (mm != tsk->active_mm) BUG();
/* more a memory barrier than a real lock */
task_lock(tsk);
tsk->mm = NULL;
task_unlock(tsk);
enter_lazy_tlb(mm, current, smp_processor_id());
mmput(mm);
}
}
实际的存储空间释放是调用 mmput() 完成的 (代码在 fork.c 中) ,我们已在前一节中读过它的代码, 这里要提醒读者的是这里对 mm_release() 的调用。在 fork() 和 execve() 两节中,读者已经看到,当 do_fork() 时标志位 CLONE_VFORK 为 1 时,父进程在睡眠,等待子进程在一个信号量上执行一次 up() 操作以后才能回到用户空间运行,而子进程必须在释放其用户存储空间时执行这个操作,所以这里要通过 mm_release() ,在这个信号量上执行一次 up() 操作唤醒睡眠中的父进程。其代码上列出在 execve() 一节中,这里不再重复。
将一个进程的 task_struct 结构中的指针 mm 清成 0,这个进程便不再有用户空间了。
⑷ exit_notify
// kernel/exit.c
/*
* Send signals to all our closest relatives so that they know
* to properly mourn us..
*/
static void exit_notify(void)
{
struct task_struct * p, *t;
// 就像人一样,所谓父进程也有“生父”和“养父”之分。在task_struct结构中有个指针p_opptr指
// 向其“original parent"也即生父,另外还有个指针p_pptr则指向养父。一个进程在创建之初其生父和
// 养父是一致的,所以两个指针指向同一个父进程。但是,在运行中p_pptr可以暂时地改变。这种改变
// 发生在一个进程通过系统调用ptrace()来跟踪另一个进程的时候,这时候被跟踪进程的p_pptr指针被设
// 置成指向正在跟踪它的进程,那个进程就暂时成了被跟踪进程的“养父”。而被跟踪进程的p_opptr指
// 针却不变,仍旧指向其生父。如果一个进程在其子进程之前“去世”的话,就要把它的子进程托付给
// 某个进程。托付给谁呢?如果当前进程是一个线程,那就托付给同一线程组中的下一个线程,使子进
// 程的p_opptr指向这个线程。否则,就只好托付给系统中的init进程,所以这init进程就好像是孤儿院。
// 由此可见,所谓“original parent"也不是永远不变的,原因在于系统中的进程号pid以及用作
// task_struct 结构的页面都是在周转使用的,所以实际上一来并没有保留这个记录的意义,
// 二来技术上也有困难。 现在,当前进程要exit()了,所以要将其所有的子进程都送进“孤儿院”,
// 要不然到它们也要exit()的时候就没有父进程来料理它们的后事了。这就是331行调用
// forget_original_parent()的目的(exit.c)。
forget_original_parent(current);
/*
* Check to see if any process groups have become orphaned
* as a result of our exiting, and if they have any stopped
* jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2)
*
* Case i: Our father is in a different pgrp than we are
* and we were the only connection outside, so our pgrp
* is about to become orphaned.
*/
// 一个用户login到系统中以后,可能会启动许多不同的进程,所有这些进程都使用同一个控制终端
// (或用来模拟一个终端的窗口)。这些使用同一个控制终端的进程属于同一个session。此外,用户可以
// 在同一条shell命令或执行程序中启动多个进程,例如在命令“ls | wc -l”中就同时启动了两个进程,
// 这些进程形成一个“组”(session与组是两个不同的概念)。每个session或进程组中都有一个为主的、
// 最早创建的进程,这个进程的pid就成为session和进程组的代号。如果当前进程与父进程属于不同的
// session,不同的组,同时又是其所在的组与其父进程之间惟一的纽带,那么一旦当前进程不存在以后,
// 这整个组就成了 “孤儿”。在这样的情况下,按POSIX 322.2的规定要给这个进程组中所有的进程都
// 先发一个SIGHUP信号,然后再发一个SIGCONT信号,这是由kill_pg()完成的。
t = current->p_pptr;
if ((t->pgrp != current->pgrp) &&
(t->session == current->session) &&
will_become_orphaned_pgrp(current->pgrp, current) &&
has_stopped_jobs(current->pgrp)) {
kill_pg(current->pgrp,SIGHUP,1);
kill_pg(current->pgrp,SIGCONT,1);
}
/* Let father know we died
*
* Thread signals are configurable, but you aren't going to use
* that to send signals to arbitary processes.
* That stops right now.
*
* If the parent exec id doesn't match the exec id we saved
* when we started then we know the parent has changed security
* domain.
*
* If our self_exec id doesn't match our parent_exec_id then
* we have changed execution domain as these two values started
* the same after a fork.
*
*/
if(current->exit_signal != SIGCHLD &&
( current->parent_exec_id != t->self_exec_id ||
current->self_exec_id != current->parent_exec_id)
&& !capable(CAP_KILL))
current->exit_signal = SIGCHLD;
/*
* This loop does two things:
*
* A. Make init inherit all the child processes
* B. Check to see if any process groups have become orphaned
* as a result of our exiting, and if they have any stopped
* jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2)
*/
write_lock_irq(&tasklist_lock);
current->state = TASK_ZOMBIE;
do_notify_parent(current, current->exit_signal);
while (current->p_cptr != NULL) {
p = current->p_cptr;
current->p_cptr = p->p_osptr;
p->p_ysptr = NULL;
p->ptrace = 0;
p->p_pptr = p->p_opptr;
p->p_osptr = p->p_pptr->p_cptr;
if (p->p_osptr)
p->p_osptr->p_ysptr = p;
p->p_pptr->p_cptr = p;
if (p->state == TASK_ZOMBIE)
do_notify_parent(p, p->exit_signal);
/*
* process group orphan check
* Case ii: Our child is in a different pgrp
* than we are, and it was the only connection
* outside, so the child pgrp is now orphaned.
*/
if ((p->pgrp != current->pgrp) &&
(p->session == current->session)) {
int pgrp = p->pgrp;
write_unlock_irq(&tasklist_lock);
if (is_orphaned_pgrp(pgrp) && has_stopped_jobs(pgrp)) {
kill_pg(pgrp,SIGHUP,1);
kill_pg(pgrp,SIGCONT,1);
}
write_lock_irq(&tasklist_lock);
}
}
write_unlock_irq(&tasklist_lock);
}
⑸ do_notify_parent
// kernel/signal.c
/*
* Let a parent know about a status change of a child.
*/
void do_notify_parent(struct task_struct *tsk, int sig)
{
struct siginfo info;
int why, status;
info.si_signo = sig;
info.si_errno = 0;
info.si_pid = tsk->pid;
info.si_uid = tsk->uid;
/* FIXME: find out whether or not this is supposed to be c*time. */
info.si_utime = tsk->times.tms_utime;
info.si_stime = tsk->times.tms_stime;
status = tsk->exit_code & 0x7f;
why = SI_KERNEL; /* shouldn't happen */
switch (tsk->state) {
case TASK_STOPPED:
/* FIXME -- can we deduce CLD_TRAPPED or CLD_CONTINUED? */
if (tsk->ptrace & PT_PTRACED)
why = CLD_TRAPPED;
else
why = CLD_STOPPED;
break;
default:
if (tsk->exit_code & 0x80)
why = CLD_DUMPED;
else if (tsk->exit_code & 0x7f)
why = CLD_KILLED;
else {
why = CLD_EXITED;
status = tsk->exit_code >> 8;
}
break;
}
info.si_code = why;
info.si_status = status;
send_sig_info(sig, &info, tsk->p_pptr);
wake_up_parent(tsk->p_pptr);
}
参数 tsk 指向当前进程的 task_struct 结构,只有当进程处于 TASK_ZOMBTE ( 正在 exit() ) 或 TASK_STOPPED (被跟踪) 时才允许调用 do_notify_parent() 。从代码中可见,这里的所谓 parent 是指当前进程的“养父”而不是“生父”,也就是由指针 p_pptr 所指而不是 p_opptr 所指的进程。在前面的 forget_original_parent() 中已经把每个子进程的 p_opptr 改成了指向 child_reaper,而 notify_parent() 中却是向 p_pptr 所指进程发信号;那样,将来当那些子进程要 exit() 时岂不是要向一个已经不存在了的父进程发信号吗?不要紧,exit_notify() 的代码中随后(392行)就把子进程的 p_pptr 设置成与 p_opptr 相同。
进程之间都通过亲缘关系连接在一起而形成“关系网”,所用的指针除 p_opptr 和 p_pptr 外,还有:
- p_cptr,指向子进程,这里的 c 表示 "child”。p_cptr 与 p_pptr 是相对应的。当一个进程有多个子进程时,p_cptr 指向其“最年轻的”,也就是最近创建的那个子进程。
- p_ysptr,指向当前进程的“弟弟”,这里的 y 表示 “younger”,而 s 表示 “sibling”。
- p_osptr,指向当前进程的“哥哥”,这里的。表示 “older"。
这样,当前进程的所有子进程都通过 p_ysptr 和 p_osptr 连接在一起形成一个双链队列。队列中每一个进程的 p_pptr 都指向当前进程,同当前进程的 p_optr 则指向队列中最后创建的子进程。有趣的是, 子进程在行事时只认其“养父”,而 p_opptr 所指的“生父”倒似乎无关紧要。当然,一个进程除身处这个由亲属关系形成的队列中之外,同时也身处其他的队列中,所以 task_struct 结构中还有其他的 task_struct 指针,从而形成一个并不简单的“关系网”。进程是在创建的时候在 do_fork() 中通过 SET_LINK 进入这个关系网的。SET_LINK 的定义在 include/linux/sched.h 中。
⑹ disassociate_ctty
// drivers/char/tty_io.c
/*
* This function is typically called only by the session leader, when
* it wants to disassociate itself from its controlling tty.
*
* It performs the following functions:
* (1) Sends a SIGHUP and SIGCONT to the foreground process group
* (2) Clears the tty from being controlling the session
* (3) Clears the controlling tty for all processes in the
* session group.
*
* The argument on_exit is set to 1 if called when a process is
* exiting; it is 0 if called by the ioctl TIOCNOTTY.
*/
void disassociate_ctty(int on_exit)
{
struct tty_struct *tty = current->tty;
struct task_struct *p;
int tty_pgrp = -1;
if (tty) {
tty_pgrp = tty->pgrp;
if (on_exit && tty->driver.type != TTY_DRIVER_TYPE_PTY)
tty_vhangup(tty);
} else {
if (current->tty_old_pgrp) {
kill_pg(current->tty_old_pgrp, SIGHUP, on_exit);
kill_pg(current->tty_old_pgrp, SIGCONT, on_exit);
}
return;
}
if (tty_pgrp > 0) {
kill_pg(tty_pgrp, SIGHUP, on_exit);
if (!on_exit)
kill_pg(tty_pgrp, SIGCONT, on_exit);
}
current->tty_old_pgrp = 0;
tty->session = 0;
tty->pgrp = -1;
read_lock(&tasklist_lock);
for_each_task(p)
if (p->session == current->session)
p->tty = NULL;
read_unlock(&tasklist_lock);
}
那么,进程与主控终端的这种联系最初是怎样,以及在什么时候建立的呢?显然,在创建子进程时,将父进程的 task_struct 结构复制给子进程的过程中把结构中的 tty 指针也复制了下来,所以子进程具有与父进程相同的主控终端。但是子进程可以通过 ioctl() 系统调用来改变主控终端,也可以先将当前的主控终端关闭然后再打开另一个时。不过,在此之前先得通过 setsid() 系统调用来建立一个新的人机交互分组 (session),并使得作此调用的进程成为该 session 的主进程 (leader)。一个 session 的主进程与其主控终端断绝关系意味着整个 session 中的进程都与之断绝了关系,所以要给同一 session 中的进程发出信号。从此以后,这些进程就没有主控终端,成了 “后台进程” 。
(2)sys_wait4
// kernel/exit.c
asmlinkage long sys_wait4(pid_t pid,unsigned int * stat_addr, int options, struct rusage * ru)
{
int flag, retval;
// 参数pid为某一个子进程的进程号。
// 首先,在当前进程的系统空间堆栈中通过DECLARE_WAITQUEUE分配空间并建立了一个
// wait_queue_t数据结构。有关的宏定义和数据结构都是在 include/linux/wait.h 中定义的
//
// 也就是说,sys_wait4() 一开头就在当前进程的系统堆栈上分配一个 wait_queue_t 数据结构
// (名为 wait),结构中的 compiler_warning 为 0x1234567,指针 task 指向当前进程的
// task_struct,而 list_head 结构 task_list 中的两个指针均为NULL。由于这个数据结构建立
// 在当前进程的系统空间堆栈中,一旦从 sys_wait4() 返回,这个数据结构就不复存在了。与此相应,
// 在进程的 task_struct 中有个 wait_queue_head_t 数据结构 wait_chldexit 用于这个目的。
DECLARE_WAITQUEUE(wait, current);
struct task_struct *tsk;
if (options & ~(WNOHANG|WUNTRACED|__WNOTHREAD|__WCLONE|__WALL))
return -EINVAL;
// 然后,通过 add_wait_queue() 将这个数据结构(wait)加入到当前进程的 wait_chldexit 队列中。
// 这样做的作用在下面重温了 do_notify_parent() 的代码以后就会清楚。接着,就进入了一个循环,
// 这是一个不小的循环
add_wait_queue(¤t->wait_chldexit,&wait);
repeat:
flag = 0;
current->state = TASK_INTERRUPTIBLE;
read_lock(&tasklist_lock);
tsk = current;
// 这个由 goto 实现的循环要到当前进程被调度运行,并且下列条件之一得到满足时才结束(见代码
// 中的 "goto end_wait4" 语句):
// 所等待的子进程的状态变成 TASK_STOPPED 或 TASK_ZOMBIE;
// 所等待的子进程存在,可是不在上述两个状态,而调用参数 options 中的 WNOHANG 标志位为
// 1,或者当前进程收到了其他的信号;
// 进程号为pid的那个进程根本不存在,或者不是当前进程的子进程。
// 否则,当前进程将其自身的状态设成TASK_INTERRUPTIBLE(见499行)并在575行调用schedule()
// 进入睡眠让别的进程先运行。当该进程因收到信号而被唤醒,并且受到调度从schedule()返回时,就又
// 经由576行的goto语句转回repeat,再次通过一个for循环扫描其子进程队列,看看所等待的子进程的
// 状态是否满足条件。这里的for循环扫描一个进程的所有子进程,从最年轻的子进程开始沿着由各个
// task_struct结构中的指针p_osptr所形成的链扫描,找寻与所等待对象的pid相符的子进程、或符合其
// 他一些条件的子进程。
do {
struct task_struct *p;
for (p = tsk->p_cptr ; p ; p = p->p_osptr) {
if (pid>0) {
if (p->pid != pid)
continue;
} else if (!pid) {
if (p->pgrp != current->pgrp)
continue;
} else if (pid != -1) {
if (p->pgrp != -pid)
continue;
}
/* Wait for all children (clone and not) if __WALL is set;
* otherwise, wait for clone children *only* if __WCLONE is
* set; otherwise, wait for non-clone children *only*. (Note:
* A "clone" child here is one that reports to its parent
* using a signal other than SIGCHLD.) */
if (((p->exit_signal != SIGCHLD) ^ ((options & __WCLONE) != 0))
&& !(options & __WALL))
continue;
flag = 1;
switch (p->state) {
case TASK_STOPPED:
if (!p->exit_code)
continue;
if (!(options & WUNTRACED) && !(p->ptrace & PT_PTRACED))
continue;
read_unlock(&tasklist_lock);
retval = ru ? getrusage(p, RUSAGE_BOTH, ru) : 0;
if (!retval && stat_addr)
retval = put_user((p->exit_code << 8) | 0x7f, stat_addr);
if (!retval) {
p->exit_code = 0;
retval = p->pid;
}
goto end_wait4;
case TASK_ZOMBIE:
current->times.tms_cutime += p->times.tms_utime + p->times.tms_cutime;
current->times.tms_cstime += p->times.tms_stime + p->times.tms_cstime;
read_unlock(&tasklist_lock);
retval = ru ? getrusage(p, RUSAGE_BOTH, ru) : 0;
if (!retval && stat_addr)
retval = put_user(p->exit_code, stat_addr);
if (retval)
goto end_wait4;
retval = p->pid;
if (p->p_opptr != p->p_pptr) {
write_lock_irq(&tasklist_lock);
REMOVE_LINKS(p);
p->p_pptr = p->p_opptr;
SET_LINKS(p);
do_notify_parent(p, SIGCHLD);
write_unlock_irq(&tasklist_lock);
} else
release_task(p);
goto end_wait4;
default:
continue;
}
}
if (options & __WNOTHREAD)
break;
// 这个for循环又嵌套在一个do_while循环中。为什么要有这个外层的do_while
// 循环呢?这是因为当前进程可能是一个线程,而所等待的对象实际上是由同一个进程克隆出来的另一
// 个线程的子进程,所以要通过这个do_while循环来检查同一个thread_group中所有线程的子进程。代
// 码中的next_thread()从同一个thread_group队列中找到下一个线程的task_struct结构,并使局部量tsk
// 指向这个结构。
tsk = next_thread(tsk);
} while (tsk != current);
read_unlock(&tasklist_lock);
if (flag) {
retval = 0;
if (options & WNOHANG)
goto end_wait4;
retval = -ERESTARTSYS;
if (signal_pending(current))
goto end_wait4;
schedule();
// 在我们这个情景中,当父进程调用wait4()而第一次扫描其子进程队列时,该进程尚
// 在运行,所以通过schedule()进入睡眠。当子进程exit()时,会向父进程发一个信号,从而将其唤醒。
// 怎么唤醒呢?我们在前面看到,子进程在exit_notify()中通过do_notify_parent()向父进程发送信号。这
// 个函数准备下一个siginfo数据结构,然后调用send_sig_info()将其发送给父进程,并调用
// wake_up_process()将父进程唤醒。对send_sig_info()的代码我们将在“进程间通信”的信号一节中介
// 绍。而 wake_up_process(),则把父进程的状态从 TASK_INTERRUPTABLE 改成 TASK_RUNNING,并
// 将其转移到可执行队列中,使schedule()能够“看”到父进程而可以调度其运行。
// 当父进程因子进程在exit()向其发送信号而被唤醒时,就转回到前面sys_wait4()中的repeat处,又
// 一次扫描其子进程队列。这一次,子进程的状态已经改成 TASK_ZOMBIE 了,所以父进程在将子进程
// 在用户空间运行的时间和系统空间运行的时间两项统计数据合并入其自身的统计数据中。然后,在典
// 型的条件下,就调用release_task()将子进程残存的资源,就是其task_struct结构和系统空间堆栈,全
// 都释放(exit.c)
goto repeat;
}
retval = -ECHILD;
end_wait4:
current->state = TASK_RUNNING;
remove_wait_queue(¤t->wait_chldexit,&wait);
return retval;
}
在 sys_wait4() 中还有个特殊情况需要考虑,那就是万一子进程的 p_opptr 与 p_pptr 不同,也就是 说其“养父”与“生父”不同。如前所述,进程在 exit() 时,do_notify_parent() 的对象是其“养父”, 但是当“生父”与“养父”不同时,其“生父”可能也在等待,所以将子进程的 p_pptr 指针设置成与 p_opptr 相同,并通过 REMOVE_LINKS 将其 task_struct 从其“养父”的队列中脱离出来,再通过 SET_LINKS 把它归还给“生父”,重新挂入其“生父”的队列。然后,给其“生父”发一信号,让它自己来处理。
此外,根据当前进程在调用 wait4() 时的要求,还可能要把一些状态信息和统计信息通过 put_user() 复制到用户空间中。如果复制失败的话,那暂时就不能将子进程的 task_struct 结构释放了(这里的 “goto end_wait4” 跳过了对 release() 的调用)。在这种情况下,系统中会留下子进程的“尸体”,用户通过 “ps” 命令来观察系统中的进程状态时,会看到有个进程的状态为 “ZOMBIE” 。读者在前面看到:在 exit_notify() 当父进程要结束生命前为其子进程“托孤”时,还要看一下子进程的状态是否 TASK_ZOMBIE,若是的话,就要替它调用 do_notify_parent() 给新的“养父”发一信息,就是这个原因。
至此,在执行了 release() 以后,子进程就最终“灰飞烟灭”,从系统中消失了。
可是,要是父进程不在 wait4() 中等待呢?那也不要紧。读者在第3章中已经看到,每当进程从系统调用、中断或异常返回时,都要检查一下是否有信号等待处理,如有的话就转入 entry.S 中的 signal_return 处调用 do_signal()。而 do_signal() 中有一个片段为 (在 arch/i386/kernel/signal.c 中)。
⑴ DECLARE_WAITQUEUE
// include/linux/wait.h
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
struct task_struct * task;
struct list_head task_list;
#if WAITQUEUE_DEBUG
long __magic;
long __waker;
#endif
};
typedef struct __wait_queue wait_queue_t;
// ------------------------------------------------------
struct __wait_queue_head {
wq_lock_t lock;
struct list_head task_list;
#if WAITQUEUE_DEBUG
long __magic;
long __creator;
#endif
};
typedef struct __wait_queue_head wait_queue_head_t;
#if WAITQUEUE_DEBUG
# define __WAITQUEUE_DEBUG_INIT(name) \
, (long)&(name).__magic, 0
# define __WAITQUEUE_HEAD_DEBUG_INIT(name) \
, (long)&(name).__magic, (long)&(name).__magic
#else
# define __WAITQUEUE_DEBUG_INIT(name)
# define __WAITQUEUE_HEAD_DEBUG_INIT(name)
#endif
#define __WAITQUEUE_INITIALIZER(name,task) \
{ 0x0, task, { NULL, NULL } __WAITQUEUE_DEBUG_INIT(name)}
#define DECLARE_WAITQUEUE(name,task) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name,task)
⑵ release_task
// kernel/exit.c
static void release_task(struct task_struct * p)
{
if (p != current) {
#ifdef CONFIG_SMP
/*
* Wait to make sure the process isn't on the
* runqueue (active on some other CPU still)
*/
for (;;) {
task_lock(p);
if (!p->has_cpu)
break;
task_unlock(p);
do {
barrier();
} while (p->has_cpu);
}
task_unlock(p);
#endif
atomic_dec(&p->user->processes);
free_uid(p->user);
unhash_process(p);
release_thread(p);
current->cmin_flt += p->min_flt + p->cmin_flt;
current->cmaj_flt += p->maj_flt + p->cmaj_flt;
current->cnswap += p->nswap + p->cnswap;
/*
* Potentially available timeslices are retrieved
* here - this way the parent does not get penalized
* for creating too many processes.
*
* (this cannot be used to artificially 'generate'
* timeslices, because any timeslice recovered here
* was given away by the parent in the first place.)
*/
current->counter += p->counter;
if (current->counter >= MAX_COUNTER)
current->counter = MAX_COUNTER;
free_task_struct(p);
} else {
printk("task releasing itself\n");
}
}
这里通过 unhash_process() 把子进程的 task_struct 结构从杂凑表队列中摘除,然后把子进程的其他几项统计信息也合并入父进程。至于 release_thread() 只是检查进程的 LDT (如果有的话)是否确已释放。 最后,就调用 free_task_struct() 将 task_struct 结构和系统空间堆栈所占据的两个物理页面释放。
⑶ do_signal
// arch/i386/kernel/signal.c
int do_signal(struct pt_regs *regs, sigset_t *oldset)
{
// ...
ka = ¤t->sig->action[signr-1];
if (ka->sa.sa_handler == SIG_IGN) {
if (signr != SIGCHLD)
continue;
/* Check for SIGCHLD: it's special. */
while (sys_wait4(-1, NULL, WNOHANG, NULL) > 0)
/* nothing */;
continue;
}
// ...
}
可见父进程在收到 SIGCHLD 信号后还会被动地来调用 sys_wait4(),此时的调用参数 pid 为 -1, 表示同一个进程组中的任何一个子进程都在处理之列 (见sys_wait4() 的for循环中对参数 pid 的比对)。
当然,如果父进程已经为 SIGCHLD 信号设置了其他的处理程序,那就另作别论了。
读者也许还会问,怎样才能保证一定会有系统调用、中断或异常来迫使其父进程执行do_signal() 呢?万一父进程在运行时既不作系统调用,也不访问外设,更没有任何操作引起异常呢?别忘记时钟中断是周期性地发生的,要不然就连调度也有可能不会发生了,正因为如此,时钟中断才被看作是系统的“心跳”。
To be continued
⇐ ⇒ ⇔ ⇆ ⇒ ⟺
①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿
⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑿⒀⒁⒂⒃⒄⒅⒆⒇
➊➋➌➍➎➏➐➑➒➓⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴
⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵
ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ
ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ
🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩
123
y = x 2 + z 3 y = x^2 + z_3 y=x2+z3
y = x 2 + z 3 + a b + b a y = x^2 + z_3 + \frac {a}{b} + \sqrt[a]{b} y=x2+z3+ba+ab
y = x 2 + z 3 (1) y = x^2 + z^3 \tag{1} y=x2+z3(1)