第三章 进程管理
3.1 进程
进程是处于执行期的代码。通常进程还要包含其他资源,像打开的文件、挂起的信号、内核的内部数据、处理器状态、一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当然还包括用来存放全局变量的数据段等。
进程提供两种虚拟机制:虚拟处理器和虚拟内存。
通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。
3.2 进程描述符以及任务结构
内核把进程的列表存放在叫做任务队列(task list)的双向链表中。链表中的每一个项都是类型为task_struct(processdescriptor
进程描述符)的结构。该结构定义在<linux/sched.h>
中。
进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态,还有其他更多信息。
3.2.1分配进程描述符
Linux通过slab分配器分配task_struct
结构,各个进程的task_struct
存放在它们内核栈的尾端,这样做是为了让那些像x86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置。
struct thread_info
在文件<asm/thread_info.h>
中定义:
struct thread_info {
struct task_struct *task;
struct exec_domain *exec_domain;
unsigned int flags;
unsigned int status;
unsigned int cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};
3.2.2进程描述符的存放
内核通过一个唯一的进程标识值(process identification value
)或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。PID的最大值默认设置为32768,这受<linux/threads.h>
中所定义PID最大值的限制。如果确实需要的话,可以不考虑与老式系统的兼容,由系统管理员通过修改/proc/sys/kernel/pid_max
来提高上限。
3.2.3进程状态
进程描述符中的state域描述了进程的当前状态,该域的值也必为下列五种状态之一:
- TASK_RUNNING(运行)—进程是可执行的。
- TASK_INTERRUPTIBLE(可中断)—进程正在睡眠,等待某些条件的达成。
- TASK_UNINTERRUPTIBLE(不可中断)
- TASK_TRACED—被其他进程跟踪的进程。
- TASK_STOP(停止)—进程停止执行。
3.2.4设置当前进程状态
内核经常需要调整某个进程的状态,这时最好使用set_task_state(task,state
)函数。参看<linux/sched.h>
中对这些相关函数实现的说明。
3.2.5进程上下文
一般程序在用户空间执行。当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。
3.2.6进程家族树
Unix系统的进程之间存在一个明显的继承关系,所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动Init进程。该进程读取系统初始化脚本(initscript
)并执行其他的相关程序,最终完成系统启动的整个过程。
每个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指向当前某个子进程*/
}
获取下一个进程和前一个进程:
Task = list_entry(task->tasks.next, struct task_struct, tasks);
Task = list_entry(task->tasks.perv, struct task_struct, tasks);
3.3 进程创建
分解到两个单独的函数中去执行:fork()和exec()。
- 首先,fork()通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID、PPID和某些资源和统计量。
- exec() 函数负责读取可执行文件并将其载入地址空间开始运行。
3.3.1写时拷贝
Linux的fork()使用写时拷贝(copy-on-write
)页实现。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。
3.3.2fork()
Linux中通过clone()系统调用实现fork()。Fork()、vfork()、和__clone()
库函数根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()
。
do_fork完成了创建中的大部分工作。该函数调用copy_process()
,然后让进程开始运行。
3.3.3vfork()
vfork()
的好处就仅限于不拷贝父进程的页表项。
3.4线程在linux中的实现
Linux把所有的线程都当做进程来实现。线程仅仅被视为一个与其它进程共享某些资源的进程。每个线程都拥有惟一隶属于自己的task_struct
。
3.4.1创建线程
线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SINHAND , 0)
;
普通的fork()的实现:clone(SIGCHILD, 0)
;
vfok()的实现:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)
;
clone使用的参数标志定义在<linux/sched.h>
中。
3.4.2内核线程
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间。
内核线程也只能由其他内核线程创建。内核是通过从kthreadd内核进程中衍生出所有新的内核线程来自动处理这一点的。在<linux/kthread.h>
中申明有接口,从现有内核线程中创建一个新的内核线程的方法:
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
新的任务是由kthread内核进程通过clone()系统调用而创建的。穿件一个进程并让它运行起来,可以通过调用kthread_run()
来达到:
struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
内核线程启动后就一直运行直到调用do_exit()
退出,或者内核的其他部分调用kthread_stop()
退出。
3.5 进程终结
一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,既可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回。不管进程是怎么终结的,该任务大部分都要靠do_exit()
来完成。
至此,与进程相关联的所有资源都被释放掉了。进程不可运行并处于EXIT_ZOMBIE
退出状态。它占用的所有内存主要就是内核栈、thread_info
和task_struct
结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统调用。
3.5.2孤儿进程造成的进退维谷
对于孤儿进程的解决办法:
解决办法就是给子进程在当前线程组内找到一个线程作为父亲,如果不行,就让init做它们的父进程。
在do_exit()
中会调用exit_notify()
,该函数会调用forget_original_parent()
,而后者会调用find_new_reaper()
来执行寻父过程。