进程管理
在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。进程是出于执行期的程序以及它所包含的资源的总成。实际上完全可能存在两个或多个进程执行的是同一个程序。并且两个或两个以上并存的进程还可以共享许多共享资源。
在Linux系统中,这通常是调用fork()系统调用的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程被称为父进程,新产生的进程被称为子进程。
1、进程描述符及任务结构
内核把进程存放在任务队列的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符的结构,该结构定义在<linux/sched.h>文件中。
1.1 分配进程描述符
linux通过slab分配器分配task_struct结构,这样能达到对象服用和缓存着色的目的。由于现在yongslab分配器动态生成task_struct,所以只需在栈底或栈顶创建一个新的结构struct thread_info。这个新的结构能使在汇编代码中计算其偏移变得相当容易。
在x86上,thread_info结构在文件<asm/thread_info.h>
struct thread_info { struct task_struct *task; struct exec_domain; unsigned long flags; unsigned long status; __u32 cpu; __u32 preempt_count; mm_segment_t addr_limit; struct restart_block restart_block; unsigned long previous_esp; _u08 supervisor_stack[0]; };
每个任务的thread_info结构在它的内核栈的未断分配。结构中task域中存放的是指向该任务实际的task_struct的指针。
1.2 进程描述符的存放
内核通过一个唯一的进程标识值或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。内核把每个进程的PID存放在他们各自的进程描述符中。
在内核中,访问任务通常需要获得指向器task_struct指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度就显得尤为重要。
1.3 进程状态
- TASK_RUNNING,进程是可执行的,或者正在执行的,或者运行队列中等待执行的。
- TASK_INTERRUPTIBLE,进程正在睡眠,等待某些条件的达成。
- TASK_UNINTERRUPRIBLE,除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可打断状态相同。
- TASK_ZOMBIE,该进程已经结束了,但是其父进程还没有调用wait4()系统调用。
- TASK_STOPPED,进程停止执行。
1.4 设置当前进程状态
内核经常需要调整某个进程的状态。这是最好使用set_task_state(task,state)函数,该函数将制定的进程设置为指定的状态。必要的时候,它会设置内存屏障来强制其它处理器做重新排序。
1.5 进程上下文
可执行程序代码是进程的总要组成部分。这些代码从可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序调用或执行了系统调用或者出发了某个异常,他就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中current宏是有效的。程序恢复在用户空间继续执行。
系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行—对内核的所有访问都必须通过这些接口。
1.6 进程家族树
在Linux系统中进程之间存在一个明显的继承关系,所有的进程都是PID为1的init进程的后代。内核在系统调用的最后阶段启动init进程。该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。
进程间的关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct、叫着parent的指针。还含有称为children的子进程链表。所以,对于当前进程,可以通过下面的代码获得其父进程或子进程的进程描述符。
struct task_struct *my_parent = current->parent;
同样,也可以按以下方式依次访问子进程:
struct task_struct *task; struct list_head *list; list_for_each(list,¤t->children) { task = list_entry(list,struct task_struct,sibling); /* task现在指向当前的某个子进程 */ }
init进程的进程描述符是作为init_task静态分配的。
struct task_struct *task; for(task = current; task != &init_task; task = task->parent) { ; }
实际上,可以通过这种集成体系从系统的任何一个进程出发查找到任意指定的其他进程。
对于给定的进程,获取链表中的下一个进程:
list_entry(task->tasks.next, struct task_struct, tasks);
获取前一个进程的方法相同
list_entry(task->tasks.prev, struct task_struct, tasks);
2、进程创建
Linux采用两个单独函数来创建进程:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID、PPID和某些资源和统计量。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
2.1 写时拷贝
2.2 fork()
Linux通过clone()系统调用实现fork()。这个调用通过一些列的参数标志来指明父、子进程需要佛N个想的资源。fork()/vfork()/__clone()库函数度根据各自需要的参数标志去调用clone()。然后去调用do_fork()。do_fork完成了创建中的大部分工作。定义在kernel/fork.c文件中。该函数调用copy_process()函数,然后让进程开始运行。
- 调用dup_task_struct()为新进程创建一个内核栈、thread_info接口和task_struct接口,这些值与当前进程的值相同。此时名字进程和父进程的描述符是相同的。
- 检查新创建的这个子进程后,当前用户所拥有的晋城树木没有超出给它分配的资源的限制。
- 现在,紫禁城找收拾自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。进程描述符的程远志并不是继承而来的,而主要是统计信息。进程描述符中的大多数数据都是共享的。
- 接下来,紫禁城的状态被设置为TASK_UNINTERRUPTIBLE以保证他不会投入运行。
- copy_process()调用copy_flags()以更新task_struct的flags成员,表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0.表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
- 调用get_pid()为新进程获取一个有效的PID。
- 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。在一般情况下,这些资源会给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。
- 让父进程和子进程平分剩余的时间片
- 最后copy_process()做扫尾工作并返回一个指向子进程的指针。
再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒冰糖器投入运行。内核又一选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。
2.3 vfork()
vfork()系统调用与fork()的功能相同,除了不拷贝父进程的页表项。子进程作为父进程的一个单独的县城在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec().子进程不能像地址空间写入。
3、线程在Linux中的实现
线程机制是现代编程技术中常用的一种抽象。该机只提供了在同一程序内共享内存地址空间运行的一组线程。这些县城还可以共享打开的文件和其它资源。线程机制支持并发程序设计技术。在多处理器系统上,他也能保证真正的并行处理。
Linux实现现成的机制非常独特。Linux的线程都当作进程来实现。内核并没有准备特别的调度算法或是定义特别的数据接口来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个限成都拥有为一缕属于自己的task_struct,所以在内核中,他看起来就像是一个普通的进程(指示该进程和其他一些进程共享某些资源,如地址空间)。
线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源。
参数标志 | 含义 |
CLONE_FILES | 父子进程共享打开的文件 |
CLONE_FS | 父子进程共享文件系统信息 |
CLONE_IDLETASK | 将PID设置为0(只供idle进程使用) |
CLONE_NEWNS | 为子进程穿件新的命名空间 |
CLONE_PARENT | 制定子进程呵父进程拥有同一个父进程 |
CLONE_SETID | 将TID回写至用户空间 |
CLONE_SETTLS | 为子进程创建新的TLS |
CLONE_SIGHAND | 父子进程共享信号处理函数 |
CLONE_SYSVSEM | 父子进程共享System V SEM_UNDO语义 |
CLONE_THREAD | 父子进程放入相同的线程组 |
CLONE_VFORK | 调用vfork(),所以父进程准备睡眠等待子进程将其唤醒 |
CLONE_UNTRACED | 防止跟踪进程在子进程上千只执行CLONE_PTRACE |
CLONE_STOP | 以TASK_STOPPED状态开始进程 |
CLONE_SETTLS | 为子进程创建新的TLS |
CLONE_CHILD_CLEARTID | 清除子进程的TID |
CLONE_PTRACE | 继续调试子进程 |
CLONE_CHILD_SETTID | 设置子进程的TID |
CLONE_PARENT_SETTID | 设置父进程的TID |
CLONE_VM | 父子进程共享地址空间 |
内核线程
内核经常需要在后台执行一些操作。这种任务可以通过内核线程完成—独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核县城没有独立的地址空间。他们只在内核空间运行,从来不切换到用户空间去。内核进程和普通的晋城一样,可以被调度,也可以被抢占。
内核线程只能由其他内核线程创建,在现有内核县城中创建一个新的内核线程的方法如下:
int kernel_thread(int (*fn)(void *),void *arg,unsigned long flags)
新的任务也是通过向普通的clone()系统调用传递特定的flags参数而创建的。
4、进程终结
进程的终结在他调用exit()之后,即可能显示的调用这个系统调用,也可能隐私的从某个程序的主函数返回。该任务大部分要靠do_exit()来完成。
- 首先,将task_struct中的标志成员设置为PF_EXITING
- 其次,调用del_timer_sync()删除任意内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
- 如果BSD的进程记账功能是开启的,do_exit()调用acct_process()来输出记帐信息
- 然后调用_exit_mm()函数放弃进程占用的mm_struct,如果没有别的进程使用它们,就彻底释放他们。
- 接下来调用exit_sem()函数。如果进程排队等候IPC信息,它则离开队列。
- 调用_exit_files(),_exit_fs(),exit_namespace()和exit_sighand(),已分别递减文件描述符、文件系统数据,进程名字空间和信号处理函数的引用计数。如果其中某些引用计数的数值降为零,NAMES就代表没有进程在使用相应的资源,此时可以释放。
- 接着吧存放在task_struct的exit_code成员中的任务推出代码置位exit()提供的代码中,或者完成任何其他有内核机制规定的退出动作,退出代码存放在这里供父进程随时检索。
- 调用exit_notify()向父进程发送信号,将子进程的父进程重新设为线程组中的其他线程或init进程,并把进程状态设为TASK_ZOMBIE。
- 最后,do_exit()调用schedule()切换到其他进程,
4.1 删除进程描述符
在调用了do_exit()之后,尽管县城已经僵死不能再运行了,但是系统还保留了它的进程描述符。当最终需要释放进程描述符时,release_task()会被调用,用以完成以下工作。
4.2 孤儿进程造成的进退维谷
如果父进程在子进程之前退出,必须有机制来保证紫禁城能找到新的父亲,否则的话这些成为孤儿的进程就会在退出时永远处于僵死状态。解决方法是给紫禁城在当前线程组内找一个县城作为父亲,如果不行,就让init做他们的父进程。在do_exit中会调用notify_parent(),该函数会通过forget_original_parent()来执行寻父进程。