第三章读书笔记

第三章 进程管理

进程

进程:处于执行期的程序(目标码存放在某种存储介质上)。

执行线程:线程(thread),进程中的活动对象。

在现代操作系统中,提供两种虚拟机制:

虚拟处理器、虚拟内存

程序本身不是进程,进程是处于执行期的程序以及相关的资源的总称。

进程在它创建时开始存活。

在Linuxxi系统中,创建新的进程都是为了立即执行新的,不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把心的程序载入其中。

最终,程序通过exit()系统调用退出执行,这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用来查询子进程的终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态,直到它的父进程调用wait()(返回关于终止进程的状态)或waitpid()为止。


进程描述符及任务结构

内核把进程的列表存放在叫做任务队列的双向循环列表之中。链表中的每一项都是类型为task_struct,称为进程描述符的结构(定义在linux/sched.h)文件中。进程描述符中包含一个具体进程的所有信息。

分配进程描述符

Linux通过slab分配器分配task_struct结构,以此达到对象复用和缓存着色的目的,只需在栈底或者栈顶创建一个新的结构struct thread_info
(x86上struct thread_info定义于<asm/thread_info.h>)

每个人物的thread_info结构在它的内核栈的尾端分配。

进程描述符的存放

内核通过一个唯一的进程标识值或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。

PID的最大值为32768,实际上就是系统中允许用时存在的进程做大数目。如果需要的话可以通过修改/proc/sys/kernelpid_max来提高上限

实际上,内核中大部分处理进程代码都是直接通过task_struct进行的。因此通过current宏查找进程速度很重要,它必须专门的硬件体系结构做处理。

进程状态

进程描述符中state域描述了进程的当前状态。系统中的每个进程都必然处于五个进程状态中的一种:

  • TASK_RUNNING(运行/可运行)
  • TASK_INTERRUPTIBLE(可中断):该进程正在休眠(阻塞中)
  • TASK_UNINTERRUPPTIBLE(不可中断):通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。
  • _TASK_TRACED(被跟踪的进程)
  • _TASK_STOPPED:通常发生在收到SIGSTOP,SIGSTP,SIGTTIN,SIGTTOU等信号的时候,此外在调试期间接收到任何的信号,都会使进程进入这种状态。

设置当前进程状态

调整某个进程的状态,最好使用set_task_state(task,state)函数(将新任务task设置为state)

该函数将指定的进程设置为指定的状态。必要的时候会设置内存屏障来强制其他处理器作重新排序。

*set_current_state(state)和set_task_state(current,state)含义是等同的。

在中断上下文中,系统不代表进程执行,而是执行一个中断处理程序。不会有进程去干扰这些中断处理程序,所以此时不存在进程上下文。

进程家族树

所有进程都是PID为1的init进程的后代。

拥有同一个父进程的所用进程被称为兄弟。

每个task_struct都包含一个指向其父进程stat_struct、叫做parent的指针,还包含一个称为children的子进程链表。

1)获得父进程的进程描述符:

struct task_struct *my_parent = current->parent

2)获取链表的下一个进程:

list_entry(task->tasks.next.struct task_struct,tasks)

3)获取链表的前一个进程:

list_entry(task->tasks.prev.struct task_struct,tasks)

init进程的进程描述符是作为init_task静态分配的。

实际上还有for_each_process(task)宏提供了依次访问整个任务队列的能力。


进程创建

一些其他系统所用的创建新进程的产生机制(新的地址空间里创建进程,读入可执行文件,最后开始执行),在Unix中被分为fork()和exec()。

fork()通过拷贝当前进程来创建一个子进程。子进程与父进程的区别只在于pid,ppid和某些资源和统计量。

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


写时拷贝

fork()使用写时拷贝来实现。内核此时并不复制整个进程空间,而是让父进程和子进程共享同一个拷贝。

资源复制只在需要写入的时候才进行

fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

inux用过clone()系统调用来实现fork。fork(),vfork(),clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()

do_fork定义在kernel/fork.c文件中。该函数调用copy_process()函数。

copy_process函数完成的工作:

1)调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct,这些值和当前进程相同。(子父描述符完全相同)。

2)检查确保新创建一个子进程后,当前用户拥有的进程数没有超过给它分配资源的限制。

3)子进程着手一些数据的修改,区别自己和父进程(主要是统计信息)。

4)子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证不会被投入运行。

5)copy_proess()调用copy_flags()以更新task_struct的flags成员。表明是否拥有超级用户权限的PF_SUPERPRIV标志被清零。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。

6)调用alloc_pid()为新进程分配一个有效的PID.

7)根据传递给clone()的参数标志,copy_process拷贝或共享打开的文件,文件系统信息,系统处理函数,进程地址空间和命名空间等。一般情况下这些进程会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。

8)最后copy_process做一些收尾工作并返回一个指向子进程的指针。

vfork()不拷贝父进程的页表项。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。

vfork()系统调用的实现是通过向一个clone()系统调用传递一个特殊标志来进行的。

1)在调用copy_process()时,task_struct的vfor_done成员被设置为NULL

2)在执行do_fork()时,如果给特定特别的标志,则vfork_done会指向一个特定地址。

3)子进程先开始执行之后,父进程不是马上恢复执行,而是一直等待,直到子进程通过vfork_done指针向它发送信号。

4)在调用mm_release()时,该函数用于进程退出内存地址空间,并检查vfork_done是否为空,如果不为空,则会向父进程发送信号。

5)回到do_fork(),父进程醒来并返回。

线程在Linux中的实现

Linux是个没有线程概念的家伙,将线程作为进程来看待。

创建线程与普通进程相似,但需一些参数标志来指明需要共享的资源。

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

新建的进程和它的父进程就是流行的所谓线程。

传递给clone()的参数决定了新创建进程的行为方式和父子进程之间共享的资源种类。 (在<linux/sched.h>)

参数列表在书的29页。

内核线程

内核线程和普通线程间的区别是内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)

使用

ps -ef

就可以看到线程啦

线程只能由其他内核线程创建。内核是通过从threadd内核进程中衍生所有新的内核线程自动处理这一点的。

从现有内核线程中创建新的内核线程的方法:

struct task_struct *kthread_create(int (*threadfn)(void *data)
							void *data,
							const char namefmt[],
							...)

新创建的进程处于不可运行的状态,如果不调用wake_uo_process()唤醒它,它不会主动运行。

创建一个进程并让它运行起来,可以通过调用thread_run()来达到。

struct task_struct *kthread_run(int (*threadfn)(void *data)
							void *data,
							const char namefmt[],
							...)

之后一直运行到调用do_exit()


进程的终结

不管进程是怎么终结,该任务大部分都要靠do_exit()(定义与kernel/exit.c)来完成,它会做以下工作:

1)将tast_ struct中标志成员设置为PX_EXITING

2)调用del_ timer_sysnc()删除任一一个内核定时器。

3)如果BSD的进程记账功能是开启的,do_ exit()调用acct_ update_integrals来输出记账信息。

4)调用exit_ mm()函数释放占用的mm_ struct.

5)调用sem_exit()函数,如果进程排队等IPC信号,它则离开队列。

6)调用exit_files()和exit_fs,以分别递减文件描述符,文件系统数据的引用计数。

7)接着存放在task_struct和exit_code成员中的任务退出设置为exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。

8)调用exit_notify()向父进程发送信号,给予进程重新找养父,养父为线程组中其他线程或者init进程并把进程状态设为EXIT_ZOMBIE.

9)do_exit调用schedule()切换到新进程。(do_exit永不返回)

此时进程状态存在唯一目的就是向它的父进程提供信息。父进程检索到信息后,或通知内核那是无关的信息后,由进程持有的剩余内存被释放,归还系统使用。


删除进程描述符

在调用do_exit之后,尽管线程已经僵死不能运行,但系统还是保留它的进程描述符。

进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。

wait()这一族函数都是通过唯一的一个系统调用wait4()来实现的。

wait4()的标准动作:

挂起调用它的进程,直到其中的一个子进程退出,此时函数返回该子进程的PID,此外调用该函数时提供的指针会包含子函数退出的退出代码。

当最终需要释放进程描述符时,release_task()会被调用。

1)它调用__exit-signal(),__exit-signal()调用_unhash_process(),后者又调用detah_pid()从pidhash上删除该进程,同时也要从任务列表中删除该进程。

2)释放目前僵死进程所使用的所有剩余资源。

3)如果这个进程是线程组的最后一个进程,且领头进程已死,那么release_task()就要通知僵死的领头进程的父进程。

4)release_task()调用put_task_struct()释放进程内核和thread_info结构所占的页,并释放tast_struct所占的slab高速缓存。

孤儿进程

如果父进程在子进程之前退出,必须有机制来保证能找到一个新父亲,否则孤儿进程就会退出时永远处于僵死状态,白白耗费内存。

——解决方法:给子进程在当前线程内找一个线程的父亲,如果不行就让init做它们的父进程。

在do_ exit中会调用exit_ notfiy(),该函数会调用forget_ original_ parent(),而后者会调用find_ new_ reaper()来执行寻找父进程

然后调用ptrace_ exit_ finish为ptraced的子进程寻找父亲。

在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程——用两个相对较小的链表减轻遍历带来的消耗。

一旦系统为进程成功地找到了和设置了新的父进程,就不会再出现驻留僵死进程的危险,init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。

原文地址:https://www.cnblogs.com/midori/p/5347569.html