文章目录
1 进程
- 进程=程序+资源,资源包括打开的文件、内核内部数据、CPU状态、挂起的信号
- Linux不特意区分线程和进程(二者都由task_struct结构体表示),线程是一种特殊的进程
1.1 两个虚拟化
现在操作系统中,进程提供两种虚拟机制:虚拟内存
和虚拟处理器
。
- 虚拟内存:实际上是使用硬盘补足进程的内存,一般电脑的内存为8G,但实际上一个进程可能就要用到多于8G的内存。还有,多个线程共享进程的虚拟内存。
- 虚拟处理器:给进程一种自己独占CPU的假象,但实际上是多个进程共用CPU。还有,每个线程都有自己的虚拟处理器(内核调度的是线程而非进程)。
1.2 任务队列
内核把进程的列表放到名为任务队列的双向循环链表中。链表中的节点类型为task_struct(进程描述符),包含了内核管理进程的所有信息。
Linux通过Slab分配器分配task_struct结构体,在内核栈尾部创建thread_info
结构体。
通过预先分配和使用task_struct,避免动态分配和释放的资源消耗。
1.3 task_struct
1.4 进程家族树
父进程:task_struct中包含名为parent、类型为task_struct的父进程
子进程:还有名为children的子进程链表
兄弟进程:父进程相同的进程被称为兄弟进程
总结:根据这个树形结构,可以从一个进程找到任意一个其他的进程
系统启动的最后阶段,会启动PID=1的init进程
,init进程会读取系统的初始化脚本完成系统启动。
1.5 进程创建
Unix采用两个函数来创建进程:fork() 和exec()
1.5.1 fork() 函数
fork() 通过拷贝当前进程创建子进程,子进程与父进程的不同在于:PID、PPID(父进程ID)和某些资源和统计量(例如挂起的信号)
Linux的fork() 使用写时拷贝
(copy-on-write)页实现。
- 内核此时并不复制整个进程空间,而是让父进程和子进程共享同一份拷贝;
- 需要写入时,进程才会复制数据,此前,进程只以只读权限共享数据;
fork() 的实际开销就是复制父进程的页表和创建子进程的task_struct(进程描述符),优点明显,避免了拷贝大量根本不需要的数据,加快了执行速度。
1.6 进程终结
进程结束时要释放资源并告知父进程,进程的结束大概率是因为显式/隐式调用了exit() 系统调用
1.6.1 do_exit()
exit()
函数可以显示调用,main() 最后也会隐式调用exit() ;但是,进程接收到无法处理也无法忽略的信号时,也可能被动退出。无论是主动还是被动,大部分都会调用do_exit()
函数执行进程终结,该函数步骤:
-
将task_struct(进程描述符)的标志成员设置为PF_EXITING;
-
调用del_timer_sync() 删除任意内核定时器。根据返回结果,他确保没有定时器在排队,也没有定时任务在运行;
-
如果BSD的进程记账功能是开启的,do_exit() 调用acct_update_integrals() 来输出记账信息;(进程记账好像是计算进程占用CPU时间之类的)
-
调用exit_mm() 释放进程占用的mm_struct,若没有别的进程使用(即没有被共享)就彻底释放;
-
调用em_exit() 。如果进程排队等待IPC信息(进程间通信),则让它离开该队列;
-
调用exit_files() 和exit_fs() ,代表文件描述符、文件系统数据的引用计数,如果降为0,则代表没有进程使用该资源,这时进程才能被释放;
-
把task_struct中的
exit_code
成员(退出代码)置为exit() 函数或其他。供父进程检索。 -
调用exit_notify() 向父进程发送信号,给该进程找养父,养父为线程组的其他线程或init进程,并把进程状态改为
EXIT_ZOMBIE
;(这段不懂的可以看看) -
调用schedule() 切换到新的进程。处于EXIT_ZOMBIE的进程(僵尸进程)不会被调度,这时进程的最后代码。do_exit() 永不返回。
至此,与该进程关联的所有资源(只被该进程使用)被释放。 进程无法被使用(也没有地址空间供它使用)且处于EXIT_ZOMBIE。这时该进程就是
僵尸进程
,唯一占用的资源就是内核栈、thread_info结构和task_struct结构。 第八点说了会向父进程发送信息,然后父进程理睬的话就会释放该进程所有资源。
1.6.2 删除进程描述符
do_exit()
执行完后,该进程状态为EXIT_ZOMBIE
(僵尸进程),同时父进程收到子进程结束信号,处理完后才会释放子进程的task_struct。
父进程接受信号的操作是调用wait()函数
:该函数会挂起当前进程,直到有子进程退出,返回退出子进程的PID。接下来,调用release_task()函数
执行删除进程描述符:
-
调用_exit_signal(),该函数调用_unhash_process() ,后者又调用detach_pid() 从pidhash删除该进程,同时也要从任务列表中删除该进程;
-
_exit_signal() 释放僵尸进程所使用的的所有剩余资源,并进行最终统计和记录;
-
如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task() 就要通知僵死的领头进程的父进程;
-
调用put_task_struct() 释放进程内核栈和thread_info结构所占有的页,并释放task_struct所占的slab高速缓存;
至此,进程描述符和进程所占有的资源都释放了。
2 线程
Linux内核其实并不区分进程或线程,因为每个线程也是用task_struct表示,内核只把一组线程当做共享某些资源的普通进程,简单高效。而例如微软的操作系统有专门的线程机制。
2.1 线程的创建
虽然内核不区分,但接下来的讲解为方便理解,可以把进程当作一个资源的容器,线程才实际上执行任务。
线程的创建与进程类似,都是调用clone()
函数,只不过需要一些参数指定要共享的资源:
clone(CLONE_VM | CLONE_FS, 0),创建的进程和这个父进程就是所说的线程。参数有以下:
2.2 内核线程
内核线程与普通线程的不同点在于:指向地址空间的mm指针为NULL,只在内核空间工作,不会切换到用户空间。