OS期中复习

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链接进进程地址空间
  • main() 执行之前

    • ld.so会调用_init();之后会调用_start, __libc_start_main, …
      • 但都是普通的“用户代码”,libc也是一个普通的C程序
      • 完成整个C runtime的初始化,其中可能调用系统调用
        • 一个有趣的系统调用: ioctl, 判断是否是tty
  • main()的执行

    • printf(…)
      • 如果有缓冲区,写入缓冲区 (fork会复制缓冲区),否则直接用write写入STDOUT_FILENO
      • 如果缓冲区满足flush条件,则用write写入
  • 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中断的到来
  • 操作系统中有大量的同步问题
    • 条件变量
    • 信号量