Linux内核设计与实现笔记--chapter3 进程管理

第三章 进程管理

借用结尾的一段话,这一章讨论了进程的重要性以及进程与线程之间的关系;然后讨论了Linux如何存放和表示进程、如何创建进程、如何把新的执行影响放到地址空间、如何表示进程的层次关系、父进程是如何收集到后代信息、以及进程最终使如何消亡的。

看似短短的十几页居然包含了如此多的内容,尽个人所能对上述内容进行总结吧。


3.1 进程

进程是处于执行期的代码以及相关资源(包括打开的文件、挂起的信号、内核的内部数据、处理器状态、具有一个或多个具有内存映射的内存地址空间以及执行线程)。进程提供两种虚拟机制:虚拟处理器和虚拟内存(在刚读到这一段的时候完全没有概念,先继续往后看吧)。


创建新的进程目的是为了立即执行新的、不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,把新程序载入。


线程是在进程中活动的对象,同时每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。


3.2 进程描述符以及任务结构

引用原话:内核把进程的列表存放在叫做(task list)的双向链表中。Tasklist中的类型是task_struct(processdescriptor 进程描述符)。Task_struct 定义在<linux/sched.h>中。


进程描述符描述一个正在执行的程序:它打开的文件、进程地址空间、挂起的信号、进程的状态等等。示意图:

 

Linux通过slab分配器分配task_struct,该结构存放在进程栈的栈尾,这样的结构可以方便的通过栈指针计算出它位置。文中使用thread_info的结构为例,说明了这个问题。

Struct thread_info 在<asm/thread_info.h>中定义,虽然task_struct是动态配置的,但是我们在<linux/sched.h>中还是可以看到一个完整的结构定义(这是一个巨大的结构定义):

struct thread_info {

       structpcb_struct pcb;              /* palcode state */

 

       structtask_struct       *task;            /* main task structure */

       unsignedint        flags;             /* low level flags */

       unsignedint        ieee_state;   /* see fpu.h */

 

       structexec_domain    *exec_domain;     /* execution domain */

       mm_segment_t           addr_limit;    /* thread address space */

       unsigned              cpu;              /* current CPU */

       int                 preempt_count; /* 0 =>preemptable, <0 => BUG */

 

       intbpt_nsaved;

       unsignedlong bpt_addr[2];              /*breakpoint handling  */

       unsignedint bpt_insn[2];

 

       structrestart_block    restart_block;

};

 

知道进程是如何分配的之后,就要了解进程是如何被存放的。内核通过(进程标识符process identification value)或PID来表示进程的。PID的类型为pid_t它是一个int类型,PID的最大默认设置为32768,在<linux/threads.h>中定义了PID的上限。同时在系统中,可以通过修改/proc/sys/kernel/pid_max修改上限。

上面所有内容都是了解进程工作前的一些准备工作。了解完原理之后就可以进行进程的各种工作状态,以及进程是如何被创建和注销的。

在task_struct中的第一个元素state就是关于进程状态的描述,(-1 unrunnable, 0 runnable, >0 stopped摘自注释)。进程总共有5种状态:

-TASk_RUNNING

-TASK_INTERRUPTIBLE

-TASK_UNINTERRUPTIBLE

_TASK_TRACED

_TASK_STOP

 

内核通过set_task_state(task,state)函数调整进程状态。相关函数的说明在<linux/sched.h>中。

 

这里再插入一个名词:进程上下文—一个程序在用户空间执行,当一个程序执行系统调用或是出发异常之后,陷入内核空间,就称内核“代表进程执行”并处于进程上下文中(process context)。

 

进程总是由其他进程创建的,linux的所有进程都是PID为1的init进程的后代。Init在系统启动后立即执行,该进程读取系统初始化脚本(initscript)并执行相关程序,完成启动过程。

 

所以为了能够使系统更好的对父进程和兄弟进程进行管理,在task_struct中有一个parent指针指向其父进程,还有一个children子进程列表。

获得父进程的进程描述符的方法:

Struct task_struct *my_parent = current->parent;

访问子进程的方法:

Struct task_struct *task;

Struct list_head *list;

 

List_for_each(list, &current->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 创建进程

在Linux中创建进程是分成两部分完成的:

1.  fork() 拷贝当前进程创建子进程,子进程与父进程的区别仅在于PID、PPID、资源和统计

2.  exec() 负责读取可执行文件并将其载入地址空间开始运行。

在下面所有工作开始之前,本书又提出了一个比较重要的概念copy-on-write(写时拷贝)。也就是说进程复制工作只有在需要写入的时候才进行,在此之外,只是只读方式共享。这样使地址空间的页拷贝被推迟到了发生写入时进行,有效的节省了拷贝造成的不必要开销。


之后就详细讲述了进程拷贝和注销的过程原理。

      

进程的复制:fork()

Linux中通过系统调用clone()实现fork()。Fork()、vfork()、__clone()根据各自的参数去调用clone(),然后由clone()去调用do_fork()。

    

定义在kernel/fork.c中的do_fork完成了创建的大部分工作。do_fork又去调用copy_process()然后让进程运行(个人理解do_fork通过了一系列复杂的判断,其实的关键工作就是调用copy_process())。copy_process()完成的工作:

      

 

进程的复制:vfork()

与fork()不同在于这个函数不调用父进程的页表。(人家说这个功能在有了copy-to-write后基本就废弃了,那咱还是别深究了吧。。。)

 

3.4线程在linux中的实现

对于线程的描述,每本linux编程书籍的描述都是一样的,线程在linux中很像进程,它在内核中被视为与其它进程共享某些资源的进程,每个线程都有属于自己的惟一task_struct。


线程创建也是用clone,只需表明出需要共享资源,对比线程、fork、vfork创建的区别(这几个定义我确实没找到在哪里,望高人指点):

Clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SINHAND , 0);

Clone(SIGCHILD, 0);

Clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

 

Clone使用的参数标志定义在,<linux/sched.h>中:


在内核空间的线程与用户空间略有不同,它没有独立的地址空间。使用ps –ef可以查看内核线程。内核线程同样是由一个父进程创建的,在<linux/kthread.h>中存放内河线程的响应功能:

struct task_struct *kthread_create(int (*threadfn)(void *data),

                               void *data,

                               const char namefmt[], ...)

       __attribute__((format(printf,3, 4)));

 

前面提到新的线程是通过clone()实现的,内核线程同样如此。新创建的进程是通过wake_up_process()函数唤醒的。

struct task_struct *kthread_run(int (*threadfn)(void *data),

                               void *data,

                               const char namefmt[], ...)

…);

内核线程的退出调用do_exit(),或者kthread_stop()。

 

3.5 进程的终结

进程的显式退出是通过调用exit(),隐式退出从某个主函数返回。无论进程是如何退出的,大部分都是调用定义在kernel/exit.c的do_exit()的功能实现的,完成的主要工作:

这些工作完成后,与进程相关的所有资源被释放,并使进程进入EXIT_ZOMBIE退出状态。剩余内存主要是内存栈、thread_info和task_struct结构。次进程存在的目的就是向父进程提供相关信息。父进程检索后,通知内核释放剩余内存。

最后释放进程描述符调用release_task(),完成的工作:

就此,释放工作全部完成。

 

对于孤儿进程的解决办法

这个问题的解决办法就是为该子进程在当前线程组内找到一个线程作为根。若没有就使init作为父进程。整个工作过程如是:

do_exit()->exit_notify()->forget_original_parent()->find_new_reaper()进行父进程查询。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值