linux 内核设计与实现——进程管理

今天没怎么看文件系统相关的东西,准备大致浏览一下内核方面的知识:

看了linux内核设计与实现·第三版,陈莉君老师的译本。

第三章 进程管理

3.1 进程

linux中进线程的区别十分模糊。

进程是处于执行期的程序,进程不只包括程序的可执行代码,还包括程序运行需要的所有资源,以及一些执行线程。

每个线程有独立的PC,进程栈和一组进程寄存器。内核调度的对象是线程不是进程。

进程支持虚拟处理器和虚拟内存,虚拟处理器指的是进程虽共享处理器,但是进程之间认为自己独占处理器,虚拟内存类似。注意线程之间可以共享虚拟内存,但是各自拥有各自的虚拟处理器。(怎么理解)

通常在linux中进程的创建有系统调用fork()来执行,在调用结束时,父进程恢复执行,子进程开始执行,fork从内核返回两次,一次回到父进程,一次回到子进程。(看fork源码)

fork之后,通常使用exec函数组创建新的地址空间。

最后,程序通过exit退出执行,该函数终结进程并释放资源。进程退出后处于僵死状态,直到父进程调用wait或waitpid为止。

3.2 进程描述符和task结构

进程描述符数据类型:task_struct,包含进程的全部信息,是相当重要的结构。

Linux通过slab分配task_struct,目的是为了对象复用和缓存着色(第12章)。早期的task_struct存放在内核栈的尾端。(什么是内核栈),目前通过slab分配,只需要在内核栈底创建一个新的thread_info结构,指向对应的task_struct。

进程通过进程标识值PID标识每个进程,PID的最大值就是系统中允许同时存在的进程的最大数目。

为什么使用thread_info来指向task_struct呢?因为需要通过current宏快速得到当前正在运行的进程,在x86这种寄存器并不富裕的体系结构中,只能通过thread_info计算偏移间接查找task_struct结构。如果寄存器富裕,可以考虑将当前的task_struct放在一个寄存器中。

进程基本上有三种状态,运行态(TASK_RUNNING),就绪态(TASK_RUNNING)和等待态(TASK_INTERUPTIBLE和TASK_UNINTERUPTIBLE)。通过set_task_state调整进程状态

进程上下文指进程陷入内核空间后,此时内核相当与代表进程执行,且处于该进程的上下文中。进程上下文中系统代表进程执行,

进程家族树:linux中全部进程都是PID=1 init进程的后代,内核在系统启动最后阶段启动init进程,init读取系统的初始化脚本并执行其他相关程序,最终完成系统启动过程。拥有同一个父进程的子进程成为兄弟进程,也就意味着可以找到任意一个进程。for_each_process提供了遍历整个task_struct队列的能力。

3.3 创建进程

一般的过程是在新的地址空间创建进程,读入可执行文件,最后开始执行。unix中分为fork和exec两个过程。

写时拷贝:Linux的fork()使用写时拷贝实现,写时拷贝推迟了内核进程地址空间的复制,而是让父进程子进程共享一个拷贝。只有在需要写入的时候,数据才会被复制。所以此时fork的实际开销就是复制父进程的页表以及给子进程创建一个唯一的进程描述符。

fork等系统调用通过设置不同的标志调用clone,clone调用do_fork,do_fork调用copy_process。

fork系统调用:

  1. 调用dup_task_struct为新进程创建一个内核栈、thread_info以及一个task_struct,这些值与当前进程的值相同。
  2. 检查创建新进程后,用户拥有的进程数没有超过限制。
  3. 子进程初始化自己进程描述符中的一些成员。
  4. 状态设置为不可打断(TASK_UNINTERRUPTIBLE),确保不会再准备好之前投入运行。
  5. copy_process调用copy_flags更新task_struct的flags成员。
  6. 调用alloc_pid分配一个有效的PID。
  7. 根据传递给clone的参数标志,copy_process拷贝或者共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
  8. 最后copy_process做收尾工作并返回一个子进程的指针。

vfork系统调用:
vfork确保子进程先执行,父进程后执行。

vfork不会拷贝父进程的页表项,而是子进程直接使用父进程的页表项,除此之外与fork功能相同。父进程被阻塞,直到子进程退出(_exit而不是exit)或者执行exec。不推荐使用vfork。(为什么?)

  1. 调用copy_process时,task_struct的vfork_done成员被设置为NULL;
  2. 在执行do_fork时,如果给定特殊标志,则vfork_done会指向一个特殊地址;
  3. 此时父进程一直等待子进程通过vfork_done指针发送的信号;
  4. 调用mm_release时,进程退出内存地址空间,并检查vfork_done是否为空,如果不为空向父进程发送信号;
  5. 回到do_fork,父进程醒来并返回。

3.4 linux中的线程

linux中的线程和进程之间的区别并不是很大,因为linux的进程本身就很轻量,同时,linux把线程看做一种进程间共享资源的手段。

创建线程与创建进程类似,需要在创建时通过设置一些标志来指出共享的资源。

内核线程:
内核线程需要在后台执行一些操作。内核线程只在内核空间运行,从不切换到用户空间去,内核线程只能由内核线程创建,通过kthread_create实现。wake_up_process会唤醒新创建的内核线程,kthread_run调用了kthread_create和wake_up_process,内核线程启动后一直运行至调用do_exit()退出,或者调用kthread_stop停止其他内核线程。

3.5 进程终结

进程终结大部分要靠do_exit()。do_exit之后进程处于僵死状态,进程只占用内核栈、thread_info和task_struct空间,此时进程向父进程提供信息,父进程接收到信息后,此进程剩余的资源被释放,归还给系统使用。

posted @ 2020-12-25 16:04  xinze  阅读(166)  评论(0编辑  收藏  举报