什么是进程?
进程是处于执行期的程序以及它所包含的资源的总称.
LINUX下进程的特性:
1.进程的出生:
fork()系统调用.其实现机制是通过复制一个现有的进程来创建一个全新的进程.
2.进程的执行:
exec*()函数族创建新地址空间并把新的程序载入执行.
3.进程的死亡:
exit()
3.1 进程描述符及任务结构
LINUX内核里面用结构体struck task_struck来表征一个进程.在32位机器上,其大小约1.7K字节.内核里面往往存在多个进程,这时候当然需要一种类型数组或链表的组织里面被组织起来.组织进程的叫做任务队列,它是一个双向链表,表中每一项都是类型为task_struck.因此,我们以后和进程打交道,就意味着和struck task_struck结构体打交道.
3.1.1 进程的三态:
运行态、停止态、休眠态.
3.1.2 进程上下文
当用户空间进行了系统调用或触发某中断陷入到内核态.这时候我们称内核态"代表进程执行",此时的场景我们谓之为进程上下文.
3.1.3 进程家族树
所有的进程都是PID为1的init进程的后代.内核在系统启动的最后阶段启动init进程.
我们常常听到类似的字眼,"父进程"、"子进程"、"兄弟进程"等.那么,作为进程的结构体表征--task_struck必定会有相关"血缘关系"的说明.如下,每个task_struck都包含一个指向其父进程的task_struck,叫做parent指针,还包含一个称为children的子进程链表.
获取当前进程的父进程示意代码:
Struck task_struck *my_parent = current->parent;
访问子进程的示意代码:
Struck task_struck *task;
Struck list_head *list;
List_for_each(list,¤t->children)
{
Task = list_entry(list,struck task_struck,sibling);
}
Init的进程描述符是作为init_task静态分配的.下面的代码可以很好的演示所有进程之间的关系:
Struct task_struck *task;
For(task = current; task != &init_task; task = task->parent)
获取下一个进程:
List_entry(task->tasks.next,struct task_struct,tasks);
Next_task(task);
获取前一个进程:
List_entry(task->tasks.prev,struct task_struck,tasks);
Prev_task(task);
访问整个任务队列:
Struct task_struct *task;
For_each_process(task)
{
/*打印每个进程的名称和PID*/
Printk("%s[%d]\n",task->comm,task->pid);
}
3.2 进程的创建
LINUX中进程的创建分两步走:fork()和exec()两函数.首先,fork()通过拷贝当前进程创建一个子进程.子进程与父进程的区别仅在于PID(每个进程唯一)、PPID(父进程的PID)和某些资源和统计量;然后,exec()函数负责读取可执行文件并将其载入地址空间运行.
3.2.1 写时拷贝
Linux 的fork()使用写进拷贝(copy-on-write)页实现.写时拷贝是一种推迟甚至免除拷贝数据的技术.内核此时并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝.只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝.这样避免了大量根本不会被用到的数据的拷贝.
3.2.2 fork()函数调用过程
调用流程:
fork()/vfork()/__clone() --> do_fork() --> copy_process().
copy_process()主要工作如下:
A.调用dup_task_struck()为新进程创建内核栈、thread_info结构和task_struck;
B.检查新进程相关合法性,如系统的进程数据是否超出给定的资源限制;
C.对进程描述符进程清0动作;
D.标识新进程状态为TASK_UNINTERRUPTIBLE保证新进程不会投入运行;
E.调用copy_flags()更新task_struck的flags成员.表明进程是否拥有超级用户权限的PF_SUPERPRIV标识被清0.表明没调用exec()函数的PF_FORKNOEXEC标识被设置;
F.调用get_pid()为新进程获取上个有效的PID;
G.初始化进程描述符;
H.让父进程了新进程平分剩余的时间片;
I.打完收工.
3.3 线程
LINUX中的线程只是一种较为特殊的进程而已.内核线程--独立运行于内核空间的标准进程,它没有独立的地址空间(其mm指针被赋为NULL).创建内核线程如下:
int kernel_thead(int (*fn)(void*),void *arg,unsigned long flags);
各参数意义如下:
fn:内核线程的运行体;
arg: 内核线程的运行体的参数;
flags:clone标志,常用的内核线程标志有CLONE_FS、CLONE_FILES、CLONE_SIGHAND.
一般而言,内核线程在创建的时候一直运行下去,直到重启或死机.
clone()参数标识罗列如下:
CLONE_FILES 父子进程共享打开的文件
CLONE_FS 父子进程共享文件系统信息
CLONE_IDLETASK 将PID设置为0(只供idle进程使用)
... ...
见<<LINUX内核设计与实现 第2版>>P25.
3.4 进程的终结
LINUX中进程的终结主要分两部分来完成.一部分是自身调用exit()函数(大部分工作靠do_exit()函数来完成);另一部分是父进程得知此子进程已经终结了,释放子进程的task_struck结构体.
3.4.1 孤儿进程
孤儿进程,是指那些其依赖的父进程先于其退出而产生的进程.由上述知道,一个进程退出分两部分,后部分是通过其父进程来回收task_struck结构体.孤儿进程并没有父进程,其退出时通过自身调用exit()处于僵死状态,其所占用的task_struck并没有父进程去回收,会导致内存的白白耗费.因此,LINUX必须提供种机制来避免这种情况.为孤儿进程找一个新的父进程.解决方案是给孤儿进程在当前线程组内找一个线程作为父进程,如果不行,就让init做它们的父进程.