Linux内核学习大总结之一:进程管理
首先,让我们了解一下linux的进程队列结构。
1.1linux进程队列其实是一个双向链表,每一个结点就是一个进程描述符。进程描述符里面包含了进程所有的信息:进程所打开的文件、进程的地址空间、挂起信号、进程状态和其他更多的信息。以下是进程描述符的部分定义:
shruct task_struct
{
unsigned long state;
int prio;
unsigned long policy;
struct task_struct *parent;
struct list_head tasks;
pid_t pid;
......
} ;
linux通过slab分配器分配task-struct结构。它实际上是一个栈,其栈顶(向上增长的栈)或栈底(向下增长的栈)中有一个thread info结构,其中的task指针指向task_struct。
1.2那么,如何识别这些进程呢?进程描述符里面有一个pid,它就是进程间区别标志。它实际上是一个短整型数据,也就是说它最大值为32767。一般来说,32767对于很多桌面系统已经足够,但是对于大型服务器,就必须修改这个上限。
那么,又如何寻找这些进程呢?呵呵,进程队列是一个双向链表,那么我们当然可以遍历它寻找需要的进程。当然,这不是什么好主意,因为这很耗费时间!一般的,有些硬件体系有专门的寄存器存放了指向当前task_struct的指针,可以很快的找到当前进程。在X86系列机中,寄存器非常有限,一般使用通过计算thread_info的偏移量的方法来找到当前进程。
1.3接下来,我们讨论linux进程的状态。
·运行(TASK_RUNNING):一般指就绪状态——指进程随时可以投入运行和运行状态——指进程正在运行。
·可中断(TASK_INTERUPTIBLE):这个时候,进程停止运行,直到它获得满足它继续运行的条件。
·不可中断(TASK_UNINTERUPTIBLE):这个时候,进程也是停止运行,但是,即便它获得满足它继续运行的条件,它也不会马上被激活。
·僵死(TASK_ZOMBIE):进程运行结束,等待父进程销毁它。
·停止(TASK_STOPPED):进程停止运行,当进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号,就会停止。在调试期间,进程收到任何信号,也会停止运行。
那么,如何设置进程这些状态呢?使用set_task_state(task,state)函数,就可以改变进程描述符里的进程状态state。
1.4好了,进程之间的关系如何?前面说了,进程队列是一个双向链表,那么,他们具体是如何连接的呢?
首先,系统开启的最后一段时间,系统将创建一个init进程,它是系统的第一个进程,它是所有进程的祖先,任何进程,如果不断的追朔其父进程,那么都会回到这个init进程。
这样,进程队列中的进程就有两种连接方式:一种就是刚才说的双向链表;还有一种就是各进程有父子关系,或者兄弟关系。
1.5关于创建进程。linux创建进程很复杂,首先调用fork(),接着fork()调用clone(),然后clone()调用do_fork(),而do_fork()调用copy_process()。每一步调用都完成不同的任务。
首先是copy_process()的一系列工作:
第一步调用dup_task_struct()复制父进程(没错,这个时候描述符也是一样的!),创建内核栈,检查进程数目有没有超过资源限制。
接着,把进程描述符中的各项设为0或者初始值,并把进程状态设为不可中断(TASK_UNINTERUPTIBLE),保证进程未创建完毕前不要投入运行。
第三步,调用copy_flags(),设置描述符中的flag,表明进程是否有超级权限。
然后调用get_pid()设置进程pid。
第五步,根据clone()传来的参数,拷贝或者打开文件、文件系统信息信号处理函数、进程地址空间和命名空间等等。一般来说,linux其实使用的是写时复用,也就是共享资源。(待会会讨论到)
第六步,让父进程和子进程平分剩余的时间片。
最后,返回一个指向子进程的指针。
如果成功的返回到do_fork(),这个新建的子进程会被唤醒,准备投入运行。当然,一般是马上投入运行,也即调用exec()函数,将程序代码(当然是计算机可以直接识别运行的代码)载入进程地址空间,执行里面的代码!
1.6vfork()——这也是一个创建进程的函数,唯一和fork()不一样的是,再创建子进程时它会使父进程挂起,直到创建结束。
1.7线程的话题。其实linux系统没有线程的概念!很吃惊吧?更准确的说,linux内核中,进程和线程都称为任务(task),只有在用户级层面,才有线程这一说法。即便如此,linux仍然视进程和线程为同样的概念,因为它们都有自己的描述符。
一般来说,我们都说线程是“轻进程”,因为线程共享产生它的进程的地址空间、文件资源等等。但是,linux中,进程的产生很多时候使用写时复用,既和父进程共享资源(如果子进程创建后马上调用exec(),那么连共享资源也免了,进程可以直接投入运行!),这一点和线程一样,使得进程和线程没有太大区别。
1.8终结进程。进程终结也需要做很多繁琐的收尾工作。系统必须保证进程所占用的资源回收,并通知父进程。
这里就只大致说下具体进程结束过程:系统调用exit(),开始析构进程。
然后调用sem__exit()使该进程进程队列。
把进程设置为僵死状态。这个时候,进程无法投入运行了,它的存在只为父进程提供信息——申请死亡。
父进程得到信息后,开始调用wait4(),最终赐死子进程——子进程占用的所有资源被全部释放。
这里很显然有一个问题:如果被杀死的子进程同时是其它进程的父进程,那么该怎么办呢?如果置之不理,那么那些子进程将会变为僵死状态,这不是我们所想看到的。
处理方法是为那些子进程寻找父进程,甚至可以设置init为其父进程。
下次,我们将讨论进程的调度问题,敬请期待!