《Linux内核设计与实现》 第三章学习笔记

时间:2023-03-09 16:09:24
《Linux内核设计与实现》 第三章学习笔记

一、进程

  1、进程就是处于执行期的程序(目标码存放在某种存储介质上)。但进程并不仅仅局限于一段可执行程序代码,通常进程还要包含其他资源。执行线程,简称线程(thread),是在进程中活动的对象。
  2、每个线程都拥有一个独立的程序计数器、进程技和一组进程寄存器。
  3、内核调度的对象是线程,而不是进程。对Linux 而言,线程是一种特殊的进程。
  4、在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。
  5、程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总称。
  6、Linux 系统中,这通常是调用fork()系统的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。在该调用结束时, 在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次: 一次回到父进程,另一次回到新产生的子进程。通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec。这组函数就可以创建新的地址空间,并把新的程序载入其中。最终,程序通过exi的系统调用退出执行。父进程可以通过wait4()系统调用查询子进程是否终结。
二、进程描述符及任务结构
  1、内核把进程的列表存放在叫做任务队列(task list) 的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构,该结构定义在<linux/sched.h> 文件中。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息。
  2、分配进程描述符《Linux内核设计与实现》 第三章学习笔记
  • 每个任务的thread_info 结构在色的内核栓的尾端分配。结构中task 域中存放的是指向该任务实际task_struct 的指针。
  • 进程描述符的存放内核通过一个唯一的进程标识值(process identification value)或PID 来标识每个进程。PID 是一个数,表示为pid_t 隐含类型,实际上就是一个int类型。
  • 在内核中,访问任务通常需要获得指向其task_struct 的指针,有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。

  3、进程状态进程描述符中的state域描述了进程的当前状态。该域的值也必为下列五种状态标志之:

《Linux内核设计与实现》 第三章学习笔记

  4、进程上下文当一个程序调执行了系统调用或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。

  5、进程家族树所有的进程都是PID 为1 的init 进程的后代。内核在系统启动的最后阶段启动init 进程。该进程读取系统的初始化脚本( initscript)并执行其他的相关程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程, 相应的,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。每个task_struct 都包含一个指向其父进程tast_struct、叫做parent 的指针,还包含一个称为children 的子进程链表。所以,对于当前进程, 可以通过下面的代码在得其父进程的进程描述符:structtaskstruct*my_parent = current- >parent;对于给定的进程,获取链表中的下一个进程:list_entry(task->tasks.next , struct task_ s truct, tasks)在取前一个进程的方撞与之相同:list_ entry(task->tasks. prev, s truct task_ struct, tasks)

三、进程创建Unix 

  1、采用了与众不同的实现方式,分解到两个单独的函数中去执行:fork() 和exec().首先, fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一)、PPID (父进程的进程号,子进程将其设置为被拷贝进程的PID )和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。3.3.1 写时拷贝传统的fork()系统调用直接把所有的资源复制给新创建的进程。Linux 的fork()使用写时拷贝( copy-on-write)页实现。

  2、只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。fork()的实际开销就是复制父进程的页表以反给予进程创建唯一的进程描述符。   

  3、fork()Linux 通过clone()系统调用实现fork() 。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork()、vfork()和clone()库函数都根据各自需要的参数去调用clone(),然后由clone()去调用do_fork()。do_fork 完成了创建中的大部分工作,它的定义在kemeVfork.c 文件中。该函数调用copy_process()函数,然后让进程开始运行。

《Linux内核设计与实现》 第三章学习笔记
《Linux内核设计与实现》 第三章学习笔记  4、vfork()除了不拷贝父进程的页表项外, vfork()系统调用和fork()的功能相同。子进程作为父进程的一个单独的钱程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。vfork()系统调用的实现是通过向clone()系统调用传递一个特殊标志来进行的。
  • 1)在调用copy_process()时, task_struct 的vfor_done 成员被设置为NULL 。
  • 2)在执行do_fork()时,如果给定特别标志,则vfork_done 会指向一个特定地址。
  • 3)子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过vfork_done 指针向它发送信号。
  • 4) 在调用mm_release()时,该函数用于进程退出内存地址空间, 并且检查vfork_done 是否为空,如果不为空,则会向父进程发送信号。
  • 5)回到do_fork(),父进程醒来并返回。
四、线程在Linux中的实现线程机制
  1、提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。
  2、创建线程线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源
  • clone(CLONE_VMclone_CLONE_FILBSCLONE_SIGHAND )
  • 递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类
《Linux内核设计与实现》 第三章学习笔记
  3、内核线程内核经常需要在后台执行一些操作。这种任务可以通过内核线程( kernel thread)完成一独立运行在内核空间的标准进程。
  4、内核线程和普通的进程的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm 指针被设置为NULL)。它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。
  5、内核钱程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop()的参数kthread_create()函数返回的task_struct 结构的地址:intkthread_stop(structtask_struct *k)
  6、进程终结不管进程是怎么终结的,该任务大部分都要靠do_exit()(定义于kemel/exit.c)来完成,它要做下面这些工作:
  • 1)将tast_struct 中的标志成员设置为PF_EXITING
  • 2)调用del_timer_ sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也设有定时器处理程序在运行。
  • 3)如果BSD 的进程记账功能是开启的, do_exit()调用acct_update_ integrals()来输出记账信息。
  • 4)然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。
  • 5) 接下来调用sem_ exit()函数。如果进程排队等候IPC 信号,它则离开队列。
  • 6)调用exit_files()和exit_fs(),以分别递藏文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
  • 7)接着把存放在task_struct 的exit_code 成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。
  • 8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为钱程组中的其他钱程或者为init 进程,并把进程状态(存放在task_struct 结构的exit_state 中〉设成EXIT_ZOMBIE 。
  • 9) do_exit()调用schedule()切换到新的进程。因为处EXIT_ZOMBIE 状态的进程不会再被调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。
  6、删除进程描述符进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程在得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct 结构才被释放。
  7、当最终需要释放进程描述符release_task()会被调用,用以完成以下工作:
  • 1)它调用一exit_signal(),该函数调用_unhash proc(),后者又调用detach_pid()从pidhash上删除该进程,同时也要从任务列表中删除该进程。
  • 2) _exit_sal()释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
  • 3)如果这个进程是钱程组最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程.的release_task()调用put_task_ struct()辑放进程内核和read_info 结构所占的页,并释放tast_struct 所占的slab 高速缓存。