《Linux内核设计与实现》CHAPTER3阅读梳理
【学习时间:3hours】
【学习内容:进程的描述;进程的生命周期(包括创建、终结)】
一、进程(任务)描述
1.进程是处于执行期的程序;除了可执行程序代码,还包括打开的文件、挂起的信号、内核内部数据、一个或者多个执行线程等多种资源
- 线程是在进程活动中的对象;内核调度的对象是线程而不是进程
- 在Linux系统中,并不区分线程和进程
- 可能存在两个或者多个进程执行的是同一个程序;甚至N个进程共享打开的文件、地址空间之类的资源
2.在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存
- 同一进程中的线程之间可以共享虚拟内存,但是每个都拥有自己的虚拟存储器
3.进程概述
- 新创建的进程调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中;
- 最终,程序通过exit()函数可以退出执行;进程退出执行之后就会变为僵尸进程,直到父进程调用wait()或waitpid()(返回关于终止进程的状态)
二、进程描述符
1.进程描述符
- 进程列表存放在任务队列(task list)这一双向链表中
- 链表的项是task_struct也就是进程描述符类型的结构体
- 该类型定义在<linux/sched.h>中
- task_struct包含了进程所需的所有资源
2.分配与存放
- 目的:Linux通过slab分配task_struct结构,以达到对象复用以及和缓存着色的目的(避免资源动态分配和释放带来的资源消耗)
- 分配:每个任务的堆栈尾端(比如,对于向上增长的堆栈来说,就是在堆栈的栈顶)有结构体thread_info,它指向了task_struct结构体
- 查找:
内核中的大部分处理处理进程的代码都是通过task_struct进行的;因此,需要通过current宏查找到当前正在运行进程的进程描述符
-
X86系统中,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移(通过current_thread_info函数)
movl $-8192, %eax
andl %esp,%eax
3.进程状态
- 进程在任何时刻,都必定处于五种状态中的一种
- TASK_RUNNING
- TASK_INTERRUPT
- TASK_UNINTERRUPT
- TASK_TRACED
- TASK_STOPPED
- 状态转换图
TASK_RUNNING:可能是正在运行,也可能表示可执行
TASK_INTERRUPT/TASK_UNINTERRUPT:都表示正在阻塞;然而后者表示的状态收到信号之后也不会被唤醒
- 设置进程当前状态
- 调用set_task_state(task,state)函数将进程设置为指定状态
- 进程上下文
- 可执行代码从一个可执行文件载入到进程的地址空间执行。当一个程序执行了系统调用,内核就会“代表进程执行”并处于进程上下文中
- 对比:在中断上下文中,系统不代表进程执行——不会有进程去干扰这些中断处理程序
- 进程继承关系
所有的进程都是PID为1的init进程的后代
-
对于给定的进程,获取链表中下一个进程:
list_entry(task->tasks.prev,struct task_struct,tasks)
二、进程创建
1.Unix系统的进程创建方式
- fork()通过拷贝当前进程创建一个子进程
- exec()负责读取可执行文件并将其载入地址空间开始运行
- 写时拷贝
- Linux的fork()使用写时拷贝推迟甚至免除拷贝。内核在创建新进程的时候并不复制整个地址空间,而是让父进程和子进程共享同一个拷贝;直到子进程/父进程需要写入的时候才进行拷贝
- 因而,fork的实际开销只是复制父进程的页表以及给子进程创建唯一的进程描述符
2.fork函数
- Linux通过clone系统调用实现fork
- 由clone去调用do_fork()
- 定义在<kernel/fork.c>中的do_fork()完成创建中的大部分工作,它调用copy_process函数,然后让进程开始运行
【我认为,以下部分与本周视频讲课内容结合起来会有更好的理解效果】
最后,copy_process返回的就是指向子进程的指针
3.线程的创建
在Linux系统中,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都有自己的task_struct
- 创建线程:与普通进程类似,只不过在调用clone()的时候需要传递一些参数标志来指明共享的资源
- 内核线程:与普通进程的区别只在于内核线程没有独立的地址空间。
- 它只能通过其他内核线程创建;内核通过kthread内核进程衍生所有的内核线程
- 新创建的线程处于不可运行状态,直到wake_up_process()明确地唤醒它
三、进程终结
1.终结进程
该任务大部分依赖于do_exit(),该函数永不返回
2.删除进程描述符
- 该任务是和清理工作分开进行的,因为这样在进程终结之后系统仍然可以获得它的信息
- 通过release_task()实现进程描述符的删除
- 至此,所有资源都被释放了
3.解决孤儿进程
- 概述:父进程在进程之前退出,就会遗留下子进程,也就是孤儿进程
- 解决方法:在当前的线程组内给孤儿进程寻找新的父进程;否则直接以init作为其父进程
- 调用顺序:do_exit()-->forget_original_parent()-->find_new_parent()-->ptrace_exit_finish()(这一函数是为被跟踪的进程寻找父进程,因为被跟踪的进程会以调试程序作为临时父亲)
疑问与自查
1.关于wait()与waitpid()函数?
参考http://no001.blog.51cto.com/1142339/493589
- wait函数的原型为:pid_t wait(int *status)
当进程退出时,它向父进程发送一个SIGCHLD信号,默认情况下总是忽略SIGCHLD信号,此时进程状态一直保留在内存中,直到父进程使用wait函数收集状态信息,才会清空这些信息; - 从本质上讲,系统调用waitpid是wait的封装,waitpid只是多出了两个可由用户控制的参数pid和options
2.什么是缓存着色?
参考http://blog.csdn.net/maray/article/details/3599845
主要就是起到区别CACHE LINE不同状态的作用
3.如何理解?
P23 进程管理-3.2.2
X86系统中,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移(通过current_thread_info函数)
参考http://blog.csdn.net/linwhwylb/article/details/6084461
因为esp和thread_info存在同一个块中,所以需要屏蔽掉esp来获得thread_info