【第三章 进程管理】
13.每个进程拥有独立的程序计数器、进程栈和一组进程寄存器。
内核调度的对象是线程而非进程。
Linux系统不区分线程和进程。
14.内核把进程的列表存放在任务队列(tasklist)的双向循环链表中。
链表中的每一项都是类型为task_struct,称为进程描述符(process descriptor )的结构。
定义于<liunx/sched.h>中
15.Linux通过Slab 分配器分配task_struct 结构,这样能达到对象复用和缓存着色(cache cloring)的目的
每个任务的thread_info 结构在他的内核栈的尾端分配。
struct thread_info{
struct task_struct *task;
struct exec_domain *exec_domain;
_u32 flag;
_u32 status;
_u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
strtct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};
16.内核通过一个唯一的进程标识值(process identification value)来标识每个进程。
默认类型为short int 。(32768)
可通过修改 /proc/sys/kernel/pid_max 提高标识值的上限。
17.为何要将task_struct或thread_info 存于内核栈尾端?
是为了避免使用额外的寄存器专门记录,X86的寄存器并不富裕。
18.进程状态
①TASK_RUNNING 可执行的,正在执行或者在运行队列等待执行。
②TASK_INTERRUPTIBLE 可中断的,
进程正在睡眠或者被阻塞,等达到某些条件的达成,之后变成运行态,处于此状态的进程会因为接收到信号而提前唤醒并准备投入运行。
③TASK_UNINTERRUPTIBLE 不可中断的。
④_TASK_TRACED 被其他进程跟踪的进程
⑤_TASK_STOPPED 停止,没有投入运行也不能投入运行。通常按剩余接收到SIGSTOP等信号。
19.设置当前进程状态 <linux/sched.h>
set_task_state( task, state);
set_current_state(state);
20.当一个程序执行了系统调用或出发了某个异常,它就陷入了内核空间,此时,我们称内核代表进程执行 并处于 进程上下文。
21.写时拷贝(copy on write)
内核先不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝,只有在需要写入数据的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。
22.copy_process()函数
①调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct。这些值和当前进程的值相同。
②检查并确保新创建的这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
③子进程着手于父进程区分开来,进程描述符内的许多成员都要被清零,或设置为初始值。
④子进程的状态被设置为TASK_UNINTERRUPTIBLE 以保证不会投入运行。
⑤copy_process() 调用copy_flags()以更新task_struct的flages成员。 PF_SUPERRIV标志清零,PF_FORKNOEXEC标志设置。
⑥调用alloc_pid()为新进程分配一个有效的pid。
⑦根据传递给clone() 的参数标志,处理资源。
⑧扫尾工作并返回一个指向子进程的指针。
23.内核线程与普通的进程间的区别:
在于内核线程没有独立的地址空间(mm指针被置为null),可被调度、抢占。
新的内核线程是通过kthread 内核进程衍生出来的。
<linux/kthread.h>
24.进程终结。 一般由do_exit()完成<linux/exit.c>
①将task_struct中的标志成员设置为PF_EXITING.
②调用 del_timer_sync() 删除任一内核定时器。根据返回的结果确保没有定时器在排队,也没有定时器处理程序在执行。
③如果BSD 的进程记账功能是开启的,do_exit()调用 acct_update_integrals() 来输出记账信息。
④然后调用exit_mm()函数释放进程占用的MM_starcut,如果没有别的进程使用他们(亦即,此地址空间没有被共享),就彻底释放他们。
⑤接下来调用 sem_exit()函数,如果进程排队等候ipc信号,它则离开队伍。
⑥调用exit_files() 和 exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
⑦接着把存放在task_struct 的exit_code 成员中的任务退出代码置为由exit() 提供的退出代码,或者去完成任何其他由内核机制规定的退出动作,退出代码存放在这里供父进程随时检索。
⑧调用 exit_notify() 向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者init进程,并把进程状态(存放在task_struct 结构的exit_state中)设为EXIT_ZOMBIE。
⑨do_exit() 调用 schedule() 切换到新的进程。因为处于EXIT_ZOMBIE状态的进程不会再调度,所以这是进程所执行的最后一段代码。do_exit()用不返回。
25.调用 do_exit()之后,尽管线程已经僵死不能再运行,但是系统还保留了它的进程描述符,这样可以让系统有办法在子进程终结后仍能获得它的信息,因此进程终结时所需的清理工作和进程描述符删除被分开执行。
在父进程获得已经终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
wait4()标准动作:
挂起调用它的进程,知道其中的一个子进程退出,此时函数返回该子进程的PID。
调用该函数时提供的指针会包含子函数退出代码。
26.释放进程描述符。 release_task()
①调用 _exit_signal(),该函数调用_unhash_process(),后者又调用detach_pid() 从pidhash上删除该进程,同时也要从任务列表中删除该进程。
②_exit_signal() 释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
③如果这个进程是线程组最后一个进程,并且领头线程已经死掉,那么 release_task() 就要通知僵死的领头进程的父进程。
④release_task() 调用 put_task_struct() 释放进程内核栈和 thread_info 结构所占的页,并释放 task_struct 所占的 slab 高速缓存。
自此,进程描述符和所有进程独享的资源就全部被释放掉了。
27.孤儿进程造成的进退维谷。
如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲。否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白白耗费内存。
解决方法就是给子进程在当前线程组内找一个线程作为父亲,如果不行就让init 做他们的父进程,在 do_exit() 中会调用exit_notify() ,该函数会调用forget_original_parent(),而后者会调用 find_new_reaper()来执行寻父过程。