进程是处于执行期的程序,除了可执行代码,还包含其他资源,如打开的文件、挂起的信号、寄存器、内存地址空间、一个或者多个执行线程。linux内核不区分进程和线程,统一为task,task就是内核调度的对象。
fork():通过复制一个现有进程来创建一个全新的进程,fork()返回两次,一次回到父进程,一次回到子进程
exit():终结进程并释放资源
wait4():父进程调用这个系统调用查询子进程是否终结。子进程退出后被设置为僵死状态,直到父进程调用wait()或者waitpid()
内核进程数据结构:
进程存放在task list中,是一个双向链表,每个链表节点是一个进程描述符,类型为task_struct,定义在<linux/sched.h>中
分配进程描述符:
linux通过slab分配器分配task_struct结构,达到对象复用和缓存着色的目的,即通过预先分配和重复使用task_struct,避免动态分配和释放造成的资源损耗,可以迅速创建进程。因此slab分配器只要分配struct thread_info结构即可,这个结构较小,内部包含一个指向task_struct的指针,定义在<asm/thread_info.h>中,如下:
struct thread_info {
struct task_struct * task;
struct exec_domain * exec_domain;
_u32 flags;
_u32 status;
_u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uacdess_err;
};
PID:用来唯一标识每一个进程,PID最大值就是系统允许同时存在的进程数量。PID使用是从小到大循环。
current:是宏,用来查找当前正在运行进程的进程描述符,和硬件体系相关,x86这种寄存器较少的体系结构需要通过创建thread_info结构通过计算偏移间接查找task_struct结构,方法是调用current_thread_info()函数,代码如下:
movl $-8192, %eax
andl %esp, %eax
意思是,赋值AX寄存器为-8192,在内存中是前面全是1,后面13个0,然后与栈顶指针SP,屏蔽后13个有效位,得到偏移
进程状态:在进程描述符中的state字段中描述,有如下5种:
1.TASK_RUNNING:进程是可执行的,或者正在执行,或者在运行队列中等待执行
2.TASK_INTERRUPTIBLE:可中断,进程正在睡眠,可以被信号唤醒
3.TASK_UNINTERRUPTIBLE:不可中断,不会被信号唤醒,这种进程不会被kill信号杀死
4._TASK_TRACED:被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪
5._TASK_STOPPED:进程停止执行,没有投入运行也不能投入运行,通常发生在进程收到SIGSTOP等信号的时候
一个进程声明周期中状态切换的例子:
1.fork()一个新进程,状态是TASK_RUNNING,但是还未被投入运行,这时候进程在运行队列中
2.调度程序将任务投入运行,schedule()函数调用context_switch()函数,此时进程状态仍然是TASK_RUNNING
3.若进程调用do_exit(),那么进程退出;若等待特定事件,那么任务在等待队列上睡眠,进入TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE状态
4.等待的事件发生之后,进程被唤醒,重新置于运行队列中,状态变成TASK_RUNNING
设置当前进程的状态:
调用set_task_state(task, state)函数,task是当前进程,state是要设置的状态
进程家族树:
linux系统中所有进程都是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程,init进程读取系统初始化脚本,执行相关程序并最终完成系统启动过程。系统中每个进程都有一个父进程,有0个或者多个子进程,有同一个父进程的进程是兄弟。进程间的关系存放在进程描述符中,每个task_struct都有一个指向父亲的指针和指向儿子的链表,通过如下代码获取父进程的进程描述符:
struct task_struct * my_parent = current->parrent;
通过如下方式遍历子进程:
struct task_struct * task;
struct list_head * list;
list_for_each(list, ¤t->children)
{
task = list_entry(list, struct task_struct, sibling);
}
如下方式找到init进程:
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);
使用如下宏遍历整个任务队列:
struct task_struct * task;
for_each_process(task) {
printk("%s[%d]\n", task->comm, task->pid);
}
unix进程创建过程:
fork()拷贝当前进程创建一个子进程,子进程的PID,PPID和父进程不同;exec()读取可执行文件,载入到地址空间并开始执行
写时拷贝:fork()的时候,内核并不复制整个进程地址空间,子进程和父进程共享同一个拷贝,只有需要写入的时候,才复制数据,让子进程和父进程有不同的拷贝,在此之前,以只读方式共享,这样地址空间的拷贝推迟到实际发生写入的时候才进行,fork()的开销实际就是复制父进程的页表和给子进程创建唯一的进程描述符
fork()代码流程:linux通过clone()系统调用实现fork(),clone()中调用do_fork(),这个函数完成了创建的大部分工作,核心是copy_process()函数,copy_process()流程如下:
1.调用dup_task_struct()给新进程创建一个内核栈、thread_info结构和task_struct,这些值和当前进程完全相同
2.检查当前用户拥有的进程数量没有超过分配给它的资源
3.开始区分子进程和父进程,进程描述符中很多字段要初始化
4.子进程状态设置为TASK_UNINTERRUPTIBLE,以保证不会被投入运行
5.调用copy_flags()更新task_struct的flags成员,设置进程是否拥有超级用户权限、是否调用了exec()函数等标记
6.调用alloc_pid()分配一个新的pid
7.根据传递给clone()的标记,拷贝或者共享打开的文件、文件系统信息、信号处理函数、进程地址空间、命令空间等
8.返回一个指向子进程的指针
9.此时返回do_fork()函数,若copy_process()函数成功返回,那么唤醒新创建的子进程并投入运行
内核线程:独立在内核空间的标准进程,只在内核态运行,创建新的内核线程方法如下:
struct task_struct * kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
新的task由kthread内核进程通过clone()创建的,运行threadfn函数,传递给它的参数是data,进程被命名为namefmt。新创建的进程处于不可运行状态,通过调用wake_up_process()唤醒,也可以通过kthread_run()函数直接完成这两个步骤。kthread_run是宏,只是简单调用了kthread_create和wake_up_process(),如下:
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *k; \
k = kthread_create(threadfn, data, namefmt, ##_VA_ARGS__); \
if (!IS_ERR(k)) \
wake_up_process(k); \
k; \
})
内核线程启动后一直运行直到do_exit()退出,或者内核的其他部分调用kthread_stop()退出
进程终结:不管主动或者被动终结,都调用do_exit()函数处理,流程如下:
1.将task_struct中的标记成员设置为PF_EXITING
2.调用del_timer_sync()删除任一内核定时器,根据返回的结果,确保没有定时器在排队,也没有定时器处理程序在运行
3.若BSD记账功能开启,那么调用acct_update_integrals()来输出记账信息
4.调用exit_mm()函数释放进程占用的mm_struct,若没有别的进程使用(即地址空间没有共享),那么彻底释放
5.调用sem__exit()函数,如果进程排队等候IPC信号,那么离开队列
6.调用exit_files()和exit_fs(),递减文件描述符、文件系统数据的引用计数
7.存放在task_struct的exit_code成员中的任务退出代码设置为exit()提供的退出代码
8.调用exit_notify()向父进程发送信号,给子进程重新找养父,养父是其他进程或者init进程,进程状态(task_struct中的state)设置为EXIT_ZOMBIE
9.调用schedule()切换到新的进程,由于EXIT_ZOMBIE的进程不会被调度,因此这是进程执行的最后一行代码,do_exit()永不返回
删除进程描述符:do_exit()之后,进程是僵死状态,进程描述符还是存在的,调用release_task()删除进程描述符,流程如下:
1.调用__exit_singal(),调用_unhash_process(),调用detach_pid(),从pidhash中删除该进程,从任务列表中删除该进程
2._exit_singal()释放目前僵死进程使用的所有剩余资源
3.若进程是线程组的最后一个进程,并且领头进程已经死了,那么通知僵死的领头进程的父进程
4.调用put_task_struct()释放进程内核栈和thread_info结构所占的页,释放task_struct占用的slab高速缓存