2.2. 创建和中止任务与内核线程
不同的操作系统书籍,从一个“正在执行的程序的实例”到“由clone或者fork系统调用产生的任务”等不同方式定义了“进程”。在linux下,共有三种类型程序:
空线程;
内核线程;
用户任务;
空线程在为第一个CPU引导时创建,然后依靠定义在arch/i386/kernel/smpboot.c的fork_by_hand()函数手工为每个CPU创建这个线程。所有的空线程共享一个init_task结构,但都拥有各自的存放在CPU队列里的init_tss表示的TSS结构。他们以CLONE_PID方式clone,PID都为零,其他任务都不能共享这个PID。
内核模式下,kernel_thread函数调用clone系统调用创建了内核线程。内核线程通常没有用户地址空间,也就是p->;mm=NULL,因为他们明确通过daemonize()函数执行exit_mm()函数。内核线程通常可以直接操作内核地址空间,并被分配低范围的pid号。当在处理器模式下运行时意味着内核线程将享用所有的I/O特权并不能被调用程序预清空。
用户任务通过clone或者fork系统调用创建,他们都在内部调用了kernel/fork.c的do_fork()函数。
让我们分析一下当用户进程调用fork系统调用时发生了什么。虽然fork操作在传递用户堆栈和寄存器时依赖于体系架构,但在下面真实执行这个操作的do_fork()函数确实简洁的,并位于kernel/fork.c文件。
以下步骤将被执行:
1) 本地变量被设置为-ENOMEM,当fork创建一个新任务结构失败时将作为错误代码返回。
2) .如果CLONE_PID标识被设置,则返回-EPERM错误,除非调用者是空线程。普通用户线程clone时不能传递CLONE_PID标识并期待操作成功。SIFCHLDclone标识,对于fork来说,它被认为是不相关的,仅在sys_clone调用do_fork时才被认为是相关的。
3) 初始化current->;vfork_sem。它将在sys_vfork函数为了休眠父进程直到子进程执行mm_release函数时使用,就像执行其他程序或者中止其他程序一样。
4) 通过alloc_task_struct()宏分配一个新的任务结构。在x86系统上,它仅是一个GFP_KERNEL优先级的gfp。这酒是为何fork系统调用可能休眠的第一个原因。如果分配失败,返回-ENOMEM错误。
5) 通过结构拷贝*p = *current,将所有当前进程结构的数据都拷贝到新进程,或许这个操作应该被memcpy替换。然后,所有不能被子进程修改的字段将被设置为正确的值。
6) 大范围的内核锁被采用以防止其他部分执行本段代码。
7) 如果父进程拥有用户资源则校验是否超出了RLIMIT_NPROC限制。如果是这样,则返回-EAGAIN错误;如果没有,则通过指定的uid将计数器p->;user->;count进程数刷新。
如果系统所有的任务数目超过了最大线程数,返回-EAGAIN错误。
9) 如果进程是依赖预模块执行的,则增加依赖模块的引用计数。
10) 如果进程是依赖预模块二进制格式的,也增加依赖模块的引用计数。
11) 子进程被标识为“没有被执行”(p->;did_exec=0)。
12) 子进程被标识为'not.swappable' (p->;swappable = 0)。
13) 子进程被置为TASK_UNINTERRUPTIBLE状态,即p->;state = TASK_UNINTERRUPTIBLE。
14) 依照clone_flags的数值设置子进程的p->;flags,如果是简单fork,p->;flags= PF_FORKNOEXEC。
15) 通过快速算法kernel/fork.c的get_pid()函数设置子进程号p->;pid。
16) 初始化子进程其他任务结构。最后子进程结构被插入到pidhash表中,并且被唤醒。
这样任务就被创建了。停止任务有很多方式。
1) 通过exit系统调用;
2) 收到一个中止信号;
3) 被确定异常强制中止;
4) 以func == 1参数调用bdflush。
系统调用的实现函数都有sys_前缀,当他们通常仅与参数检测或者以细节方式传递信息,真正的操作是由do_**函数完成的。所以sys_exit()函数调用了do_exit来完成操作。尽管如此,内核其他部分有时也通过调用sys_exit实现堆do_exit的调用。
do_exit函数定义在kernel/exit.c中,按照以下几个步骤执行:
获取内核全局锁;
在最后调用一直循环的schedule()函数;
设置任务状态为TASK_ZOMBIE;
以current->;pdeath_signa信号通知所有的子进程;
以等同于SIGCHLD的信号current.>;exit_signal通知父进程;
释放fork函数分配的资源,关闭已经打开的文件;
在采用少量FPU切换的体系中,不管硬件设备要求什么都向FPU的所有者传递一个“none”;