您的位置:首页 > 运维架构 > Linux

《Linux内核的设计与实现》读书笔记(三)---进程管理

2016-11-24 21:08 274 查看

第3章 进程管理

进程与线程

进程是出于执行期的程序。线程是进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈、和一组进程寄存器。(内核调度的对象是线程,不是进程)

进程提供两种虚拟机制:虚拟处理器和虚拟内存。同一进程中的线程之间可以共享虚拟内存。

进程与线程的区别

进程的存放和表示(task_struct和thread_info)

进程描述符(task_struct)

进程描述符(一个结构体)中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态等等。

1、分配进程描述符

Linux通过slab分配器分配task_struct结构,这样能达到对象的复用和缓存着色的目的(通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗)。

2.6以前的内核:各个进程的task_struct存放在他们内核栈的尾端。

2.6及2.6以后:用slab分配器动态生成task_struct,只需要在内核栈的尾端创建一个struct thread_info。

thread_info结构中有个指向该任务实际task_struct的指针。

2、进程描述符的存放

内核通过唯一的进程标识值或PID来标识每个进程。

内核把每个进程的PID存放在它们各自的进程描述符中

内核访问任务通常需要获得指向其task_struct的指针

通过current宏查找当前正在运行进程的进程描述符

3、进程状态

TASK_RUNNING(运行)—在用户空间中执行的唯一可能的状态(因为在用户进程看来,它是独享处理器的,就不存在像内核空间那样的就绪态和执行态的区别了)

TASK_INTERRUPTIBLE(可中断)—等待事件发生或接收信号

TASK_UNINTERRUPTIBLE(不可中断)—对信号不做响应外和可中断相同

TASK_TRACED(被跟踪)

TASK_STOPPED(停止)—没有投入运行也不能投入运行

4、设置当前进程的状态

内核调整某个进程的状态

set_task_state(task,state);    /*将任务task的状态设置为state*/


5、进程上下文

用户空间的进程陷入内核空间执行,称内核“代表进程执行”并处于进程上下文中。

系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行—对内核的所有访问都必须通过这些接口。

6、进程家族树

进程间存在明显的继承关系

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

进程间的关系存放在进程描述符中

每个进程描述符都有一个parent父进程指针和一个children的子进程链表

进程的创建(通过fork(),实际上最终是clone())

进程的创建分为两个步骤:fork()和exec().

fork():通过拷贝当前进程创建一个子进程

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

进程创建调用流程:

fork()/vfork()/_clone() -> clone() -> do_fork() -> copy_process()


copy_process()实现流程

调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程相同。

check新进程(进程数目是否超出上限等)

清理新进程的信息(比如PID置0等),使之与父进程区别开。

新进程状态置为 TASK_UNINTERRUPTIBLE

更新task_struct的flags成员。

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

根据clone()的参数标志,拷贝或共享相应的信息

做一些扫尾工作并返回新进程指针

写时拷贝

写时拷贝是一种可以推迟甚至免于拷贝数据的技术。Linux的fork()使用的就是写时拷贝页实现,此时内核并不复制地址空间,以只读的方式共享,资源的复制只有在需要写入(父进程或子进程需要写入)的时候才进行。

优势:一般的情况下,进程的创建马上就会运行一个可执行的文件,可以避免拷贝大量不会被使用的数据。

fork()

内核有意选择子进程首先执行(但是并非总能如此),因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能开始向地址空间写入。

vfork()

除了不拷贝父进程页表项外,vfork()系统调用和fork()的功能相同。

在进程创建的过程中,子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过vfork_done指针像它发送信号。

fork()和vfork()的区别

fork():子进程拷贝父进程的数据段和代码段

vfork():子进程与父进程共享数据段

fork():父子进程的执行次序不确定

vfork():保证子进程先运行,在调用exec()或exit()之前与父进程数据共享,调用之后父进程才被调度运行

vfork()保证子进程先运行,在调用exec()或exit()之前与父进程数据共享,调用之后父进程才被调度运行,如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁

线程在Linux中的实现

创建线程

创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。

比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)

创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES |CLONE_SIGHAND, 0)

内核线程

内核线程和普通线程的区别在于内核线程没有独立的地址空间,它们只运行在内核空间,同样可以被调度,被抢占。

进程终结

进程终结的流程

进程终结需要调用do_exit(),具体流程如下:

设置task_struct中的标识成员设置为PF_EXITING

调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行

调用exit_mm()释放进程占用的mm_struct

调用sem__exit(),使进程离开等待IPC信号的队列

调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源

把task_struct的exit_code设置为进程的返回值

调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE

切换到新进程继续执行

删除进程描述符

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

父进程收尸—wait()

父进程受到子进程发送的exit_notify()信号后,将该子进程的进程描述符和所有进程独享的资源全部删除。

孤儿进程

当父进程先于子进程结束,那么必须保证子进程能够找到新的父亲。

子进程在调用exit_notify()时已经考虑到了这点。

如果子进
4000
程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程。

find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  读书笔记