📄 index.html
字号:
CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_PID<font face="宋体">,</font>CLONE_VFORK<font face="宋体">等等标志位,任何一位被置</font>1<font face="宋体">了则表明创建的子进程和父进程共享该位对应的资源。所以在</font>vfork<font face="宋体">的实现中,</font>cloneflags
= CLONE_VFORK | CLONE_VM | SIGCHLD<font face="宋体">,这表示子进程和父进程共享地址空间,同时</font>do_fork<font face="宋体">会检查</font>CLONE_VFORK<font face="宋体">,如果该位被置</font>1<font face="宋体">了,子进程会把父进程的地址空间锁住,直到子进程退出或执行</font>exec<font face="宋体">时才释放该锁。</font></font>
<p><font face="宋体"><font size=+0> </font></font>
<p><font size=+0><font face="宋体"> 在讲述</font>clone<font face="宋体">系统调用前先简单介绍线程的一些概念。</font></font>
<p><font face="宋体"><font size=+0> 线程是在进程的基础上进一步的抽象,也就是说一个进程分为两个部分:线程集合和资源集合。线程是进程中的一个动态对象,它应该是一组独立的指令流,进程中的所有线程将共享进程里的资源。但是线程应该有自己的私有对象:比如程序计数器、堆栈和寄存器上下文。</font></font>
<p><font face="宋体"><font size=+0> 线程分为三种类型:</font></font>
<p><font face="宋体"><font size=+0> 内核线程、轻量级进程和用户线程。</font></font>
<p><b><font face="宋体"><font size=+0>内核线程:</font></font></b>
<p><font face="宋体"><font size=+0> 它的创建和撤消是由内核的内部需求来决定的,用来负责执行一个指定的函数,一个内核线程不需要</font>和<font size=+0>一个用户进程联系起来。它共享内核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度并且使用标准的内核同步机制,可以被单独的分配到一个处理器上运行。内核线程的调度由于不需要经过态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。</font></font>
<p><b><font face="宋体"><font size=+0>轻量级进程:</font></font></b>
<p><font size=+0><font face="宋体"> 轻量级进程是核心支持的用户线程,它在一个单独的进程中提供多线程控制。这些轻量级进程被单独的调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上,而且在它的生命周期这种绑定都是有效的。轻量级进程被独立调度并且共享地址空间和进程中的其它资源,但是每个</font>LWP<font face="宋体">都应该有自己的程序计数器、寄存器集合、核心栈和用户栈。</font></font>
<p><b><font face="宋体"><font size=+0>用户线程:</font></font></b>
<p><font face="宋体"><font size=+0> 用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。线程库提供了同步和调度的方法。这样进程可以使用大量的线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实现是可能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。每个用户线程都可以有自己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区。库通过保存当前线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换。</font></font>
<p><font face="宋体"><font size=+0> 内核仍然负责进程的切换,因为只有内核具有修改内存管理寄存器的权力。用户线程不是真正的调度实体,内核对它们一无所知,而只是调度用户线程下的进程或者轻量级进程,这些进程再通过线程库函数来调度它们的线程。当一个进程被抢占时,它的所有用户线程都被抢占,当一个用户线程被阻塞时,它会阻塞下面的轻量级进程,如果进程只有一个轻量级进程,则它的所有用户线程都会被阻塞。</font></font>
<p><font face="宋体"><font size=+0> </font></font>
<p><font size=+0><font face="宋体"> 明确了这些概念后,来讲述</font>Linux<font face="宋体">的线程和</font>clone<font face="宋体">系统调用。</font></font>
<p><font size=+0><font face="宋体"> 在许多实现了</font>MT<font face="宋体">的操作系统中(如:</font>Solaris<font face="宋体">,</font>Digital
Unix<font face="宋体">等), 线程和进程通过两种数据结构来抽象表示: 进程表项和线程表项,
一个进程表项可以指向若干个线程表项, 调度器在进程的时间片内再调度线程。
但是在</font>Linux<font face="宋体">中没有做这种区分, 而是统一使用</font>task_struct<font face="宋体">来管理所有进程</font>/<font face="宋体">线程,只是线程与线程之间的资源是共享的,这些资源可是是前面提到过的:虚存、文件系统、文件</font>I/O<font face="宋体">以及信号处理函数甚至</font>PID<font face="宋体">中的几种。</font></font>
<p><img SRC="a5.gif" height=221 width=447>
<br><font size=+0><font face="宋体"> 也就是说</font>Linux<font face="宋体">中,每个线程都有一个</font>task_struct<font face="宋体">,所以线程和进程可以使用同一调度器调度。其实</font>Linux<font face="宋体">核心中,轻量级进程和进程没有质上的差别,因为</font>Linux<font face="宋体">中进程的概念已经被抽象成了计算状态加资源的集合,这些资源在进程间可以共享。如果一个</font>task<font face="宋体">独占所有的资源,则是一个</font>HWP<font face="宋体">,如果一个</font>task<font face="宋体">和其它</font>task<font face="宋体">共享部分资源,则是</font>LWP<font face="宋体">。</font></font>
<p><font size=+0> clone<font face="宋体">系统调用就是一个创建轻量级进程的系统调用:</font></font>
<p><font size=+0> int clone(int (*fn)(void * arg), void
*stack, int flags, void * arg);</font>
<p><font size=+0><font face="宋体"> 其中</font>fn<font face="宋体">是轻量级进程所执行的过程,</font>stack<font face="宋体">是轻量级进程所使用的堆栈,</font>flags<font face="宋体">可以是前面提到的</font>CLONE_VM,
CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID<font face="宋体">的组合。</font>Clone
<font face="宋体">和</font>fork<font face="宋体">,</font>vfork<font face="宋体">在实现时都是调用核心函数</font>do_fork<font face="宋体">。</font></font>
<p><font size=+0> do_fork(unsigned long clone_flag, unsigned
long usp, struct pt_regs)<font face="宋体">;</font></font>
<p><font size=+0><font face="宋体"> 和</font>fork<font face="宋体">、</font>vfork<font face="宋体">不同的是,</font>fork<font face="宋体">时</font>clone_flag
= SIGCHLD<font face="宋体">;</font></font>
<p><font size=+0> vfork<font face="宋体">时</font>clone_flag
= CLONE_VM | CLONE_VFORK | SIGCHLD<font face="宋体">;</font></font>
<p><font size=+0><font face="宋体"> 而在</font>clone<font face="宋体">中,</font>clone_flag<font face="宋体">由用户给出。</font></font>
<p><font size=+0><font face="宋体"> 下面给出一个使用</font>clone<font face="宋体">的例子。</font></font>
<p><font size=+0> Void * func(int arg)</font>
<p><font size=+0> {</font>
<p><font size=+0> . . . . . .</font>
<p><font size=+0> }</font>
<p><font size=+0> int main()</font>
<p><font face="宋体"><font size=+0> {</font></font>
<p><font size=+0>int clone_flag, arg;</font>
<p><font size=+0>. . . . . .</font>
<p><font size=+0>clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS |</font>
<p><font size=+0>CLONE_FILES;</font>
<p><font size=+0>stack = (char *)malloc(STACK_FRAME);</font>
<p><font size=+0>stack += STACK_FRAME;</font>
<p><font size=+0>retval = clone((void *)func, stack, clone_flag, arg);</font>
<p><font size=+0>. . . . . .</font>
<p><font face="宋体"><font size=+0>}</font></font>
<p><font size=+0><font face="宋体"> 看起来</font>clone<font face="宋体">的用法和</font>pthread_create<font face="宋体">有些相似,两者的最根本的差别在于</font>clone<font face="宋体">是创建一个</font>LWP<font face="宋体">,对核心是可见的,由核心调度,而</font>pthread_create<font face="宋体">通常只是创建一个用户线程,对核心是不可见的,由线程库调度。</font></font>
<p><b><font face="宋体"><font size=+0> </font></font></b>
<p><b><font face="宋体"><font size=+0>Nanosleep & sleep</font></font></b>
<p><font size=+0><font face="宋体"> sleep和</font>nanosleep<font face="宋体">都是使进程睡眠一段时间后被唤醒,但是二者的实现完全不同。</font></font>
<p><font size=+0> Linux<font face="宋体">中并没有提供系统调用</font>sleep<font face="宋体">,</font>sleep<font face="宋体">是在库函数中实现的,它是通过调用</font>alarm<font face="宋体">来设定报警时间,调用</font>sigsuspend<font face="宋体">将进程挂起在信号</font>SIGALARM<font face="宋体">上,</font>sleep<font face="宋体">只能精确到秒级上。</font></font>
<p><font size=+0> nanosleep<font face="宋体">则是</font>Linux<font face="宋体">中的系统调用,它是使用定时器来实现的,该调用使调用进程睡眠,并往定时器队列上加入一个</font>time_list<font face="宋体">型定时器,</font>time_list<font face="宋体">结构里包括唤醒时间以及唤醒后执行的函数,通过</font>nanosleep<font face="宋体">加入的定时器的执行函数仅仅完成唤醒当前进程的功能。系统通过一定的机制定时检查这些队列(比如通过系统调用陷入核心后,从核心返回用户态前,要检查当前进程的时间片是否已经耗尽,如果是则调用</font>schedule()<font face="宋体">函数重新调度,该函数中就会检查定时器队列,另外慢中断返回前也会做此检查),如果定时时间已超过,则执行定时器指定的函数唤醒调用进程。当然,由于系统时间片可能丢失,所以</font>nanosleep<font face="宋体">精度也不是很高。</font></font>
<p><font size=+0> alarm<font face="宋体">也是通过定时器实现的,但是其精度只精确到秒级,另外,它设置的定时器执行函数是在指定时间向当前进程发送</font>SIGALRM<font face="宋体">信号。</font></font>
<p><font face="宋体"><font size=+0> </font></font>
<br><b><font face="宋体"><font size=+0>2.存储相关的系统调用</font></font></b></ol>
<dir><font size=+0>mmap<font face="宋体">:文件映射</font></font>
<p><font size=+0><font face="宋体"> 在讲述文件映射的概念时,不可避免的要牵涉到虚存(</font>SVR
4<font face="宋体">的</font>VM<font face="宋体">)。实际上,文件映射是虚存的中心概念,文件映射一方面给用户提供了一组措施,似的用户将文件映射到自己地址空间的某个部分,使用简单的内存访问指令读写文件;另一方面,它也可以用于内核的基本组织模式,在这种模式种,内核将整个地址空间视为诸如文件之类的一组不同对象的映射。</font></font>
<p><font size=+0> Unix<font face="宋体">中的传统文件访问方式是,首先用</font>open<font face="宋体">系统调用打开文件,然后使用</font>read<font face="宋体">,</font>write<font face="宋体">以及</font>lseek<font face="宋体">等调用进行顺序或者随即的</font>I/O<font face="宋体">。这种方式是非常低效的,每一次</font>I/O<font face="宋体">操作都需要一次系统调用。另外,如果若干个进程访问同一个文件,每个进程都要在自己的地址空间维护一个副本,浪费了内存空间。而如果能够通过一定的机制将页面映射到进程的地址空间中,也就是说首先通过简单的产生某些内存管理数据结构完成映射的创建。当进程访问页面时产生一个缺页中断,内核将页面读入内存并且更新页表指向该页面。而且这种方式非常方便于同一副本的共享。</font></font>
<p><font face="宋体"><font size=+0> 下面给出以上两种方式的对比图:</font></font>
<p><font face="宋体"><font size=+0> </font></font><img SRC="a6.gif" height=418 width=437>
<p><font size=+0> VM<font face="宋体">是面向对象的方法设计的,这里的对象是指内存对象:内存对象是一个软件抽象的概念,它描述内存区与后备存储之间的映射。系统可以使用多种类型的后备存储,比如交换空间,本地或者远程文件以及帧缓存等等。</font>VM<font face="宋体">系统对它们统一处理,采用同一操作集操作,比如读取页面或者回写页面等。每种不同的后备存储都可以用不同的方法实现这些操作。这样,系统定义了一套统一的接口,每种后备存储给出自己的实现方法。</font></font>
<p><font face="宋体"><font size=+0> 这样,进程的地址空间就被视为一组映射到不同数据对象上的的映射组成。所有的有效地址就是那些映射到数据对象上的地址。这些对象为映射它的页面提供了持久性的后备存储。映射使得用户可以直接寻址这些对象。</font></font>
<p><font size=+0><font face="宋体"> 值得提出的是,</font>VM<font face="宋体">体系结构独立于</font>Unix<font face="宋体">系统,所有的</font>Unix<font face="宋体">系统语义,如正文,数据及堆栈区都可以建构在基本</font>VM<font face="宋体">系统之上。同时,</font>VM<font face="宋体">体系结构也是独立于存储管理的,存储管理是由操作系统实施的,如:究竟采取什么样的对换和请求调页算法,究竟是采取分段还是分页机制进行存储管理,究竟是如何将虚拟地址转换成为物理地址等等(</font>Linux<font face="宋体">中是一种叫</font>Three
Level Page Table<font face="宋体">的机制),这些都与内存对象的概念无关。</font></font>
<p><font size=+0><font face="宋体"> 下面介绍</font>Linux<font face="宋体">中</font>VM<font face="宋体">的实现。</font></font>
<p><font size=+0><font face="宋体"> 如下图所示,一个进程应该包括一个</font>mm_struct<font face="宋体">(</font>memory
manage struct<font face="宋体">),该结构是进程虚拟地址空间的抽象描述,里面包括了进程虚拟空间的一些管理信息:</font>start_code,
end_code, start_data, end_data, start_brk, end_brk<font face="宋体">等等信息。另外,也有一个指向进程虚存区表</font>(vm_area_struct
<font face="宋体">:</font>virtual
memory area)<font face="宋体">的指针,该链是按照虚拟地址的增长顺序排列的。</font></font>
<p><img SRC="a7.gif" height=417 width=540>
<br><font size=+0><font face="宋体"> 在</font>Linux<font face="宋体">进程的地址空间被分作许多区(</font>vma<font face="宋体">),每个区(</font>vma<font face="宋体">)都对应虚拟地址空间上一段连续的区域,</font>vma<font face="宋体">是可以被共享和保护的独立实体,这里的</font>vma<font face="宋体">就是前面提到的内存对象。这里给出</font>vm_area_struct<font face="宋体">的结构,其中,前半部分是公共的,与类型无关的一些数据成员,如:指向</font>mm_struct<font face="宋体">的指针,地址范围等等,后半部分则是与类型相关的成员,其中最重要的是一个指向</font>vm_operation_struct<font face="宋体">向量表的指针</font>vm_ops<font face="宋体">,</font>vm_pos<font face="宋体">向量表是一组虚函数,定义了与</font>vma<font face="宋体">类型无关的接口。每一个特定的子类,即每种</font>vma<font face="宋体">类型都必须在向量表中实现这些操作。这里包括了:</font>open,
close, unmap, protect, sync, nopage, wppage, swapout<font face="宋体">这些操作。</font></font>
<p><font size=+0>struct vm_area_struct {</font>
<p><font size=+0>/*<font face="宋体">公共的,与</font>vma<font face="宋体">类型无关的</font>
*/</font>
<dir>
<dir><font size=+0>struct mm_struct * vm_mm;</font>
<p><font size=+0>unsigned long vm_start;</font>
<p><font size=+0>unsigned long vm_end;</font>
<p><font size=+0>struct vm_area_struct *vm_next;</font>
<p><font size=+0>pgprot_t vm_page_prot;</font>
<p><font size=+0>unsigned long vm_flags;</font>
<p><font size=+0>short vm_avl_height;</font>
<p><font size=+0>struct vm_area_struct * vm_avl_left;</font>
<p><font size=+0>struct vm_area_struct * vm_avl_right;</font>
<p><font size=+0>struct vm_area_struct *vm_next_share;</font>
<p><font size=+0>struct vm_area_struct **vm_pprev_share;</font>
<p><font size=+0>/* <font face="宋体">与类型相关的</font> */</font>
<p><font size=+0>struct vm_operations_struct * vm_ops;</font>
<p><font size=+0>unsigned long vm_pgoff;</font>
<p><font size=+0>struct file * vm_file;</font>
<p><font size=+0>unsigned long vm_raend<font face="宋体">;</font></font>
<p><font size=+0>void * vm_private_data;</font></dir>
</dir>
</dir>
<font size=+0><font face="宋体">}</font>;</font>
<p><font size=+0>vm_ops: open, close, no_page, swapin, swapout . . . .
. .</font>
<dir><font size=+0><font face="宋体"> 介绍完</font>VM<font face="宋体">的基本概念后,我们可以讲述</font>mmap,
munmap<font face="宋体">系统调用了。</font>mmap<font face="宋体">调用实际上就是一个内存对象</font>vma<font face="宋体">的创建过程,</font>mmap<font face="宋体">的调用格式是:</font></font>
<dir><font size=+0>void * mmap(void *start, size_t length, int prot , int
flags, int fd, off_t offset);</font></dir>
<font size=+0><font face="宋体">其中</font>start<font face="宋体">是映射地址,</font>length<font face="宋体">是映射长度,如果</font>flags<font face="宋体">的</font>MAP_FIXED<font face="宋体">不被置位,则该参数通常被忽略,而查找进程地址空间中第一个长度符合的空闲区域;</font>Fd<font face="宋体">是映射文件的文件句柄,</font>offset<font face="宋体">是映射文件中的偏移地址;</font>prot<font face="宋体">是映射保护权限,可以是</font>PROT_EXEC,
PROT_READ, PROT_WRITE, PROT_NONE<font face="宋体">,</font>flags<font face="宋体">则是指映射类型,可以是</font>MAP_FIXED,
MAP_PRIVATE, MAP_SHARED<font face="宋体">,该参数必须被指定为</font>MAP_PRIVATE<font face="宋体">和</font>MAP_SHARED<font face="宋体">其中之一,</font>MAP_PRIVATE<font face="宋体">是创建一个写时拷贝映射</font>(copy-on-write)<font face="宋体">,也就是说如果有多个进程同时映射到一个文件上,映射建立时只是共
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -