20135302魏静静——课本第3章学习笔记
第三章 进程管理
本章主要内容:
- 进程和线程
- 进程的任务结构
- 进程和线程的创建
- 进程的终止
1. 进程和线程
- 进程:进程就是处于执行期的程序,实际上,进程就是正在执行的程序代码的实时结果;
- 线程:执行线程,简称线程,是进程中活动的对象(每个线程拥有独立的程序计数器、进程栈、和一组进程寄存器),内核调度的对象是线程,不是进程
进程提供2种虚拟机制:虚拟处理器和虚拟内存
每个进程有独立的虚拟处理器和虚拟内存,
每个线程有独立的虚拟处理器,同一个进程内的线程有可能会共享虚拟内存。
内核中进程的信息主要保存在task_struct中(include/linux/sched.h)
进程标识PID和线程标识TID对于同一个进程或线程来说都是相等的。
fork()系统调用:
通过复制一个现有进程来创建一个全新的进程;接这调用exce()函数,可以创建新的地址空间,并把程序载入其中;最后,程序通过exit()系统调用退出调用。
这个函数会终结进程并将其占用的资源释放掉。
*fork()实际上是由clone()系统调用实现的
Linux中可以用ps命令查看所有进程的信息:
ps -eo pid,tid,ppid,comm
2. 进程的任务结构
下图为进程的数据结构简化图:
- 双向循环链表中,每一项都是类型为task_struct,称为进程描述符的结构;该结构定义在<sched.h>文件中
- 进程描述符中包含的数据能完整地描述一个正在执行的程序
- 每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际的task_struct的指针
- 进程描述符的PID的最大值实际就是系统中允许同时存在的进程的最大数目;
- 进程描述符中的state域描述了进程的当前状态;进程的各个状态之间的转化构成了进程的整个生命周期,进程必然处于五个状态的一种:
- task_running
- task_interruprtion
- task_uninterruption
- task_traced
- task_stopped
*调整某个进程的状态,这时使用set_task_state函数
- 进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过这些接口
系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或者多个子进程;拥有同一个父进程的所有进程被称为兄弟;进程间的关系存放在进程描述符中
3. 进程和线程的创建
- Linux中创建进程分2步:
fork: 通过拷贝当前进程创建一个子进程
exec: 读取可执行文件,将其载入到内存中运行
- 创建的流程:
- 调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程相同。
- check新进程(进程数目是否超出上限等)
- 清理新进程的信息(比如PID置0等),使之与父进程区别开。
- 新进程状态置为 TASK_UNINTERRUPTIBLE
- 更新task_struct的flags成员。
- 调用alloc_pid()为新进程分配一个有效的PID
- 根据clone()的参数标志,拷贝或共享相应的信息
- 做一些扫尾工作并返回新进程指针
- 创建进程的fork()函数实际上最终是调用clone()函数。
- 创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。
- linux通过clone()系统调用实现fork();do_fork完成创建中的大部分工作,定义在kernel/fork.c文件中
- 除了不拷贝父进程的页表项外,vfork()系统调用和fork功能相同;vfork()系统调用的实现是通过向clone()系统调用传递一个特殊标志来进行的
比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)
创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
- 创建线程:与普通进程的创建类似,只不过调用clone()时候需要传递一些参数标志来指明需要共享的资源,参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类
- 内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,它们只在内核空间进行,从来不切换到用户空间。内核进程和普通进程一样,可以被调度,也可以被抢占
- kthread内核进程通过clone()系统调用而创建。新的进程将运行threadfn函数,传递的参数为data
在内核中创建的内核线程与普通的进程之间还有个主要区别在于:内核线程没有独立的地址空间,它们只能在内核空间运行。
4. 进程的终止
和创建进程一样,终结一个进程同样有很多步骤:
- 子进程上的操作(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
- 切换到新进程继续执行
子进程进入EXIT_ZOMBIE之后,虽然永远不会被调度,关联的资源也释放掉了,但是它本身占用的内存还没有释放,
比如创建时分配的内核栈,task_struct结构等。这些由父进程来释放。
- 父进程上的操作(release_task)
父进程受到子进程发送的exit_notify()信号后,将该子进程的进程描述符和所有进程独享的资源全部删除。
-
除此之外的注意事项:
在调用do_exit()之后,尽管线程已经僵死不能再运行,但是系统还是保留了它的进程描述符。
wait()这一组函数通过唯一的一个系统调用wait4()来实现
linux如何存放(task_struct)和表示进程(thread_info);创建(fork()),实际上最终clone()
如果子进程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程。
find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程