全方位剖析 Linux 操作系统,太全了( 八 )


系统调用指令描述pause挂起信号nice改变分时进程的优先级ptrace进程跟踪kill向进程发送信号pipe创建管道mkfifo创建 fifo 的特殊文件(命名管道)sigaction设置对指定信号的处理方法msgctl消息控制操作semctl信号量控制
Linux 进程和线程的实现Linux 进程在 Linux 内核结构中,进程会被表示为 任务,通过结构体 structure 来创建 。不像其他的操作系统会区分进程、轻量级进程和线程,Linux 统一使用任务结构来代表执行上下文 。因此,对于每个单线程进程来说,单线程进程将用一个任务结构表示,对于多线程进程来说,将为每一个用户级线程分配一个任务结构 。Linux 内核是多线程的,并且内核级线程不与任何用户级线程相关联 。
对于每个进程来说,在内存中都会有一个 task_struct 进程描述符与之对应 。进程描述符包含了内核管理进程所有有用的信息,包括 调度参数、打开文件描述符等等 。进程描述符从进程创建开始就一直存在于内核堆栈中 。
Linux 和 Unix 一样,都是通过 PID 来区分不同的进程,内核会将所有进程的任务结构组成为一个双向链表 。PID 能够直接被映射称为进程的任务结构所在的地址,从而不需要遍历双向链表直接访问 。
我们上面提到了进程描述符,这是一个非常重要的概念,我们上面还提到了进程描述符是位于内存中的,这里我们省略了一句话,那就是进程描述符是存在用户的任务结构中,当进程位于内存并开始运行时,进程描述符才会被调入内存 。

进程位于内存被称为 PIM(Process In Memory) ,这是冯诺伊曼体系架构的一种体现,加载到内存中并执行的程序称为进程 。简单来说,一个进程就是正在执行的程序 。
进程描述符可以归为下面这几类
  • 调度参数(scheduling parameters):进程优先级、最近消耗 CPU 的时间、最近睡眠时间一起决定了下一个需要运行的进程
  • 内存映像(memory image):我们上面说到,进程映像是执行程序时所需要的可执行文件,它由数据和代码组成 。
  • 信号(signals):显示哪些信号被捕获、哪些信号被执行
  • 寄存器:当发生内核陷入 (trap) 时,寄存器的内容会被保存下来 。
  • 系统调用状态(system call state):当前系统调用的信息,包括参数和结果
  • 文件描述符表(file descriptor table):有关文件描述符的系统被调用时,文件描述符作为索引在文件描述符表中定位相关文件的 i-node 数据结构
  • 统计数据(accounting):记录用户、进程占用系统 CPU 时间表的指针,一些操作系统还保存进程最多占用的 CPU 时间、进程拥有的最大堆栈空间、进程可以消耗的页面数等 。
  • 内核堆栈(kernel stack):进程的内核部分可以使用的固定堆栈
  • 其他: 当前进程状态、事件等待时间、距离警报的超时时间、PID、父进程的 PID 以及用户标识符等
有了上面这些信息,现在就很容易描述在 Linux 中是如何创建这些进程的了,创建新流程实际上非常简单 。为子进程开辟一块新的用户空间的进程描述符,然后从父进程复制大量的内容 。为这个子进程分配一个 PID,设置其内存映射,赋予它访问父进程文件的权限,注册并启动 。
当执行 fork 系统调用时,调用进程会陷入内核并创建一些和任务相关的数据结构,比如内核堆栈(kernel stack) 和 thread_info 结构 。
关于 thread_info 结构可以参考
https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch_2avr32_2include_2asm_2thread__info_8h_source.html
这个结构中包含进程描述符,进程描述符位于固定的位置,使得 Linux 系统只需要很小的开销就可以定位到一个运行中进程的数据结构 。
进程描述符的主要内容是根据父进程的描述符来填充 。Linux 操作系统会寻找一个可用的 PID,并且此 PID 没有被任何进程使用,更新进程标示符使其指向一个新的数据结构即可 。为了减少 hash table 的碰撞,进程描述符会形成链表 。它还将 task_struct 的字段设置为指向任务数组上相应的上一个/下一个进程 。
task_struct : Linux 进程描述符,内部涉及到众多 C++ 源码,我们会在后面进行讲解 。
从原则上来说,为子进程开辟内存区域并为子进程分配数据段、堆栈段,并且对父进程的内容进行复制,但是实际上 fork 完成后,子进程和父进程没有共享内存,所以需要复制技术来实现同步,但是复制开销比较大,因此 Linux 操作系统使用了一种 欺骗 方式 。即为子进程分配页表,然后新分配的页表指向父进程的页面,同时这些页面是只读的 。当进程向这些页面进行写入的时候,会开启保护错误 。内核发现写入操作后,会为进程分配一个副本,使得写入时把数据复制到这个副本上,这个副本是共享的,这种方式称为 写入时复制(copy on write),这种方式避免了在同一块内存区域维护两个副本的必要,节省内存空间 。


推荐阅读