迪趣网

​lkd是什么(内核该怎么学?Linux进程管理工作原理)

点击: 来源:迪趣网
摘要:lkd是什么(内核该怎么学?Linux进程管理工作原理) 前言: Linux内核里大部分都是C语言。建议先看《Linux内核设计与实现(Linux Kernel Development)》,Robert Love,也就是LKD。 Linux是一种动态系统,

lkd是什么(内核该怎么学?Linux进程管理工作原理)

前言:

Linux内核里大部分都是C语言。建议先看《Linux内核设计与实现(Linux Kernel Development)》,Robert Love,也就是LKD。

Linux是一种动态系统,能够适应不断变化的计算需求。Linux计算需求的表现是以进程的通用抽象为中心的。进程可以是短期的(从命令行执行的一个命令),也可以是长期的(一种网络服务)。因此,对进程及其调度进行一般管理就显得极为重要。

在用户空间,进程是由进程标识符(PID)表示的。从用户的角度来看,一个 PID 是一个数字值,可惟一标识一个进程。一个 PID 在进程的整个生命期间不会更改,但 PID 可以在进程销毁后被重新使用,所以对它们进行缓存并不见得总是理想的。在用户空间,创建进程可以采用几种方式。可以 执行一个程序(这会导致新进程的创建),也可以 在程序内,调用一个 fork或 exec 系统调用。fork调用会导致创建一个子进程,而exec调用则会用新程序代替当前进程上下文。这里将对这几种方法进行讨论以便您能很好地理解它们的工作原理。

这里将按照下面的顺序展开对进程的介绍,首先展示进程的内核表示以及它们是如何在内核内被管理的,然后来看看进程创建和调度的各种方式(在一个或多个处理器上),最后介绍进程的销毁。内核的版本为2.6.32.45。

一,进程描述符

在Linux内核内,进程是由相当大的一个称为 task_struct 的结构表示的。此结构包含所有表示此进程所必需的数据,此外,还包含了大量的其他数据用来统计(accounting)和维护与其他进程的关系(如父和子)。task_struct 位于
./linux/include/linux/sched.h(注意./linux/指向内核源代码树)。

下面是task_struct结构:

structtask_struct{
volatilelongstate;/*-1不可运行,0可运行,>0已停止*/
void*stack;/*堆栈*/
atomic_tusage;
unsignedintflags;/*一组标志*/
unsignedintptrace;
/*...*/
intprio,static_prio,normal_prio;/*优先级*/
/*...*/
structlist_headtasks;/*执行的线程(可以有很多)*/
structplist_nodepushable_tasks;
structmm_struct*mm,*active_mm;/*内存页(进程地址空间)*/
/*进行状态*/
intexit_state;
intexit_code,exit_signal;
intpdeath_signal;/*当父进程死亡时要发送的信号*/

/* ... */

pid_tpid;/*进程号*/
pid_ttgid;
/*...*/
structtask_struct*real_parent;/*实际父进程realparentprocess*/
structtask_struct*parent;/*SIGCHLD的接受者,由wait4()报告*/
structlist_headchildren;/*子进程列表*/
structlist_headsibling;/*兄弟进程列表*/
structtask_struct*group_leader;/*线程组的leader*/
/*...*/
charcomm[TASK_COMM_LEN];/*可执行程序的名称(不包含路径)*/
/*文件系统信息*/
intlink_count,total_link_count;
/*...*/
/*特定CPU架构的状态*/
structthread_structthread;
/*进程当前所在的目录描述*/
structfs_struct*fs;
/*打开的文件描述信息*/
structfiles_struct*files;
/*...*/
};

在task_struct结构中,可以看到几个预料之中的项,比如执行的状态、堆栈、一组标志、父进程、执行的线程(可以有很多)以及开放文件。state 变量是一些表明任务状态的比特位。最常见的状态有:TASK_RUNNING 表示进程正在运行,或是排在运行队列中正要运行;TASK_INTERRUPTIBLE 表示进程正在休眠、TASK_UNINTERRUPTIBLE表示进程正在休眠但不能叫醒;TASK_STOPPED 表示进程停止等等。这些标志的完整列表可以在
./linux/include/linux/sched.h 内找到。

flags 定义了很多指示符,表明进程是否正在被创建(PF_STARTING)或退出(PF_EXITING),或是进程当前是否在分配内存(PF_MEMALLOC)。可执行程序的名称(不包含路径)占用 comm(命令)字段。每个进程都会被赋予优先级(称为 static_prio),但进程的实际优先级是基于加载以及其他几个因素动态决定的。优先级值越低,实际的优先级越高。tasks字段提供了链接列表的能力。它包含一个 prev 指针(指向前一个任务)和一个 next 指针(指向下一个任务)。

进程的地址空间由mm 和 active_mm 字段表示。mm代表的是进程的内存描述符,而 active_mm 则是前一个进程的内存描述符(为改进上下文切换时间的一种优化)。thread_struct thread结构则用来标识进程的存储状态,此元素依赖于Linux在其上运行的特定架构。例如对于x86架构,在
./linux/arch/x86/include/asm/processor.h的thread_struct结构中可以找到该进程自执行上下文切换后的存储(硬件注册表、程序计数器等)。

代码如下:

structthread_struct{
/*CachedTLSdescriptors:*/
structdesc_structtls_array[GDT_ENTRY_TLS_ENTRIES];
unsignedlongsp0;unsignedlongsp;#ifdefCONFIG_X86_32
unsignedlongsysenter_cs;#else
unsignedlongusersp;/*CopyfromPDA*/
unsignedshortes;unsignedshortds;unsignedshortfsindex;unsignedshortgsindex;#endif#ifdefCONFIG_X86_32
unsignedlongip;#endif
/*...*/#ifdefCONFIG_X86_32
/*Virtual86modeinfo*/
structvm86_struct__user*vm86_info;
unsignedlongscreen_bitmap;unsignedlongv86flags;unsignedlongv86mask;unsignedlongsaved_sp0;unsignedintsaved_fs;unsignedintsaved_gs;#endif
/*IOpermissions:*/
unsignedlong*io_bitmap_ptr;unsignedlongiopl;/*Maxallowedportinthebitmap,inbytes:*/
unsignedio_bitmap_max;/*MSR_IA32_DEBUGCTLMSRvaluetoswitchinifTIF_DEBUGCTLMSRisset.*/
unsignedlongdebugctlmsr;/*DebugStorecontext;seeasm/ds.h*/
structds_context*ds_ctx;};

二,进程管理

在很多情况下,进程都是动态创建并由一个动态分配的 task_struct 表示。一个例外是init 进程本身,它总是存在并由一个静态分配的task_struct表示,参看
./linux/arch/x86/kernel/init_task.c。

代码如下:

staticstructsignal_structinit_signals=INIT_SIGNALS(init_signals);staticstructsighand_structinit_sighand=INIT_SIGHAND(init_sighand);
/*
*初始化线程结构
*/unionthread_unioninit_thread_union__init_task_data=
{INIT_THREAD_INFO(init_task)};
/*
*初始化init进程的结构。所有其他进程的结构将由fork.c中的slabs来分配
*/structtask_structinit_task=INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);
/*
*per-CPUTSSsegments.
*/DEFINE_PER_CPU_SHARED_ALIGNED(structtss_struct,init_tss)=INIT_TSS;

注意进程虽然都是动态分配的,但还是需要考虑最大进程数。在内核内最大进程数是由一个称为max_threads的符号表示的,它可以在 ./linux/kernel/fork.c 内找到。可以通过
/proc/sys/kernel/threads-max 的 proc 文件系统从用户空间更改此值。

Linux 内所有进程的分配有两种方式。第一种方式是通过一个哈希表,由PID 值进行哈希计算得到;第二种方式是通过双链循环表。循环表非常适合于对任务列表进行迭代。由于列表是循环的,没有头或尾;但是由于 init_task 总是存在,所以可以将其用作继续向前迭代的一个锚点。让我们来看一个遍历当前任务集的例子。任务列表无法从用户空间访问,但该问题很容易解决,方法是以模块形式向内核内插入代码。下面给出一个很简单的程序,它会迭代任务列表并会提供有关每个任务的少量信息(name、pid和 parent 名)。注意,在这里,此模块使用 printk 来发出结果。要查看具体的结果,可以通过 cat 实用工具(或实时的 tail -f/var/log/messages)查看 /var/log/messages 文件。next_task函数是 sched.h 内的一个宏,它简化了任务列表的迭代(返回下一个任务的 task_struct 引用)。

如下:

#definenext_task(p)\
list_entry_rcu((p)->tasks.next,structtask_struct,tasks)

查询任务列表信息的简单内核模块:

#include#include#includeintinit_module(void)
{/*Setuptheanchorpoint*/
structtask_struct*task=&init_task;/*Walkthroughthetasklist,untilwehittheinit_taskagain*/
do{
printk(KERN_INFO"===%s[%d]parent%s\n",
task->comm,task->pid,task->parent->comm);
}while((task=next_task(task))!=&init_task);
printk(KERN_INFO"Currenttaskis%s[%d]\n",current->comm,current->pid);return0;
}
voidcleanup_module(void)
{return;
}

编译此模块的Makefile文件如下:

obj-m+=procsview.o
KDIR:=/lib/modules/$(shelluname-r)/build
PWD:=$(shellpwd)
default:
$(MAKE)-C$(KDIR)SUBDIRS=$(PWD)modules

在编译后,可以用insmod procsview.ko 插入模块对象,也可以用 rmmod procsview 删除它。插入后,/var/log/messages可显示输出,如下所示。从中可以看到,这里有一个空闲任务(称为 swapper)和init 任务(pid 1)。

Dec 28 23:18:16 ubuntu kernel: [12128.910863]=== swapper [0] parent swapper
Dec 28 23:18:16 ubuntu kernel: [12128.910934]=== init [1] parent swapper
Dec 28 23:18:16 ubuntu kernel: [12128.910945]=== kthreadd [2] parent swapper
Dec 28 23:18:16 ubuntu kernel: [12128.910953]=== migration/0 [3] parent kthreadd
......
Dec 28 23:24:12 ubuntu kernel: [12485.295015]Current task is insmod [6051]

Linux 维护一个称为current的宏,标识当前正在运行的进程(类型是 task_struct)。模块尾部的那行prink用于输出当前进程的运行命令及进程号。注意到当前的任务是 insmod,这是因为 init_module 函数是在insmod 命令执行的上下文运行的。current 符号实际指的是一个函数(get_current),可在一个与 arch 有关的头部中找到它。比如
./linux/arch/x86/include/asm/current.h,

如下:

#include#include#ifndef__ASSEMBLY__structtask_struct;
DECLARE_PER_CPU(structtask_struct*,current_task);
static__always_inlinestructtask_struct*get_current(void){returnpercpu_read_stable(current_task);
}
#definecurrentget_current()
#endif/*__ASSEMBLY__*/
#endif/*_ASM_X86_CURRENT_H*/

三,进程创建

用户空间内可以通过执行一个程序、或者在程序内调用fork(或exec)系统调用来创建进程,fork调用会导致创建一个子进程,而exec调用则会用新程序代替当前进程上下文。一个新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个用户态函数均由libc库提供,它们分别会调用Linux内核提供的同名系统调用fork,vfork和clone。下面以fork系统调用为例来介绍。

传统的创建一个新进程的方式是子进程拷贝父进程所有资源,这无疑使得进程的创建效率低,因为子进程需要拷贝父进程的整个地址空间。更糟糕的是,如果子进程创建后又立马去执行exec族函数,那么刚刚才从父进程那里拷贝的地址空间又要被清除以便装入新的进程映像。为了解决这个问题,

内核中提供了上述三种不同的系统调用。

内核采用写时复制技术对传统的fork函数进行了下面的优化。即子进程创建后,父子以只读的方式共享父进程的资源(并不包括父进程的页表项)。当子进程需要修改进程地址空间的某一页时,才为子进程复制该页。采用这样的技术可以避免对父进程中某些数据不必要的复制。

使用vfork函数创建的子进程会完全共享父进程的地址空间,甚至是父进程的页表项。父子进程任意一方对任何数据的修改使得另一方都可以感知到。为了使得双方不受这种影响,vfork函数创建了子进程后,父进程便被阻塞直至子进程调用了exec()或exit()。由于现在fork函数引入了写时复制技术,在不考虑复制父进程页表项的情况下,vfork函数几乎不会被使用。

clone函数创建子进程时灵活度比较大,因为它可以通过传递不同的clone标志参数来选择性的复制父进程的资源。

大部分系统调用对应的例程都被命名为 sys_* 并提供某些初始功能以实现调用(例如错误检查或用户空间的行为),实际的工作常常会委派给另外一个名为 do_* 的函数。


./linux/include/asm-generic/unistd.h中记录了所有的系统调用号及名称。注意fork实现与体系结构相关,对32位的x86系统会使用./linux/arch/x86/include/asm/unistd_32.h中的定义,fork系统调用编号为2。

fork系统调用在unistd.h中的宏关联如下:

#define__NR_fork1079#ifdefCONFIG_MMU__SYSCALL(__NR_fork,sys_fork)#else__SYSCALL(__NR_fork,sys_ni_syscall)#endif

在unistd_32.h中的调用号关联为:

#define __NR_fork 2

在很多情况下,用户空间任务和内核任务的底层机制是一致的。系统调用fork、vfork和clone在内核中对应的服务例程分别为sys_fork(),sys_vfork()和sys_clone()。它们最终都会依赖于一个名为do_fork 的函数来创建新进程。例如在创建内核线程时,内核会调用一个名为 kernel_thread 的函数(对32位系统)参见
./linux/arch/x86/kernel/process_32.c,注意process.c是包含32/64bit都适用的代码,process_32.c是特定于32位架构,process_64.c是特定于64位架构),此函数执行某些初始化后会调用 do_fork。创建用户空间进程的情况与此类似。在用户空间,一个程序会调用fork,通过int $0x80之类的软中断会导致对名为sys_fork的内核函数的系统调用(参见./linux/arch/x86/kernel/process_32.c),如下:

intsys_fork(structpt_regs*regs){returndo_fork(SIGCHLD,regs->sp,regs,0,NULL,NULL);
}

最终都是直接调用do_fork。进程创建的函数层次结构如下图:

进程创建的函数层次结构

从图中可以看到 do_fork 是进程创建的基础。可以在 ./linux/kernel/fork.c 内找到 do_fork 函数(以及合作函数 copy_process)。

当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。在X86体系中,可以通过两种不同的方式进入系统调用:执行int $0×80汇编命令和执行sysenter汇编命令。后者是Intel在Pentium II中引入的指令,内核从2.6版本开始支持这条命令。这里将集中讨论以int $0×80方式进入系统调用的过程。

通过int $0×80方式调用系统调用实际上是用户进程产生一个中断向量号为0×80的软中断。当用户态fork()调用发生时,用户态进程会保存调用号以及参数,然后发出int $0×80指令,陷入0x80中断。CPU将从用户态切换到内核态并开始执行system_call()。这个函数是通过汇编命令来实现的,它是0×80号软中断对应的中断处理程序。对于所有系统调用来说,它们都必须先进入system_call(),也就是所谓的系统调用处理程序。再通过系统调用号跳转到具体的系统调用服务例程处。32位x86系统的系统调用处理程序在
./linux/arch/x86/kernel/entry_32.S中,代码如下:

.macroSAVE_ALL
cld
PUSH_GS
pushl%fs
CFI_ADJUST_CFA_OFFSET4
/*CFI_REL_OFFSETfs,0;*/
pushl%es
CFI_ADJUST_CFA_OFFSET4
/*CFI_REL_OFFSETes,0;*/
pushl%ds
CFI_ADJUST_CFA_OFFSET4
/*CFI_REL_OFFSETds,0;*/
pushl%eax
CFI_ADJUST_CFA_OFFSET4
CFI_REL_OFFSETeax,0
pushl%ebp
CFI_ADJUST_CFA_OFFSET4
CFI_REL_OFFSETebp,0
pushl%edi
CFI_ADJUST_CFA_OFFSET4
CFI_REL_OFFSETedi,0
pushl%esi
CFI_ADJUST_CFA_OFFSET4
CFI_REL_OFFSETesi,0
pushl%edx
CFI_ADJUST_CFA_OFFSET4
CFI_REL_OFFSETedx,0
pushl%ecx
CFI_ADJUST_CFA_OFFSET4
CFI_REL_OFFSETecx,0
pushl%ebx
CFI_ADJUST_CFA_OFFSET4
CFI_REL_OFFSETebx,0
movl$(__USER_DS),%edx
movl%edx,%ds
movl%edx,%es
movl$(__KERNEL_PERCPU),%edx
movl%edx,%fs
SET_KERNEL_GS%edx.endm/*...*/ENTRY(system_call)
RING0_INT_FRAME#无论如何不能进入用户空间
pushl%eax#将保存的系统调用编号压入栈中
CFI_ADJUST_CFA_OFFSET4
SAVE_ALL
GET_THREAD_INFO(%ebp)#检测进程是否被跟踪
testl$_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnzsyscall_trace_entry
cmpl$(nr_syscalls),%eax
jaesyscall_badsyssyscall_call:
call*sys_call_table(,%eax,4)#跳入对应服务例程
movl%eax,PT_EAX(%esp)#保存进程的返回值syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY)#不要忘了在中断返回前关闭中断
TRACE_IRQS_OFF
movlTI_flags(%ebp),%ecx
testl$_TIF_ALLWORK_MASK,%ecx#current->work
jnesyscall_exit_workrestore_all:
TRACE_IRQS_IRETrestore_all_notrace:
movlPT_EFLAGS(%esp),%eax#mixEFLAGS,SSandCS#Warning:PT_OLDSS(%esp)containsthewrong/randomvaluesifwe#arereturningtothekernel.#Seecommentsinprocess.c:copy_thread()fordetails.
movbPT_OLDSS(%esp),%ah
movbPT_CS(%esp),%al
andl$(X86_EFLAGS_VM|(SEGMENT_TI_MASK<<8)|SEGMENT_RPL_MASK),%eax
cmpl$((SEGMENT_LDT<<8)|USER_RPL),%eax
CFI_REMEMBER_STATE
jeldt_ss#returningtouser-spacewithLDTSSrestore_nocheck:
RESTORE_REGS4#skiporig_eax/error_code
CFI_ADJUST_CFA_OFFSET-4irq_return:
INTERRUPT_RETURN.section.fixup,"ax"

分析:

(1)在system_call函数执行之前,CPU控制单元已经将eflags、cs、eip、ss和esp寄存器的值自动保存到该进程对应的内核栈中。随之,在 system_call内部首先将存储在eax寄存器中的系统调用号压入栈中。接着执行SAVE_ALL宏。该宏在栈中保存接下来的系统调用可能要用到的所有CPU寄存器。

(2)通过GET_THREAD_INFO宏获得当前进程的thread_inof结构的地址;再检测当前进程是否被其他进程所跟踪(例如调试一个程序时,被调试的程序就处于被跟踪状态),也就是thread_info结构中flag字段的_TIF_ALLWORK_MASK被置1。如果发生被跟踪的情况则转向syscall_trace_entry标记的处理命令处。

(3)对用户态进程传递过来的系统调用号的合法性进行检查。如果不合法则跳入到syscall_badsys标记的命令处。

(4)如果系统调用好合法,则根据系统调用号查找
./linux/arch/x86/kernel/syscall_table_32.S中的系统调用表sys_call_table,找到相应的函数入口点,跳入sys_fork这个服务例程当中。由于 sys_call_table表的表项占4字节,因此获得服务例程指针的具体方法是将由eax保存的系统调用号乘以4再与sys_call_table表的基址相加。

syscall_table_32.S中的代码如下:

ENTRY(sys_call_table)
.longsys_restart_syscall/*0-old"setup()"systemcall,usedforrestarting*/
.longsys_exit
.longptregs_fork
.longsys_read
.longsys_write
.longsys_open/*5*/
.longsys_close/*...*/

sys_call_table是系统调用多路分解表,使用 eax 中提供的索引来确定要调用该表中的哪个系统调用。

(5)当系统调用服务例程结束时,从eax寄存器中获得当前进程的的返回值,并把这个返回值存放在曾保存用户态eax寄存器值的那个栈单元的位置上。这样,用户态进程就可以在eax寄存器中找到系统调用的返回码。

经过的调用链为fork()--->int$0×80软中断--->ENTRY(system_call)--->ENTRY(sys_call_table)--->sys_fork()--->do_fork()。实际上fork、vfork和clone三个系统调最终都是调用do_fork()。只不过在调用时所传递的参数有所不同,而参数的不同正好导致了子进程与父进程之间对资源的共享程度不同。因此,分析do_fork()成为我们的首要任务。在进入do_fork函数进行分析之前,很有必要了解一下它的参数。

clone_flags:该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合(本文所涉及的标志含义详见下表),也就是若干个标志之间的或运算。通过 clone标志可以有选择的对父进程的资源进行复制。

本文所涉及到的clone标志详见下表:

stack_start:

子进程用户态堆栈的地址。

regs:

指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中。

stack_size:

未被使用,通常被赋值为0。

parent_tidptr:

父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义。

child_tidptr:

子进程在用户态下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义。

do_fork函数在./linux/kernel/fork.c中,主要工作就是复制原来的进程成为另一个新的进程,它完成了整个进程创建中的大部分工作。

代码如下:

longdo_fork(unsignedlongclone_flags,unsignedlongstack_start,structpt_regs*regs,unsignedlongstack_size,int__user*parent_tidptr,int__user*child_tidptr)
{structtask_struct*p;inttrace=0;longnr;
/*
*做一些预先的参数和权限检查
*/
if(clone_flags&CLONE_NEWUSER){if(clone_flags&CLONE_THREAD)return-EINVAL;/*希望当用户名称被支持时,这里的检查可去掉
*/
if(!capable(CAP_SYS_ADMIN)||!capable(CAP_SETUID)||
!capable(CAP_SETGID))return-EPERM;
}
/*
*希望在2.6.26之后这些标志能实现循环
*/
if(unlikely(clone_flags&CLONE_STOPPED)){staticint__read_mostlycount=100;
if(count>0&&printk_ratelimit()){charcomm[TASK_COMM_LEN];
count--;
printk(KERN_INFO"fork():process`%s'useddeprecated"
"cloneflags0x%lx\n",
get_task_comm(comm,current),
clone_flags&CLONE_STOPPED);
}
}
/*
*当从kernel_thread调用本do_fork时,不使用跟踪
*/
if(likely(user_mode(regs)))/*如果从用户态进入本调用,则使用跟踪*/
trace=tracehook_prepare_clone(clone_flags);
p=copy_process(clone_flags,stack_start,regs,stack_size,
child_tidptr,NULL,trace);/*
*在唤醒新线程之前做下面的工作,因为新线程唤醒后本线程指针会变成无效(如果退出很快的话)
*/
if(!IS_ERR(p)){structcompletionvfork;
trace_sched_process_fork(current,p);
nr=task_pid_vnr(p);
if(clone_flags&CLONE_PARENT_SETTID)
put_user(nr,parent_tidptr);
if(clone_flags&CLONE_VFORK){
p->vfork_done=&vfork;
init_completion(&vfork);
}
audit_finish_fork(p);
tracehook_report_clone(regs,clone_flags,nr,p);
/*
*我们在创建时设置PF_STARTING,以防止跟踪进程想使用这个标志来区分一个完全活着的进程
*和一个还没有获得trackhook_report_clone()的进程。现在我们清除它并且设置子进程运行
*/
p->flags&=~PF_STARTING;
if(unlikely(clone_flags&CLONE_STOPPED)){/*
*我们将立刻启动一个即时的SIGSTOP
*/
sigaddset(&p->pending.signal,SIGSTOP);
set_tsk_thread_flag(p,TIF_SIGPENDING);
__set_task_state(p,TASK_STOPPED);
}else{
wake_up_new_task(p,clone_flags);
}
tracehook_report_clone_complete(trace,regs,
clone_flags,nr,p);
if(clone_flags&CLONE_VFORK){
freezer_do_not_count();
wait_for_completion(&vfork);
freezer_count();
tracehook_report_vfork_done(p,nr);
}
}else{
nr=PTR_ERR(p);
}returnnr;
}

(1)在一开始,该函数定义了一个task_struct类型的指针p,用来接收即将为新进程(子进程)所分配的进程描述符。trace表示跟踪状态,nr表示新进程的pid。接着做一些预先的参数和权限检查。

(2)接下来检查clone_flags是否设置了CLONE_STOPPED标志。如果设置了,则做相应处理,打印消息说明进程已过时。通常这样的情况很少发生,因此在判断时使用了unlikely修饰符。使用该修饰符的判断语句执行结果与普通判断语句相同,只不过在执行效率上有所不同。正如该单词的含义所表示的那样,当前进程很少为停止状态。因此,编译器尽量不会把if内的语句与当前语句之前的代码编译在一起,以增加cache的命中率。与此相反,likely修饰符则表示所修饰的代码很可能发生。tracehook_prepare_clone用于设置子进程是否被跟踪。所谓跟踪,最常见的例子就是处于调试状态下的进程被debugger进程所跟踪。进程的ptrace字段非0说明debugger程序正在跟踪它。如果调用是从用户态进来的(而不从kernel_thread进来的),且当前进程(父进程)被另外一个进程所跟踪,那么子进程也要设置为被跟踪,并且将跟踪标志CLONE_PTRACE加入标志变量clone_flags中。如果父进程不被跟踪,则子进程也不会被跟踪,设置好后返回trace。

(3)接下来的这条语句要做的是整个创建过程中最核心的工作:通过copy_process()创建子进程的描述符,分配pid,并创建子进程执行时所需的其他数据结构,最终则会返回这个创建好的进程描述符p。该函数中的参数意义与do_fork函数相同。注意原来内核中为子进程分配pid的工作是在do_fork中完成,现在新的内核已经移到copy_process中了。

(4)如果copy_process函数执行成功,那么将继续执行if(!IS_ERR(p))部分。首先定义了一个完成量vfork,用task_pid_vnr(p)从p中获取新进程的pid。如果clone_flags包含CLONE_VFORK标志,那么将进程描述符中的vfork_done字段指向这个完成量,之后再对vfork完成量进行初始化。完成量的作用是,直到任务A发出信号通知任务B发生了某个特定事件时,任务B才会开始执行,否则任务B一直等待。我们知道,如果使用vfork系统调用来创建子进程,那么必然是子进程先执行。究其原因就是此处vfork完成量所起到的作用。当子进程调用exec函数或退出时就向父进程发出信号,此时父进程才会被唤醒,否则一直等待。此处的代码只是对完成量进行初始化,具体的阻塞语句则在后面的代码中有所体现。

(5)如果子进程被跟踪或者设置了CLONE_STOPPED标志,那么通过sigaddset函数为子进程增加挂起信号,并将子进程的状态设置为TASK_STOPPED。signal对应一个unsignedlong类型的变量,该变量的每个位分别对应一种信号。具体的操作是将SIGSTOP信号所对应的那一位置1。如果子进程并未设置CLONE_STOPPED标志,那么通过wake_up_new_task将进程放到运行队列上,从而让调度器进行调度运行。wake_up_new_task()在./linux/kernel/sched.c中,用于唤醒第一次新创建的进程,它将为新进程做一些初始的必须的调度器统计操作,然后把进程放到运行队列中。一旦当然正在运行的进程时间片用完(通过时钟tick中断来控制),就会调用schedule(),从而进行进程调度。

代码如下:

voidwake_up_new_task(structtask_struct*p,unsignedlongclone_flags){unsignedlongflags;structrq*rq;
intcpu=get_cpu();
#ifdefCONFIG_SMP
rq=task_rq_lock(p,&flags);
p->state=TASK_WAKING;
/*
*Forkbalancing,doithereandnotearlierbecause:
*-cpus_allowedcanchangeintheforkpath
*-anypreviouslyselectedcpumightdisappearthroughhotplug
*
*WesetTASK_WAKINGsothatselect_task_rq()candroprq->lock
*withoutpeoplepokingat->cpus_allowed.
*/
cpu=select_task_rq(rq,p,SD_BALANCE_FORK,0);
set_task_cpu(p,cpu);
p->state=TASK_RUNNING;
task_rq_unlock(rq,&flags);#endif
rq=task_rq_lock(p,&flags);
update_rq_clock(rq);
activate_task(rq,p,0);
trace_sched_wakeup_new(rq,p,1);
check_preempt_curr(rq,p,WF_FORK);#ifdefCONFIG_SMP
if(p->sched_class->task_woken)
p->sched_class->task_woken(rq,p);#endif
task_rq_unlock(rq,&flags);
put_cpu();
}

这里先用get_cpu()获取CPU,如果是对称多处理系统(SMP),先设置我为TASK_WAKING状态,由于有多个CPU(每个CPU上都有一个运行队列),需要进行负载均衡,选择一个最佳CPU并设置我使用这个CPU,然后设置我为TASK_RUNNING状态。这段操作是互斥的,因此需要加锁。注意TASK_RUNNING并不表示进程一定正在运行,无论进程是否正在占用CPU,只要具备运行条件,都处于该状态。 Linux把处于该状态的所有PCB组织成一个可运行队列run_queue,调度程序从这个队列中选择进程运行。事实上,Linux是将就绪态和运行态合并为了一种状态。然后用
./linux/kernel/sched.c:activate_task()把当前进程插入到对应CPU的runqueue上,最终完成入队的函数是active_task()--->enqueue_task(),其中核心代码行为:p->sched_class->enqueue_task(rq, p,wakeup, head);sched_class在./linux/include/linux/sched.h中,是调度器一系列操作的面向对象抽象,这个类包括进程入队、出队、进程运行、进程切换等接口,用于完成对进程的调度运行。

(6)
tracehook_report_clone_complete函数用于在进程复制快要完成时报告跟踪情况。如果父进程被跟踪,则将子进程的pid赋值给父进程的进程描述符的pstrace_message字段,并向父进程的父进程发送SIGCHLD信号。

(7)如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。

(8)如果copy_process()在执行的时候发生错误,则先释放已分配的pid,再根据PTR_ERR()的返回值得到错误代码,保存于nr中。

四,copy_process: 进程描述符的处理

copy_process函数也在./linux/kernel/fork.c中。它会用当前进程的一个副本来创建新进程并分配pid,但不会实际启动这个新进程。它会复制寄存器中的值、所有与进程环境相关的部分,每个clone标志。新进程的实际启动由调用者来完成。

对于每一个进程而言,内核为其单独分配了一个内存区域,这个区域存储的是内核栈和该进程所对应的一个小型进程描述符thread_info结构。在
./linux/arch/x86/include/asm/thread_info.h中,如下:

structthread_info{
structtask_struct*task;/*主进程描述符*/
structexec_domain*exec_domain;/*执行域*/
__u32flags;/*低级别标志*/
__u32status;/*线程同步标志*/
__u32cpu;/*当前CPU*/
intpreempt_count;/*0=>可抢占,BUG*/
mm_segment_taddr_limit;structrestart_blockrestart_block;
void__user*sysenter_return;#ifdefCONFIG_X86_32
unsignedlongprevious_esp;/*先前栈的ESP,以防嵌入的(IRQ)栈*/
__u8supervisor_stack[0];#endif
intuaccess_err;
};
/*...*/
/*怎样从C获取当前栈指针*/registerunsignedlongcurrent_stack_pointerasm("esp")__used;
/*怎样从C获取当前线程信息结构*/staticinlinestructthread_info*current_thread_info(void){return(structthread_info*)
(current_stack_pointer&~(THREAD_SIZE-1));
}

之所以将线程信息结构称之为小型的进程描述符,是因为在这个结构中并没有直接包含与进程相关的字段,而是通过task字段指向具体某个进程描述符。通常这块内存区域的大小是8KB,也就是两个页的大小(有时候也使用一个页来存储,即4KB)。

一个进程的内核栈和thread_info结构之间的逻辑关系如下图所示:

进程内核栈和thread_info结构的存储

从上图可知,内核栈是从该内存区域的顶层向下(从高地址到低地址)增长的,而thread_info结构则是从该区域的开始处向上(从低地址到高地址)增长。内核栈的栈顶地址存储在esp寄存器中。所以,当进程从用户态切换到内核态后,esp寄存器指向这个区域的末端。从代码的角度来看,内核栈和thread_info结构是被定义在
./linux/include/linux/sched.h中的一个联合体当中的:

unionthread_union{structthread_infothread_info;
unsignedlongstack[THREAD_SIZE/sizeof(long)];
};

其中,THREAD_SIZE的值取8192时,stack数组的大小为2048;THREAD_SIZE的值取4096时,stack数组的大小为1024。

现在我们应该思考,为何要将内核栈和thread_info(其实也就相当于task_struct,只不过使用thread_info结构更节省空间)紧密的放在一起?最主要的原因就是内核可以很容易的通过esp寄存器的值获得当前正在运行进程的thread_info结构的地址,进而获得当前进程描述符的地址。在上面的current_thread_info函数中,定义current_stack_pointer的这条内联汇编语句会从esp寄存器中获取内核栈顶地址,和~(THREAD_SIZE - 1)做与操作将屏蔽掉低13位(或12位,当THREAD_SIZE为4096时),此时所指的地址就是这片内存区域的起始地址,也就刚好是thread_info结构的地址。但是,thread_info结构的地址并不会对我们直接有用。我们通常可以轻松的通过 current宏获得当前进程的task_struct结构,前面已经列出过get_current()函数的代码。current宏返回的是thread_info结构task字段,而task正好指向与thread_info结构关联的那个进程描述符。得到 current后,我们就可以获得当前正在运行进程的描述符中任何一个字段了,比如我们通常所做的current->pid。

下面看copy_process的实现:

staticstructtask_struct*copy_process(unsignedlongclone_flags,
unsignedlongstack_start,
structpt_regs*regs,
unsignedlongstack_size,
int__user*child_tidptr,
structpid*pid,
inttrace)
{
intretval;
structtask_struct*p;
intcgroup_callbacks_done=0;
if((clone_flags&(CLONE_NEWNS|CLONE_FS))==(CLONE_NEWNS|CLONE_FS))returnERR_PTR(-EINVAL);
/*
*Threadgroupsmustsharesignalsaswell,anddetachedthreads
*canonlybestartedupwithinthethreadgroup.
*/
if((clone_flags&CLONE_THREAD)&&!(clone_flags&CLONE_SIGHAND))returnERR_PTR(-EINVAL);
/*
*SharedsignalhandlersimplysharedVM.Bywayoftheabove,
*threadgroupsalsoimplysharedVM.Blockingthiscaseallows
*forvarioussimplificationsinothercode.
*/
if((clone_flags&CLONE_SIGHAND)&&!(clone_flags&CLONE_VM))returnERR_PTR(-EINVAL);
/*
*Siblingsofglobalinitremainaszombiesonexitsincetheyare
*notreapedbytheirparent(swapper).Tosolvethisandtoavoid
*multi-rootedprocesstrees,preventglobalandcontainer-inits
*fromcreatingsiblings.
*/
if((clone_flags&CLONE_PARENT)&&
current->signal->flags&SIGNAL_UNKILLABLE)returnERR_PTR(-EINVAL);
retval=security_task_create(clone_flags);if(retval)gotofork_out;
retval=-ENOMEM;
p=dup_task_struct(current);if(!p)gotofork_out;
ftrace_graph_init_task(p);
rt_mutex_init_task(p);
#ifdefCONFIG_PROVE_LOCKING
DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);#endif
retval=-EAGAIN;if(atomic_read(&p->real_cred->user->processes)>=
p->signal->rlim[RLIMIT_NPROC].rlim_cur){if(!capable(CAP_SYS_ADMIN)&&!capable(CAP_SYS_RESOURCE)&&
p->real_cred->user!=INIT_USER)gotobad_fork_free;
}
retval=copy_creds(p,clone_flags);if(retvalclear_child_tid()fromwritingtoauser-controlled
*kerneladdress.
*/
set_fs(USER_DS);
tracehook_report_exit(&code);
validate_creds_for_do_exit(tsk);
/*
*We'retakingrecursivefaultshereindo_exit.Safestistojust
*leavethistaskaloneandwaitforreboot.
*/
if(unlikely(tsk->flags&PF_EXITING)){
printk(KERN_ALERT"Fixingrecursivefaultbutrebootisneeded!\n");/*
*Wecandothisunlockedhere.Thefutexcodeuses
*thisflagjusttoverifywhetherthepistate
*cleanuphasbeendoneornot.Intheworstcaseit
*loopsoncemore.Wepretendthatthecleanupwas
*doneasthereisnowaytoreturn.Eitherthe
*OWNER_DIEDbitissetbynoworwepushtheblocked
*taskintothewaitforevernirwanaaswell.
*/
tsk->flags|=PF_EXITPIDONE;
set_current_state(TASK_UNINTERRUPTIBLE);
schedule();
}
exit_irq_thread();
exit_signals(tsk);/*setsPF_EXITING*/
/*
*tsk->flagsarecheckedinthefutexcodetoprotectagainst
*anexitingtaskcleaninguptherobustpifutexes.
*/
smp_mb();
spin_unlock_wait(&tsk->pi_lock);
if(unlikely(in_atomic()))
printk(KERN_INFO"note:%s[%d]exitedwithpreempt_count%d\n",
current->comm,task_pid_nr(current),
preempt_count());
acct_update_integrals(tsk);
group_dead=atomic_dec_and_test(&tsk->signal->live);if(group_dead){
hrtimer_cancel(&tsk->signal->real_timer);
exit_itimers(tsk->signal);if(tsk->mm)
setmax_mm_hiwater_rss(&tsk->signal->maxrss,tsk->mm);
}
acct_collect(code,group_dead);if(group_dead)
tty_audit_exit();if(unlikely(tsk->audit_context))
audit_free(tsk);
tsk->exit_code=code;
taskstats_exit(tsk,group_dead);
exit_mm(tsk);
if(group_dead)
acct_process();
trace_sched_process_exit(tsk);
exit_sem(tsk);
exit_files(tsk);
exit_fs(tsk);
check_stack_usage();
exit_thread();
cgroup_exit(tsk,1);
if(group_dead&&tsk->signal->leader)
disassociate_ctty(1);
module_put(task_thread_info(tsk)->exec_domain->module);
proc_exit_connector(tsk);
/*
*Flushinheritedcounterstotheparent-beforetheparent
*getswokenupbychild-exitnotifications.
*/
perf_event_exit_task(tsk);
exit_notify(tsk,group_dead);#ifdefCONFIG_NUMA
mpol_put(tsk->mempolicy);
tsk->mempolicy=NULL;#endif#ifdefCONFIG_FUTEX
if(unlikely(current->pi_state_cache))
kfree(current->pi_state_cache);#endif
/*
*Makesureweareholdingnolocks:
*/
debug_check_no_locks_held(tsk);/*
*Wecandothisunlockedhere.Thefutexcodeusesthisflag
*justtoverifywhetherthepistatecleanuphasbeendone
*ornot.Intheworstcaseitloopsoncemore.
*/
tsk->flags|=PF_EXITPIDONE;
if(tsk->io_context)
exit_io_context();
if(tsk->splice_pipe)
__free_pipe_info(tsk->splice_pipe);
validate_creds_for_do_exit(tsk);
preempt_disable();
exit_rcu();/*causesfinalput_task_structinfinish_task_switch().*/
tsk->state=TASK_DEAD;
schedule();
BUG();/*Avoid"noreturnfunctiondoesreturn".*/
for(;;)
cpu_relax();/*ForwhenBUGisnull*/}
EXPORT_SYMBOL_GPL(do_exit);

(1)为进程销毁做一系列准备。用set_fs设置USER_DS。注意如果do_exit是因为当前进程出现不可预知的错误而被调用,这时get_fs()有可能得到的仍然是KERNEL_DS状态,因此我们要重置它为USER_DS状态。还有一个可能原因是这可以防止mm_release()->clear_child_tid()写一个被用户控制的内核地址。

(2)清除所有信号处理函数。exit_signals函数会设置PF_EXITING标志来表明进程正在退出,并清除所有信息处理函数。内核的其他方面会利用PF_EXITING来防止在进程被删除时还试图处理此进程。

(3)清除一系列的进程资源。比如比如 exit_mm删除内存页、exit_files关闭所有打开的文件描述符,这会清理I/O缓存,如果缓存中有数据,就会将它们写入相应的文件,以防止文件数据的丢失。exit_fs清除当前目录关联的inode、exit_thread清除线程信息、等等。

(4)发出退出通知。调用exit_notify执行一系列通知。例如通知父进程我正在退出。

如下:

staticvoidexit_notify(structtask_struct*tsk,intgroup_dead)
{
intsignal;
void*cookie;
/*
*Thisdoestwothings:
*
*A.Makeinitinheritallthechildprocesses
*B.Checktoseeifanyprocessgroupshavebecomeorphaned
*asaresultofourexiting,andiftheyhaveanystopped
*jobs,sendthemaSIGHUPandthenaSIGCONT.(POSIX3.2.2.2)
*/
forget_original_parent(tsk);
exit_task_namespaces(tsk);
write_lock_irq(&tasklist_lock);if(group_dead)
kill_orphaned_pgrp(tsk->group_leader,NULL);
/*Letfatherknowwedied
*
*Threadsignalsareconfigurable,butyouaren'tgoingtouse
*thattosendsignalstoarbitaryprocesses.
*Thatstopsrightnow.
*
*Iftheparentexeciddoesn'tmatchtheexecidwesaved
*whenwestartedthenweknowtheparenthaschangedsecurity
*domain.
*
*Ifourself_execiddoesn'tmatchourparent_exec_idthen
*wehavechangedexecutiondomainasthesetwovaluesstarted
*thesameafterafork.
*/
if(tsk->exit_signal!=SIGCHLD&&!task_detached(tsk)&&
(tsk->parent_exec_id!=tsk->real_parent->self_exec_id||
tsk->self_exec_id!=tsk->parent_exec_id))
tsk->exit_signal=SIGCHLD;
signal=tracehook_notify_death(tsk,&cookie,group_dead);if(signal>=0)
signal=do_notify_parent(tsk,signal);
tsk->exit_state=signal==DEATH_REAP?EXIT_DEAD:EXIT_ZOMBIE;
/*mt-exec,de_thread()iswaitingforus*/
if(thread_group_leader(tsk)&&
tsk->signal->group_exit_task&&
tsk->signal->notify_countsignal->group_exit_task);
write_unlock_irq(&tasklist_lock);
tracehook_report_death(tsk,signal,cookie,group_dead);
/*Iftheprocessisdead,releaseit-nobodywillwaitforit*/
if(signal==DEATH_REAP)
release_task(tsk);
}

exit_notify将当前进程的所有子进程的父进程ID设置为1(init),让init接管所有这些子进程。如果当前进程是某个进程组的组长,其销毁导致进程组变为“无领导状态“,则向每个组内进程发送挂起信号SIGHUP,然后发送SIGCONT。这是遵循POSIX3.2.2.2标准。接着向自己的父进程发送SIGCHLD信号,然后调用do_notify_parent通知父进程。若返回DEATH_REAP(这个意思是不管是否有其他进程关心本进程的退出信息,自动完成进程退出和PCB销毁),就直接进入EXIT_DEAD状态,如果不是,则就需要变为EXIT_ZOMBIE状态。

注意在最初父进程创建子进程时,如果调用了waitpid()等待子进程结束(表示它关心子进程的状态),子进程结束时父进程会处理它发来的SIGCHILD信号。如果不调用wait(表示它不关心子进程的死活状态),则不会处理子进程的SIGCHILD信号。参看
./linux/kernel/signal.c:do_notify_parent(),代码如下:

intdo_notify_parent(structtask_struct*tsk,intsig)
{structsiginfoinfo;
unsignedlongflags;structsighand_struct*psig;
intret=sig;
BUG_ON(sig==-1);
/*do_notify_parent_cldstopshouldhavebeencalledinstead.*/
BUG_ON(task_is_stopped_or_traced(tsk));
BUG_ON(!task_ptrace(tsk)&&
(tsk->group_leader!=tsk||!thread_group_empty(tsk)));
info.si_signo=sig;
info.si_errno=0;/*
*weareundertasklist_lockheresoourparentistiedto
*usandcannotexitandreleaseitsnamespace.
*
*theonlyitcanistoswitchitsnsproxywithsys_unshare,
*buuncharingpidnamespacesisnotallowed,sowe'llalways
*seerelevantnamespace
*
*write_lock()currentlycallspreempt_disable()whichisthe
*sameasrcu_read_lock(),butaccordingtoOleg,thisisnot
*correcttorelyonthis
*/
rcu_read_lock();
info.si_pid=task_pid_nr_ns(tsk,tsk->parent->nsproxy->pid_ns);
info.si_uid=__task_cred(tsk)->uid;
rcu_read_unlock();
info.si_utime=cputime_to_clock_t(cputime_add(tsk->utime,
tsk->signal->utime));
info.si_stime=cputime_to_clock_t(cputime_add(tsk->stime,
tsk->signal->stime));
info.si_status=tsk->exit_code&0x7f;if(tsk->exit_code&0x80)
info.si_code=CLD_DUMPED;elseif(tsk->exit_code&0x7f)
info.si_code=CLD_KILLED;else{
info.si_code=CLD_EXITED;
info.si_status=tsk->exit_code>>8;
}
psig=tsk->parent->sighand;
spin_lock_irqsave(&psig->siglock,flags);if(!task_ptrace(tsk)&&sig==SIGCHLD&&
(psig->action[SIGCHLD-1].sa.sa_handler==SIG_IGN||
(psig->action[SIGCHLD-1].sa.sa_flags&SA_NOCLDWAIT))){/*
*Weareexitingandourparentdoesn'tcare.POSIX.1
*definesspecialsemanticsforsettingSIGCHLDtoSIG_IGN
*orsettingtheSA_NOCLDWAITflag:weshouldbereaped
*automaticallyandnotleftforourparent'swait4call.
*Ratherthanhavingtheparentdoitasamagickindof
*signalhandler,wejustsetthistotelldo_exitthatwe
*canbecleanedupwithoutbecomingazombie.Notethat
*westillcall__wake_up_parentinthiscase,becausea
*blockedsys_wait4mightnowreturn-ECHILD.
*
*WhetherwesendSIGCHLDornotforSA_NOCLDWAIT
*isimplementation-defined:wedo(ifyoudon'twant
*it,justuseSIG_IGNinstead).
*/
ret=tsk->exit_signal=-1;if(psig->action[SIGCHLD-1].sa.sa_handler==SIG_IGN)
sig=-1;
}if(valid_signal(sig)&&sig>0)
__group_send_sig_info(sig,&info,tsk->parent);
__wake_up_parent(tsk,tsk->parent);
spin_unlock_irqrestore(&psig->siglock,flags);
returnret;
}

我们可以看到,如果父进程显示指定对子进程的SIGCHLD信号处理为SIG_IGN,或者标志为SA_NOCLDWAIT,则返回的是ret=-1,即DEATH_REAP(这个宏在
./linux/include/tracehook.h中定义为-1),这时在exit_notify中子进程马上变为EXIT_DEAD,表示我已退出并且死亡,最后被后面的release_task回收,将不会再有进程等待我。否则返回值与传入的信号值相同,子进程变成EXIT_ZOMBIE,表示已退出但还没死。不管有没有处理SIGCHLD,do_notify_parent最后都会用__wake_up_parent来唤醒正在等待的父进程。

可见子进程在结束前不一定都需要经过一个EXIT_ZOMBIE过程。如果父进程调用了waitpid等待子进程,则会显示处理它发来的SIGCHILD信号,子进程结束时会自我清理(在do_exit中自己用release_task清理);如果父进程没有调用waitpid等待子进程,则不会处理SIGCHLD信号,子进程不会马上被清理,而是变成EXIT_ZOMBIE状态,成为著名的僵尸进程。还有一种特殊情形,如果子进程退出时父进程恰好正在睡眠(sleep),导致没来得急处理SIGCHLD,子进程也会成为僵尸,只要父进程在醒来后能调用waitpid,也能清理僵尸子进程,因为wait系统调用内部有清理僵尸子进程的代码。因此,如果父进程一直没有调用waitpid,那么僵尸子进程就只能等到父进程退出时被init接管了。init进程会负责清理这些僵尸进程(init肯定会调用wait)。

我们可以写个简单的程序来验证,父进程创建10个子进程,子进程sleep一段时间后退出。第一种情况,父进程只对1~9号子进程调用waitpid(),1~9号子进程都正常结束,而在父进程结束前,pids[0]为EXIT_ZOMBIE。第二种情况,父进程创建10个子进程后,sleep()一段时间,在这段时间内_exit()的子进程都成为EXIT_ZOMBIE。父进程sleep()结束后,依次调用waitpid(),子进程马上变为EXIT_DEAD被清理。

为了更好地理解怎么清理僵尸进程,我们简要地分析一下wait系统调用。wait族的系统调用如waitpid,wait4等,最后都会进入
./linux/kernel/exit.c:do_wait()内核例程,而后函数链为do_wait()--->do_wait_thread()--->wait_consider_task(),在这里,如果子进程在exit_notify中设置的tsk->exit_state为EXIT_DEAD,就返回0,即wait系统调用返回,说明子进程不是僵尸进程,会自己用release_task进行回收。如果它的exit_state是EXIT_ZOMBIE,进入wait_task_zombie()。在这里使用xchg尝试把它的exit_state设置为EXIT_DEAD,可见父进程的wait4调用会把子进程由EXIT_ZOMBIE设置为EXIT_DEAD。最后wait_task_zombie()在末尾调用release_task()清理这个僵尸进程。

(5)设置销毁标志并调度新的进程。在do_exit的最后,用exit_io_context清除IO上下文、preempt_disable禁用抢占,设置进程状态为TASK_DEAD,然后调用
./linux/kernel/sched.c:schedule()来选择一个将要执行的新进程。注意进程在退出并回收之后,其位于调度器的进程列表中的进程描述符(PCB)并没有立即释放,必须在设置task_struct的state为TASK_DEAD之后,由schedule()中的finish_task_switch()--->put_task_struct()把它的PCB重新放回到freelist(可用列表)中,这时PCB才算释放,然后切换到新的进程。

七,exit与_exit的差异

为了理解这两个系统调用的差异,先来讨论文件内存缓存区的问题。 在linux中,标准输入输出(I/O)函数都是作为文件来处理。对应于打开的每个文件,在内存中都有对应的缓存,每次读取文件时,会多读一些记录到缓存中,这样在下次读文件时,就在缓存中读取;同样,在写文件时也是写在文件对应的缓存中,并不是直接写入硬盘的文件中,等满足了一定条件(如达到一定数量,遇到换行符\n或文件结束标志EOF)才将数据真正的写入文件。这样做的好处就是加快了文件读写的速度。但这样也带来了一些问题,比如有一些数据,我们认为已经写入了文件,但实际上没有满足一定条件而任然驻留在内存的缓存中,这样,如果我们直接用_exit()函数直接终止进程,将导致数据丢失。如果改成exit,就不会有数据丢失的问题出现了,这就是它们之间的区别了.要解释这个问题,就要涉及它们的工作步骤了。

exit():通过前面源代码分析可知,在执行该函数时,进程会检查文件打开情况,清理I/O缓存,如果缓存中有数据,就会将它们写入相应的文件,这样就防止了文件数据的丢失,然后终止进程。

_exit():在执行该函数时,并不清理标准输入输出缓存,而是直接清除内存空间,当然也就把文件缓存中尚未写入文件的数据给销毁了。由此可见,使用exit()函数更加安全。

此外,对于它们两者的区别还有各自的头文件不同。exit()在stdlib.h中,_exit()在unistd.h中。一般情况下exit(0)表示正常退出,exit(1),exit(-1)为异常退出,0、1、-1是返回值,具体含义可以自定。还要注意return是返回函数调用,如果返回的是main函数,则为退出程序 。exit是在调用处强行退出程序,运行一次程序就结束。

下面是完整的Linux进程运行流程:

arch/x86/include/asm/unistd_32.h:fork()用户空间来调用(如C程序)
--->int$0×80产生0x80软中断
--->arch/x86/kernel/entry_32.S:ENTRY(system_call)中断处理程序system_call()
--->执行SAVE_ALL宏保存所有CPU寄存器值
--->arch/x86/kernel/syscall_table_32.S:ENTRY(sys_call_table)系统调用多路分解表
--->arch/x86/kernel/process_32.c:sys_fork()
--->kernel/fork.c:do_fork()复制原来的进程成为另一个新的进程
--->kernel/fork.c:copy_process()
--->structtask_struct*p;定义新的进程描述符(PCB)
--->clone_flags标志的合法性检查
--->security_task_create()安全性检查(SELinux机制)
--->kernel/fork.c:dup_task_struct()复制进程描述符
--->structthread_info*ti;定义线程信息结构
--->alloc_task_struct()为新的PCB分配内存
--->kernel/fork.c:arch_dup_task_struct()复制父进程的PCB
--->atomic_set(&tsk->usage,2)将PCB使用计数器设置为2,表示活动状态
--->copy_creds()复制权限及身份信息
--->检测进程总数是否超过max_threads
--->初始化PCB中各个字段
--->sched_fork()调度器相关设置
--->复制进程所有信息copy_semundo(),copy_files(),
--->copy_signal(),copy_mm()
--->copy_thread()复制线程
--->alloc_pid()分配pid
--->更新属性和进程数量计数
--->kernel/sched.c:wake_up_new_task()把进程放到运行队列上,让调度器进行调度
--->kernel/sched.c:select_task_rq()选择最佳的CPU(SMP中有多个CPU)
--->p->state=TASK_RUNNING设置成TASK_RUNNING状态
--->activate_task()
--->enqueue_task()把当前进程插入到对应CPU的runqueue上
--->有CLONE_VFORK标志:wait_for_completion()让父进程阻塞,等待子进程结束
--->返回分配的pid
kernel/sched.c:schedule()调度新创建的进程
进程运行中
exit()用户空间来调用(如C程序)
--->0x80中断跳转到include/linux/syscalls.h:sys_exit()
--->kernel/exit.c:do_exit()负责进程的退出
--->structtask_struct*tsk=current;获取我的PCB
--->set_fs(USER_DS)设置使用的文件系统模式
--->exit_signals()清除信号处理函数并设置PF_EXITING标志
--->清除进程一系列资源exit_mm(),exit_files()
--->exit_fs(),exit_thread()
--->kernel/exit.c:exit_notify()退出通知
--->forget_original_parent()把我的所有子进程过继给init进程
--->kill_orphaned_pgrp()向进程组内各进程发送挂起信号SIGHUP及SIGCONT
--->tsk->exit_signal=SIGCHLD;向我的父进程发送SIGCHLD信号
--->kernel/exit.c:do_notify_parent()通知父进程
--->如果父进程处理SIGCHLD信号,返回DEATH_REAP
--->如果父进程不处理SIGCHLD信号,返回传入时的信号值
--->__wake_up_parent()唤醒父进程
--->通知返回DEATH_REAP,设置exit_state为EXIT_DEAD我退出并且死亡
--->否则设置我为EXIT_ZOMBIE我退出但没死亡,成为僵尸进程
--->如果为DEATH_REAP:release_task()我自己清理相关资源
--->如果为僵尸,在我的父进程退出时我会过继给init进程,由init负责清理
--->exit_io_context()清理IO上下文
--->preempt_disable()禁用抢占
--->tsk->state=TASK_DEAD;设置我为进程死亡状态
--->kernel/sched.c:schedule()释放我的PCB,调度另一个新的进程
清理僵尸进程:wait系统调用等待子进程结束
--->0x80中断最后到达kernel/exit.c:do_wait()
--->do_wait_thread()
--->wait_consider_task()
--->如果子进程为EXIT_DEAD,返回0,wait调用返回,子进程自己清理自己
--->如果子进程为EXIT_ZOMBIE:wait_task_zombie()
--->xchg()设置僵尸子进程为EXIT_DEAD
--->release_task()清理僵尸子进程

下面是基本的执行流程图:

相关文章