2012年(11)
分类: LINUX
2012-09-15 15:41:28
一个process是一个正在running的program,不仅包括了代码(text段),还包括各种资源(地址空间、信号量、文件描述符)。譬如,两个process可以执行同一个program。program是一个可执行文件(elf格式等)。
Linux中task和process是一个意思。Linux中不特别区分thread和process,但thread可以看成是一种特殊的process,多个thread共享了同样的资源。
• fork()
process通过fork()来创建,通过exec()来执行program。
fork()内部调用clone()系统调用,其流程通常是分配内核栈,拷贝parent process的task_struct等副本,修改一些需要私有的变量(如PID、PPID、status等),然后根据flag来拷贝parent process的资源,资源包括了打开的文件描述符、信号、地址空间、page table等。来看一下clone()系统调用,普通创建一个process的fork()调用clone()的方式为:
clone(SIGCHLD, 0);//只共享了signal handlers
而创建一个thread调用clone()的方式为:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
//共享了地址空间(Virtual Memory)、文件系统、文件描述符等
从中也可以看到threads和process的创建都靠clone(),只是flag不一样而已,因为创建child process有可能是为了执行新的program,那么在fork()复制parent process的资源就会多此一举。
fork()为了避免copy采用了Copy-on-Write技术,child process如果需要修改资源,才会拷贝一份供自己专门使用。
内核线程和普通process不一样的地方在于,它们没有地址空间(mm指针为NULL),而且只存在于内核空间,像ksoftirqd、flush等都是内核线程。内核线程通过kthread_create、kthread_run等来创建和运行。
parent和child process都从fork()返回处继续或开始执行。
若child parent要执行一个新的program,它要执行exec()。exec()创建一个新的地址空间并加载新的program到process的地址空间中去执行。process通过exit()来结束。
parent process可通过wait()等函数获取child process的终止状态(zombie等)。
process在执行main函数中的exit()或者遇到了处理不了的signal的时候都会被终止,其流程为:
释放相关的资源(比如地址空间、从wait queue中移除、一些参考计数递减等)
通知parent process,并设置task_struct中的exit_state变量为EXIT_ZOMBIE
do_exit()调用schedule()来调度新的process。至此,该process已不能运行了,因为很多资源都释放了,但是还占有一定内存(内核栈、task_struct、thread_info)。需要ZOMBIE状态的原因是让parent process得知该进程退出时的状态。
接着是由parent process来清除处于ZOMBIE状态的process的所有内存。parent process通常会调用wait(),它会使parent process挂起直到收到某一个child process退出的信号。
若是parent process比child process先退出,然后child process再退出,那么kernel会重新设置child process的parent(比如设置为init),否则处于ZOMBIE状态的process的资源无法完全释放。
• task_struct结构
kernel保存了一个task list,其中的每一个元素都是一个process描述符,它包含了一个process的所有信息(地址空间、信号handler、process状态等),其数据结构为task_struct:
task_struct是通过slab allocator来分配的。在2.6以前,task_struct是保存在每一个process的内核栈的底端。在2.6以后,一个新的结构体stuct thread_info保存在了每一个process的底端,并通过它来寻找process描述符——task_struct。
通过current宏可以获取当前task的task_struct。current宏的原理是将内核栈指针低位清零,因为内核栈总是固定的大小(4KB或者8KB),找到栈顶的thread_struct之后再找到task_struct。
一个process有一个parent process和一些child processes,遍历方法如下:
但current宏只能在内核中使用吧,在用户空间不能够使用。
• process状态
process有这么几种状态:
TASK_RUNNING——process正在running或在running queue中等待着。
TASK_INTERRUPTIBLE——process正在sleeping(blocked),当条件满足,kernel会设置process的状态为TASK_RUNNING,从而唤醒这个process。处于该状态的process也可能因为收到signal而被唤醒。
TASK_UNINTERRUPTIBLE——与TASK_INTERRUPTIBLE区别在于即使收到signal也不会被唤醒。
还有__TASK_TRACED和__TASK_STOPPED。
通常process运行在用户空间,通过系统调用进入内核空间。此时的内核便处于进程上下文(process context),也可以说是内核代表着process在执行。退出内核空间的时候,若有高优先级的process,可能会发生process的切换。
另一个与process context相对应的是中断上下文(interrupt context),此时内核不代替process来执行,而是执行一个中断处理程序(interrupt handler)。处于中断上下文时,不存在process的概念(No process is tied to interrupt handlers),因为它没有task_struct。
• 进程调度
通过进程调度(process scheduling)让人感觉所有的process都是同时运行的。
进程调度有两个指标:低延迟fast process response time (low latency) 和高吞吐量 maximal system utilization (high throughput)。像IO型的process(硬盘、GUI、keyboard、网络)等都是访问频率高但是计算量小的,而像算法等process是访问频率低但是消耗计算量的,所以对于后者需要尽力减小scheduling的次数来提高效率。
Linux采用抢占式的进程调度,涉及优先级(priority,nice值)和时间片(time slice)两个概念。Linux的调度算法称为Completely Fair Scheduler (CFS)。CFS算法不分配给每个process一个固定的timeslice,而是根据nice值计算每个process在一个target latency中的权重,或者说是在一个target latency能够占用CPU的时间比重。当某个process的时间用尽,下一个被调度的process应该也是通过这两个值确定的吧?
系统定时器(system timer)以HZ的频率发出中断,中断间隔为一个tick。在该中断的处理程序中会减少当前process可以用的时间片,这些信息保存在task_struct的struct sched_entity变量中。
• 进程的sleep/阻塞/挂起
引入sleep是因为很多时候process需要等待某些事件,如果没有sleep的话,CPU只能循环忙等了。sleep的流程为:
标记process的状态为sleep(TASK_INTERRUPTIBLE或UNINTERRUPTIBLE)
加入waiting queue
退出running queue
调用schedule()(其实是交出CPU)
当条件满足,kernel会唤醒在wait queue上的process(全部或单个)。
• 抢占
抢占分为用户抢占(user preemption)和内核抢占(kernel preemption)。
用户抢占可能会发生在:系统调用返回至用户空间,中断处理返回至用户空间,进程sleep,显示调用schedule()等情况。
返回的时候会检查process中的need_resched标志(系统时间中断设置的?preempt_count?),如果制位,说明有高优先级的process准备中,这样就会schedule()。
以前内核是不能被抢占的,处于内核空间的process一定是等执行完其在内核空间的代码才能够调度。2.6之后内核可以被抢占。
• 系统调用
POSIX提供了标准的API接口,C库实现了API函数包括系统调用的接口。
通常C库函数的返回值为0表述函数执行成功,非0表示失败,并记录在全局变量errno中,通过perror()函数可以打印出具有可读性的错误信息。
系统调用的函数通常都是以sys_xxx()命名。考虑到安全性,应用程序不能直接访问内核空间,所以系统调用可以看成是应用程序通知内核去执行某一段内核程序,也可以看成为内核代替应用程序在执行。而应用程序通知内核的方法就是软件中断(不是软中断),在X86上就是“int $0x80”指令,因为软件中断号是128,在触发软件中断以前,需要设置系统调用号(syscall number)和系统调用参数。
仍然出于安全性的考虑,从用户空间传递给内核空间的读写指针需经过检查,这通过copy_from_user和copy_to_user来完成。这两个函数都有可能会导致挂起,因为需操作的用户数据可能在swap区域,而swap至内存是会让process sleep的。
执行系统调用的时候,内核处于进程上下文(process context),它是可以sleep、调度和被抢占的。
• 进程地址空间
task_struct中包含的mm_struct和vm_area_struct描述了上述地址空间的布局。
进程在用户空间使用的是用户空间的堆栈,进入内核态后,使用内核空间的堆栈。
进入内核态时,需要保存进程在用户态的运行环境,如被打断的位置、CPU现场信息(如寄存器、用户栈地址)等;恢复用户态时,从内核栈上保存的内容中恢复用户态的运行环境即可。
内核栈顶地址信息保存在task_struct中,而且进程从用户态转到内核态的时候,进程的内核栈总是空的,所以用户态就不用保存内核栈地址。
• 栈布局
图片来源:http://blog.csdn.net/wangxiaolong_china/article/details/6844371,感谢作者。
• 参考
Linux Device Drivers
Linux Kernel Development
http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory
http://blog.chinaunix.net/uid-20543672-id-2996319.html
http://blog.csdn.net/wangxiaolong_china/article/details/6844371