OS:进程和线程的前世今生

本文探讨了进程作为执行中程序的实例,详细解析了进程的内存架构,包括段页式内存管理、虚拟内存如何实现一个程序独占处理器的假象。通过例子解释了从源代码到内存映像的过程,并介绍了内核、系统调用在创建和执行进程中的作用,以及进程通信的几种方式,如共享存储、管道通信和消息传递。
摘要由CSDN通过智能技术生成

导读:

想写这个玩意很久了,进程这个东西是一个很巧妙的东西,当你打开计算机时,他无时无刻不在,以前也研究了很多,在另外一篇博客上也写了许多的分支(管理,调度,状态,通信等等),所以这个玩意是很重要滴!(严肃脸)才有了今晚的进程文章(现在在凌晨四点ing,主要是白天在搞数据结构和算法,也没时间写这些),希望让大家和自己有一个更加清晰的认识咯!

参考书籍和视频:

  • 《深入计算机操作系统》
  • 《Linux内核设计与实现》

进程的经典定义就是一个执行中程序的实例,程序指的是一个可执行文件(.out,本篇文章是以Linux下文件为例),执行的意思是把该可执行文件放到内存中。

我们先来了解一下进程的架构。在OS上运行一个程序的时候,我们会得到一个这个程序是当前唯一运行的程序的假象,独占处理器和整块内存(除了内核内存区),处理器就好像无间断的执行我们的程序中的指令一样,这是为什么?

请大家移步操作系统内存管理,该篇博客能解决这个问题。

好了,我就当大家看完了这篇文章啦!那么,现在存在一个问题,既然OS是通过段页式的方式将可执行文件的指令放到内存中的,那么OS要怎么找到这些相对应的指令呢?可执行文件当然要夫唱妇随啦!既然你要我这么做,那我肯定做的好一点让你不那么麻烦,我自己把自己整理好再让你拿走。(不开车…)

当我们写完一段简单的代码

// hello.c
#include <stdio.h>
int main(int argc, char* argv, char** env[]) {
	printf("Hello World\n");
	return 0;
} 

编译源代码

gcc hello.c -o hello

他就会给我们生成相对应的段空间地址啦!其中里面有一个段(segment)映射(mapping)节(section)的表可以通过段页式内存管理映射到内存!使用如下命令可查看该可执行文件的信息(里面还有很多信息,比如符号表,动态链接库的位置等等),这些被称为进程地址空间

readelf -a executeFile
Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .plt.got .plt.sec .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .data .bss

段的地址我们也可以查看到(段的地址都是相对地址,即每一段都是从0地址开始的)

nm -C executeFile
0000000000004010 B __bss_start
0000000000004000 D __data_start
0000000000004000 W data_start
0000000000002000 R _IO_stdin_used
0000000000001149 T main
0000000000001060 T _start

一般有几个部分:代码段、数据段、未初始化数据段、堆、栈。被称为内存映像

那么回到原来的问题,“一个程序独占处理器”,这是因为虚拟内存,段空间分配的时候默认分配的是整个内存地址(为什么这么做?因为段通过页映射到内存上可以到任意位置!),映射到内存上会加上一个偏移地址从而获得具体的物理地址。

当然,运行一个程序远远不是内存分配那么简单。

因为内核!内核是什么?内核与系统调用

根据内存的特性,我们假设一个进程独占内存。

当用户向shell输入一个可执行文件的名字,OS就会使用fork()函数创建一个进程,该进程的ID是其父进程的ID,其页表和进程描述符也是其父进程的ID(现在fork有写时拷贝的功能,vfork也能节省一定的资源)然后向下执行exec()函数,该函数触发一个系统调用中断(一般是int0x80),内核遇到中断(这个中断会存放在中断栈中,中断栈是在用户态和内核态之间游离)便会将子进程带入内核(此时进程要切换进程上下文)接着该进程描述符的几乎信息(除了进程ID)都会被替换掉,内核也会把该进程的进程描述符(task_struct)放入PCB进程控制块中,然后分配前面说的内存区域(mmap或者其他方法进行内存映射<虚拟内存的内存映像—内存的进程分布>)。

除此之外,当一个进程上下文陷入系统调用,进程会把用户堆栈地址保存在内核态堆栈中,然后设置堆栈寄存器为内核栈地址,在内存中维护一个内核栈用于存放进程信息(当进程到用户态该栈为空),最后回到用户态,进程会再次切换进程上下文到用户态,此时进程便在内存中了。(该部分我后面会展开详细讲解)

当访问任务的时候通常需要获得其task_struct指针,实际上,内核大部分处理进程的代码都是直接通过task_struct进行的,因此就会在内核栈的尾端创建thread_info结构,通过计算偏移量间接的查找task_struct结构(2.6以前的内核各个进程的task_struct存放在他们内核栈的尾端)

系统中的每个程序都运行在某个进程的上下文中,上下文指的是由程序正确运行所需的状态组成的,这个状态包括放在内存中的程序的代码和数据,她的栈、通用目的寄存器的内存,程序计数器、环境变了以及打开文件描述集合。

进程从main主函数入口开始执行,如果遇到动态链接的过程会直接去寻找动态链接库(PST表和GOT表),遇到IO函数会接着执行系统调用,最后到这个进程执行完为止。

一个孤独进程的一生

strace executeFile

execve("./hello", ["./hello"], 0x7fff6fb3b330 /* 25 vars */) = 0
mmap(NULL, 27334, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5867556000
...
write(1, "Hello World\n", 12Hello World)           = 12
+++ exited with 0 +++

如果是一个孤独的进程自己运行倒是没有什么,但是我们经常要进行两个进程通信啊!比如用QQ给WX发文件这种,怎么办?这就涉及到进程通信啦!

我们首先要清楚一个概念:进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立,为了保证安全,一个进程不能直接访问另一个进程的地址空间。但是依旧有方法(进程通信):

  • 共享存储:分配共享空间:两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统提供的工具实现)。
  • 管道通信:“管道”是指用于连接读写进程的一个共享文件,又名pipe文件。其实就是在内存中开辟一个大小固定的缓冲区
  • 消息传递:进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。

当然这中间还涉及进程管理,进程调度,进程控制,进程状态的知识,这是后话了,我后面再补充,今晚先睡了~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值