- 进程=程序+资源,资源包括打开的文件、内核内部数据、CPU状态、挂起的信号
- Linux不特意区分线程和进程(二者都由task_struct结构体表示),线程是一种特殊的进程
现在操作系统中,进程提供两种虚拟机制:虚拟内存
和虚拟处理器
。
内核把进程的列表放到名为任务队列的双向循环链表中。链表中的节点类型为task_struct(进程描述符),包含了内核管理进程的所有信息。
Linux通过Slab分配器分配task_struct结构体,在内核栈尾部创建thread_info
结构体。
通过预先分配和使用task_struct,避免动态分配和释放的资源消耗。
task_struct状态,僵尸、孤儿进程
父进程:task_struct中包含名为parent、类型为task_struct的父进程
子进程:还有名为children的子进程链表
兄弟进程:父进程相同的进程被称为兄弟进程
总结:根据这个树形结构,可以从一个进程找到任意一个其他的进程
系统启动的最后阶段,会启动PID=1的init进程
,init进程会读取系统的初始化脚本完成系统启动。
Unix采用两个函数来创建进程:fork() 和exec()
fork() 通过拷贝当前进程创建子进程,子进程与父进程的不同在于:PID、PPID(父进程ID)和某些资源和统计量(例如挂起的信号)
Linux的fork() 使用写时拷贝
(copy-on-write)页实现。
fork() 的实际开销就是复制父进程的页表和创建子进程的task_struct(进程描述符),优点明显,避免了拷贝大量根本不需要的数据,加快了执行速度。
进程结束时要释放资源并告知父进程,进程的结束大概率是因为显式/隐式调用了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结构。 第八点说了会向父进程发送信息,然后父进程理睬的话就会释放该进程所有资源。
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高速缓存;
至此,进程描述符和进程所占有的资源都释放了。
Linux内核其实并不区分进程或线程,因为每个线程也是用task_struct表示,内核只把一组线程当做共享某些资源的普通进程,简单高效。而例如微软的操作系统有专门的线程机制。
虽然内核不区分,但接下来的讲解为方便理解,可以把进程当作一个资源的容器,线程才实际上执行任务。
线程的创建与进程类似,都是调用clone()
函数,只不过需要一些参数指定要共享的资源:
clone(CLONE_VM | CLONE_FS, 0),创建的进程和这个父进程就是所说的线程。参数有以下:
内核线程与普通线程的不同点在于:指向地址空间的mm指针为NULL,只在内核空间工作,不会切换到用户空间。