Hello World的故事:
Hello World的一生是从execve()开始的
- 继承父进程的文件描述符(
./a.out > /dev/null
;./.a.out | cat
, …) - 内核会为
a.out
创建代码、数据、堆栈
- 继承父进程的文件描述符(
执行的第一条指令
- 从ELF的entry开始执行
- 静态链接:
a.out
的entry - 动态链接:
ld.so
的entry (动态链接器) - 动态链接libc:链接器使用一系列mmap把libc链接进进程地址空间
- 静态链接:
- 从ELF的entry开始执行
main() 执行之前
ld.so
会调用_init()
;之后会调用_start
,__libc_start_main
, …- 但都是普通的“用户代码”,libc也是一个普通的C程序
- 完成整个C runtime的初始化,其中可能调用系统调用
- 一个有趣的系统调用: ioctl, 判断是否是tty
main()的执行
- printf(…)
- 如果有缓冲区,写入缓冲区 (fork会复制缓冲区),否则直接用write写入
STDOUT_FILENO
- 如果缓冲区满足flush条件,则用write写入
- 如果有缓冲区,写入缓冲区 (fork会复制缓冲区),否则直接用write写入
- printf(…)
main执行结束后,libc代码依然会执行(exit()也进行这些操作)
- 调用
atexit()
注册的回调函数 - 清空缓冲区、释放资源
- 执行
_exit()
退出 - 如果结束前调用了_exit(), 则直接结束, 不进行上述操作
- 调用
进程: 操作系统视角
- 操作系统就是个中断处理程序
- 系统启动时完成初始化
- 然后等待中断到来(打开中断,死循环或
yield()
- 在中断返回时,精心设计一个进程的上下文(context)
- 在CR3寄存器中配置好虚拟内存的地址映射和权限
- 设置好寄存器的值:CS:EIP; SS:ESIP; …
- 执行
iret
让进程暂时占有CPU执行
- “进程”只是操作系统中的一些数据,操作系统代码维护了代表“进程”的对象,以及和进程相关的对象
- 进程上下文(寄存器的数值)
- 文件描述符(指向操作系统内对象的指针;文件访问的偏移量)
- 内存映射区域
- 进程的地址空间(页表、地址空间中的页面)
重新理解系统调用
操作系统为用户进程提供的一组API,通常在内核空间中实现,实现用户进程对操作系统对象/物理硬件访问的请求。
在刚才的视角上理解系统调用
- 进程 = 操作系统中的数据
- 系统调用 = 这些数据上的操作
- 例子:write()向某个操作系统的对象写入数据
- 例子:mmap()创建一个映射区域
文件访问的偏移量问题:
- 系统中所有以O_APPEND的文件描述符共享一个offset
- 每次单独的open都有一个独立的offset
- fork()后父子进程在复制的文件描述符上共享一个offset
操作系统与并发
- 操作系统中的对象是在处理器之间共享的
- 多处理器系统:原子性、顺序、可见性的丧失
- 系统调用执行需要协调系统中的各个部分
- 例子:read()管道时,需要等数据;write()管道时,需要等待管道的空位,否则阻塞
- 例子:read()终端时,需要等缓冲区中的数据;按下按键时,向缓冲区中写入数据
- 例子:使用DMA完成磁盘I/O,等待DMA中断的到来
- 操作系统中有大量的同步问题
- 条件变量
- 信号量