深入理解内核阻塞与唤醒进程

进程和线程

进程

操作系统运行一个可执行程序,首先要把文件加载到内存,然后cpu读取和执行指令,一个进程就是一次程序的运行过程,内核给每个进程创建一tast_struct的数据结构。内核也是一段程序,系统启动时就被加载到内存。

虚拟内存

进程运行过程中要访问内存,物理内存有限,比如16GB,把有限的内存分配给不同进程使用,linux通过给每个进程虚拟出一块很大的地址空间,32位机器进程的虚拟内存空间是4GB,但是4GB并不是真实的物理内存,而是进程访问到哪个虚拟地址,如果这个地址还没有对应的物理内存页,会产生缺页中断,分配物理地址。

MMU(内存管理单元)将虚拟地址和物理内存页的映射关系保存在页表中,再次访问这个虚拟地址,就能找到相应的物理内存页。

进程的虚拟地址空间总体分为用户空间和内核空间,低地址的3GB属于用户空间,高地址的1GB属于内核空间。用户程序只能访问用户空间,内核程序可以访问整个进程空间,并且只有内核可以直接访问各种硬件资源,比如磁盘和网卡。 用户程序通过系统调用访问硬件资源,内核调用是内核实现的函数,比如应用程序通过网卡接收数据,调用socket的read函数。

cpu在系统调用过程会从用户态切换到内核态,cpu在用户态下执行用户程序,使用的是用户空间的栈,访问用户空间的内存;当cpu切换到内核态后,执行内核代码,使用的是内核空间上的栈。

用户空间从低到高,依次是代码区、数据区、堆、共享库与mmap内存映射区、栈、环境变量。堆向高地址增长,栈向低地址增长。

用户空间的共享库和mmap映射区,linux提供了内存映射函数mmap,可以将文件内容映射到这个内存区域,用户通过读写这段内存,实现对文件的读取和修改,无需通过read、write系统调用来读写文件,省去了用户空间和内核空间之间的数据拷贝。

Java中nio的MappedByteBuffer使用mmap实现的,用户程序用到的系统共享库也是通过mmap映射到这个区域的。

task_struct结构体,本身分配在内核空间,vm_struct成员变量保存了各内存区域的起始和结束地址,task_struct还保存了进程的其他信息,比如进程号、打开的文件、创建的socket以及cpu上下文等。

在linux中,线程是一个轻量级的进程,线程只是cpu的一个调度单元,因此线程有自己的task_struct结构体和运行栈区,但是线程的其他资源都是跟父进程公用的,比如虚拟空间、打开的文件和socket等。

名词总结

  • MMC: cpu内存管理单元
  • 物理内存: 内存条的内存空间
  • 虚拟内存: 使得程序认为自己拥有连续的可用的内存,实际上,通常是被分割成多个物理内存碎片,还有部分暂时存储在外部磁盘存储上。
  • 页面文件: 操作系统反映使用虚拟内存的硬盘空间大小而创建的文件。
  • 缺页中断: 用户程序访问已经映射在虚拟地址空间中,但未被加载到物理内存的一个分页时,由MMC发出的中断。

虚拟内存空间进行分页产生页(page),物理内存地址空间进行分页产生页帧(page frame),页和页帧大小一样。虚拟内存页的个数大于页帧的个数,通过页表,产生页号到页帧号的映射,映射虚拟内存页到物理内存页。 操作系统通过页面失效(page fault)功能,找到一个最少使用的页帧,使之失效,并把它写入磁盘,随后把要访问的页放到页帧中,并修改页表中的映射,保证了所有的页都会被调度。

阻塞和唤醒

linux内核将线程当做一个进程进行cpu调度,内核维护了一个可运行的进程队列,所有处于 TASK_RUNNING状态的进程都会被放入队列中,本质是用双向链表将tast_struct连接起来,排队使用cpu时间片,时间片用完重新调度cpu。

调度就是在可运行进程列表中选择一个进程,再次cpu列表中选择一个可用的cpu,将进程的上下文恢复到这个cpu的寄存器中,然后执行进程上下文指定的下一条指令。

上下文:某一时间点cpu寄存器和程序计数器的内容。

线程阻塞:

阻塞的本质就是将进程的task_struct移出运行队列,添加到等待队列,并且将进程的状态设置为task_uninterruptiable或者task_interruptible,重新触发一次cpu调度让出cpu。

线程唤醒:

线程在加入等待队列的同时,向内核注册了回调函数,通知内核在等待这个socket上的数据,如果数据来了就唤醒我。在网卡接收到数据时,产生硬件中断,内核再通过调用回调函数唤醒进程。

唤醒的过程是将进程的task_struct从等待队列中移到运行队列中,并将task_struct的状态设置为task_running,这样进程就有机会重新获取时间片。

这个过程,内核还将数据从内核空间拷贝到用户空间的堆上。

当read系统调用返回时,cpu又从内核态切换到用户态,继续执行read调用的下一行代码,并且能从用户空间上的buffer读到数据。

问题

  1. 既然用户态运行时也会占用cpu,内核态又可以访问整个虚拟空间,为什么不让cpu一直处在内核态,这样就没有切换来带的损耗?

如果是这样的话,你写的应用程序直接在内核态运行,权限级别太高了,出了问题会导致整个操作系统崩溃,所有才有了用户态,用户态是一种隔离和容错手段。

2.用户态切换到内核态,使用的是虚拟空间的内核地址?用户线程挂起执行内核线程,这是两个线程吗?

cpu处于内核态使用的是内核地址空间,用户线程的挂起其实就是内核完成的,具体来说就是系统调用触发软中断,cpu在内核态模式下执行软中断程序,也就是系统调用的具体实现函数,内核代码执行过程中,发现网络数据未就绪就主动让出cpu。这个时候才会将当前线程阻塞,从这个角度来看,是一个线程在不同cpu模式下的执行过程。

3.socket read系统调用过程。

cpu在用户态执行应用程序的代码,访问进行虚拟地址空间的用户空间,read系统调用时,cpu从用户态切换到内核态,执行内核代码,内核检测到socket上的数据未就绪,将进程的task_struct结构体从运行队列中移出到等待队列,并触发一次cpu调度,这是进程会让出cpu。当网卡数据到达时,内核数据从内核空间拷贝到用户空间的buffer,接着将进程的task_struct结构体重新移回运行队列,进程就有机会获得cpu时间片,系统调用返回,cpu从内核态切换到用户态,访问用户空间的数据。

- - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛

原文地址:深入理解内核阻塞与唤醒进程 - 文件系统 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛(版权归原作者所有,侵删)

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值