进程和线程
进程
操作系统运行一个可执行程序,首先要把文件加载到内存,然后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读到数据。
问题
- 既然用户态运行时也会占用cpu,内核态又可以访问整个虚拟空间,为什么不让cpu一直处在内核态,这样就没有切换来带的损耗?
如果是这样的话,你写的应用程序直接在内核态运行,权限级别太高了,出了问题会导致整个操作系统崩溃,所有才有了用户态,用户态是一种隔离和容错手段。
2.用户态切换到内核态,使用的是虚拟空间的内核地址?用户线程挂起执行内核线程,这是两个线程吗?
cpu处于内核态使用的是内核地址空间,用户线程的挂起其实就是内核完成的,具体来说就是系统调用触发软中断,cpu在内核态模式下执行软中断程序,也就是系统调用的具体实现函数,内核代码执行过程中,发现网络数据未就绪就主动让出cpu。这个时候才会将当前线程阻塞,从这个角度来看,是一个线程在不同cpu模式下的执行过程。
3.socket read系统调用过程。
cpu在用户态执行应用程序的代码,访问进行虚拟地址空间的用户空间,read系统调用时,cpu从用户态切换到内核态,执行内核代码,内核检测到socket上的数据未就绪,将进程的task_struct结构体从运行队列中移出到等待队列,并触发一次cpu调度,这是进程会让出cpu。当网卡数据到达时,内核数据从内核空间拷贝到用户空间的buffer,接着将进程的task_struct结构体重新移回运行队列,进程就有机会获得cpu时间片,系统调用返回,cpu从内核态切换到用户态,访问用户空间的数据。
- - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛
原文地址:深入理解内核阻塞与唤醒进程 - 文件系统 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛(版权归原作者所有,侵删)