Next Previous Contents
--------------------------------------------------------------------------------
2. 进程和中断管理
2.1 任务结构和进程表
Linux下每一个进程动态分配一个struct task_struct结构。可以建立的最大进程数只是由当前的物理内存数量所限制(参见 kernel/fork.c:fork_init()):
--------------------------------------------------------------------------------
/*
* The default maximum number of threads is set to a safe
* value: the thread structures can take up at most half
* of memory.
*/
max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2;
--------------------------------------------------------------------------------
在IA32体系结构中, 它基本上等于num_physpages/4. 例如,在一台具有512M的机器上,你可以建立32k个线程。对于老的核心(2.2和更早的版本)4k多一点的限制来说,这是一个很大的进步。而且,这还可以在运行时用KERN_MAX_THREADS sysctl(2)改变或是简单的通过procfs接口来调整:
--------------------------------------------------------------------------------
# cat /proc/sys/kernel/threads-max
32764
# echo 100000 > /proc/sys/kernel/threads-max
# cat /proc/sys/kernel/threads-max
100000
# gdb -q vmlinux /proc/kcore
Core was generated by `BOOT_IMAGE=240ac18 ro root=306 video=matrox:vesa:0x118'.
#0 0x0 in ?? ()
(gdb) p max_threads
$1 = 100000
--------------------------------------------------------------------------------
Linux系统的一组进程是通过许多的struct task_struct结构来表示的, 它们通过两种方法来链接在一起:
作为一个哈希表, 通过pid来建立
作为一个圆环,用p->next_task 和 p->prev_task指针建立的一个双向链表。
这个哈希表称为 pidhash[],在include/linux/sched.h中定义:
--------------------------------------------------------------------------------
/* PID hashing. (shouldnt this be dynamic?) */
#define PIDHASH_SZ (4096 >> 2)
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
--------------------------------------------------------------------------------
任务通过它们的pid值来建立哈希表,上面的哈希函数可以在它们的范围中(0 to PID_MAX-1)均一的分配元素。哈希表用include/linux/sched.h中的find_task_pid() 内联函数,可以快速查找一个给定pid的任务:
--------------------------------------------------------------------------------
static inline struct task_struct *find_task_by_pid(int pid)
{
struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)];
for(p = *htable; p && p->pid != pid; p = p->pidhash_next)
;
return p;
}
--------------------------------------------------------------------------------
每个哈希列表中的任务(即散列成同样的值)是通过p->pidhash_next/pidhash_pprev 来链接的,hash_pid() 和 unhash_pid() 使用它们来插入和删除一个哈希表中指定的进程 。这些都是在被称为tasklist_lock的读写自旋锁的保护下完成的。
圆环双向链表使用p->next_task/prev_task 来进行维护,因此可以在系统中容易地遍历所有任务。这是通过include/linux/sched.h中的宏for_each_task() 来完成的:
--------------------------------------------------------------------------------
#define for_each_task(p)
for (p = &init_task ; (p = p->next_task) != &init_task ; )
--------------------------------------------------------------------------------
for_each_task()使用者应该采用tasklist_lock来读。注意for_each_task() 是用init_task来标记列表的开始和结束,这是安全的方法,因为空闲任务(pid=0)从来不存在。
进程哈希表或进程表链的修改,特别是 fork(), exit() 和 ptrace(), 必须使用 tasklist_lock 来写。更有趣的是写时还必须在本地CPU上关闭中断。原因很简单:send_sigio() 函数遍历任务列表从而采用tasklist_lock 来读,在中断上下文中,它还被 kill_fasync() 调用。这就是为什么要在写时禁止中断的原因了,而读取时却不需要。
现在我们已明白 task_struct 结构是如何相互链接在一起,让我们检查一下 task_struct的成员。它们松散地对应着 UNIX 'proc' 和 'user' 结构的组合。
UNIX 的其它版本将任务状态信息分为两部分,一部分内容一直保留在内存(称为 'proc structure' ,它包括进程状态,调度信息等等) ;另外一部分是只有当进程运行时才需要的(称为'u 区' ,包括文件描述符表,磁盘限额信息等等)。这么丑陋的设计唯一的原因就是内存时非常紧缺的资源。现代操作系统(当然,目前只有Linux而不是其它操作系统,比如FreeBSD似乎在这方面朝着Linux提高)不需要这样区分,从而在任何时候都是在核心内存驻留数据结构中维护进程状态。
task_struct 结构申明在include/linux/sched.h ,目前的大小为1680字节。
状态域申明为:
--------------------------------------------------------------------------------
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
#define TASK_EXCLUSIVE 32
--------------------------------------------------------------------------------
为什么TASK_EXCLUSIVE 定义为32而不是16?因为16已经被TASK_SWAPPING 使用,当我已经删除了TASK_SWAPPING (有时是在2.3.x中)所有的引用后,却忘记了将TASK_EXCLUSIVE 提前。
p->state 中的volatile 申明意味着它可以异步地修改(从中断处理器中):
TASK_RUNNING: 意味着此任务被假定是在运行队列。它可能还不在运行队列中的原因是标记一个任务为TASK_RUNNING和将它放置在运行队列并不是原子性的。为了查看运行队列,你需要持有runqueue_lock 读写自旋锁进行读操作。如果你那样做的话,你将看见任务队列中的每一个任务都处于TASK_RUNNING 状态。然而,反之却未必正确,原因上面已经解释了。同样地,驱动程序(准确地说是它们所运行的进程上下文)可以标记它们自己为TASK_INTERRUPTIBLE (或 TASK_UNINTERRUPTIBLE)然后调用schedule(),这样将会把它从运行队列中删除掉(除非有一个未决的信号,这样它会留在运行队列)。
TASK_INTERRUPTIBLE: 意味着进程正在睡眠,但可以通过一个信号或定时器超时来唤醒。
TASK_UNINTERRUPTIBLE: 跟TASK_INTERRUPTIBLE一样,但它不能被唤醒。
TASK_ZOMBIE: 任务已经终止,但它的状态还没有被父进程(本身的或是收养的)收集(通过wait()调用)
TASK_STOPPED: 任务停止,要么是由于任务控制信号或是因为调用ptrace(2)。
TASK_EXCLUSIVE: 这不是一个单独的状态,它可以与TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE中的任一个相或。这意味着此任务与其它许多任务睡眠在等待队列中时,它将被单独唤醒而不是引起一个"雷鸣般的牧群"问题,唤醒队列中的所有任务。
任务标记包含不是互斥的进程状态信息:
--------------------------------------------------------------------------------
unsigned long flags; /* per process flags, defined below */
/*
* Per process flags
*/
#define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */
/* Not implemented yet, only for 486*/
#define PF_STARTING 0x00000002 /* being created */
#define PF_EXITING 0x00000004 /* getting shut down */
#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */
#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */
#define PF_DUMPCORE 0x00000200 /* dumped core */
#define PF_SIGNALED 0x00000400 /* killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_VFORK 0x00001000 /* Wake up parent in mm_release */
#define PF_USEDFPU 0x00100000 /* task used FPU this quantum (SMP) */
--------------------------------------------------------------------------------
p->has_cpu, p->processor, p->counter, p->priority, p->policy 和 p->rt_priority 域与调度器相关,后面我们将会看到。
域p->mm 和 p->active_mm 分别指向进程的地址空间和活动地址空间(如果没有一个真正的进程,如核心线程),它是由mm_struct 结构描述。 这在当任务被调度出去交换地址空间时可以最小化TLB表。因此,如果我们正调度进核心线程(它没有p->mm) ,于是它的next->active_mm将被设置成调度出去的任务的prev->active_mm ,如果prev->mm != NULL,它将与prev->mm一致。如果CLONE_VM标记被传给clone(2) 系统调用或vfork(2)系统调用,那么地址空间就可以在线程之间共享。
域p->exec_domain 和 p->personality跟任务的特征相关,比如说为了仿真UNIX的外来有趣"特征"的某些系统调用行为方法。
域p->fs 包含文件系统信息,Linux下代表三种信息:
根目录项和安装点,
预备根目录项和安装点,
当前工作目录项和安装点。
这个结构还包括引用记数,因为当CLONE_FS标记传送给clone(2)系统调用时它要在克隆的任务之间共享。
域p->files 包含文件描述符表。这也可以在任务之间共享,只要在clone(2)系统调用中指定了CLONE_FILES标记。
域p->sig包含信号处理器,可以通过CLONE_SIGHAND在克隆的任务之间共享。
2.2 任务和核心线程的建立和终止
操作系统类不同的书定义"进程"有不同的方法,但都是以"程序执行的事例"开始,以"通过clone(2)或fork(2)系统调用来产生"结束。在Linux下,有3种进程:
空闲线程,
核心线程,
用户任务。
空闲线程是在编译时为第一个CPU建立的;然后通过特定体系结构arch/i386/kernel/smpboot.c中的fork_by_hand()(在某些体系结构中它是fork(2)系统调用的手工展开)来为每一个CPU“手工的”建立一个。空闲任务有一个私有的TSS结构,它们在每个CPU数组init_tss中,但共享一个init_task结构。 所有空闲任务的pid = 0 ,没有其它的任务能够共享此pid,即使在clone(2)时使用了CLONE_PID。
核心线程是在核心模式下使用kernel_thread()函数执行clone(2)系统调用来建立的。核心线程通常没有用户地址空间,即p->mm = NULL,因为它们显式地调用exit_mm(),如通过daemonize()函数。核心线程总是直接地访问核心地址空间。它们在很低的区间分配pid号。运行在处理器的第0环意味着核心线程享有所有的I/O特权,并且不能够被调度器抢占。
用户任务是通过clone(2) 或 fork(2) 系统调用来建立的,它们内部都是执行kernel/fork.c:do_fork()。
让我们看看当一个用户进程运行fork(2)系统调用时会发生什么。尽管fork(2)是与体系结构相关的,因为传递给用户栈和寄存器有不同方式,但真正基本的函数do_fork()完成那些工作并且是可移植的。它位于kernel/fork.c。
它完成下面的步骤:
局部变量retval设置成-ENOMEM,如果fork(2)未能分配一个新的任务结构的话errno应该设置成这个值。
如果在clone_flags中设置了CLONE_PID,就返回错误(-EPERM), 除非调用者是空闲线程(仅在启动时)。于是,普通用户线程不能传递CLONE_PID 给clone(2) 并期望能成功。对于fork(2)来说,由于clone_flags 被设置成 SIFCHLD,这并不相关 - 它只有当do_fork() 从 sys_clone()执行时才相关,它从需要的用户空间请求的值传递给clone_flags。
current->vfork_sem 初始化(它在子进程中清除)。sys_vfork() (vfork(2) 系统调用,对应于clone_flags = CLONE_VFORK|CLONE_VM|SIGCHLD) 使父进程睡眠时会用到,直到子进程执行mm_release(), 例如作为exec()执行另外的程序或exit(2)的结果。
使用体系结构相关的alloc_task_struct()宏分配一个新的任务结构。在x86中,它就是用GFP_KERNEL优先级获取一个空闲页。这就是为什么fork(2)系统调用可能睡眠的第一个原因。如果分配失败,我们返回-ENOMEM。
使用赋值语句*p = *current,当前进程的任务结构所有的值都拷贝到新的结构中。 也许这应该用memset来替换?稍后,不应该被子进程继承的值设置成准确的值。
余下的代码采用大核心锁,它们是不可重入的。
如果父进程有用户资源(UID思想,Linux 很灵活,它是提出问题而不是制造事实),然后检验用户是否超出了RLIMIT_NPROC软限制 - 如果是的话,返回-EAGAIN,如果没有超出,就增加由给定uid进程记数p->user->count。
如果系统中任务数量超出可调的max_threads值,返回错误-EAGAIN。
如果正在执行的二进制代码属于模块化的执行域,增加相应的模块引用记数。
如果正在执行的二进制代码属于模块化的二进制格式,增加相应的模块引用记数。
子进程标记为'还未执行' (p->did_exec = 0)
子进程标记为'不可交换' (p->swappable = 0)
将子进程置为'不可中断睡眠'状态,即p->state = TASK_UNINTERRUPTIBLE (TODO: 为什么要这样做? 我认为它是不需要的 - 去掉它,Linus 确认它是不需要的)
根据clone_flags的值设置子进程的p->flags;对于简单的fork(2),它将是p->flags = PF_FORKNOEXEC。
子进程的pid p->pid在kernel/fork.c:get_pid()里用快速算法来设置 (TODO: lastpid_lock 自旋锁显得多余了,因为get_pid() 总是在大核心锁下从do_fork()中调用,同样去掉get_pid()的标记参数,在2000-06-20将补丁送给了Alan - 后来又发了一次)。
do_fork()中剩下的代码初始化子进程其余的任务结构。在最后,子进程的任务结构被散列进pidhash哈希表,子进程被唤醒 (TODO: wake_up_process(p) 设置 p->state = TASK_RUNNING 并且增加这个进程到运行队列,因此我们do_fork())之前多半不需要设置 p->state 为 TASK_RUNNING 。感兴趣的部分是设置p->exit_signal 为 clone_flags & CSIGNAL, 这对于 fork(2) 就意味着SIGCHLD ,设 p->pdeath_signal 为 0。当一个进程‘忘记'了原始的父进程(由于死了),就会用到pdeath_signal,它可以通过prctl(2)系统调用中的PR_GET/SET_PDEATHSIG命令来设置/获取(你也许会认为通过在prctl(2)用户空间的指针参数来返回pdeath_signal值这种方法很笨 - 是我的过失,在Andries Brouwer更新手册页之后已经太迟了还没有更正;)
这样任务就建立了。有几种原因使得任务终止:
执行exit(2) 系统调用;
接收到一个信号,缺省处理就是死亡;
在某些异常条件下被迫死亡;
通过func == 1调用bdflush(2)(这是Linux专用的,由于和在/etc/inittab中仍然有‘update'行的老的发布兼容的目的 - 现在更新的工作是通过核心线程kupdate来完成的)。
在Linux下实现系统调