操作系统基础

操作系统

进程线程模型

对于进程和线程的理解和把握可以说基本奠定了对系统的认知和把控能力。==其核心意义绝不仅仅是“线程是调度的基本单位,进程是资源分配的基本单位”这么简单。==

多线程

特征:

  1. 一个进程下有多个线程
  2. 所有线程共享同一个进程的内存空间
  3. 由于线程的访问顺序不同,需要注意对临界变量的访问—> 产生冒险 相同的代码由于调度的顺序不同导致不同的结果

好处:

  1. 不同的线程可以独立的完成工作
  2. 线程的切换的效率要高很多

关键点:

  1. 线程之间有无先后访问的顺序 (线程的依赖关系)
  2. 线程共享访问同一变量(同步互斥问题)
进程

一个可执行文件(程序)的内容:

  • 二进制格式标识
  • 机器语言指令
  • 程序的入口地址
  • 数据
  • 符号表及重定位表
  • 共享库和动态链接信息
  • 其他信息:程序文件包含很多其他信息

进程内存布局

  • 进程的内存分配 P96

    1. 文本段
    2. 初始化数据段
    3. 未初始化数据段
    4. 栈-(为每个函数设置栈帧,包括函数的地址,变量)
    5. argv 和 environ
    6. 映射到内核的数据结构
  • 虚拟内存管理 P98

    1. 为什么可以用虚拟内存进行管理,局部性原理
      1. 时间局部性
      2. 空间局部性
    2. 内核为每一个进程维护一个页表
  • 栈和栈帧

    栈 为 每个调用的函数分配一个栈帧,其中包括

    • 函数实参和局部变量
    • 调用的链接信息 (CPU寄存器,程序计数器,下一条机器指令的地址)。当调用另一函数时,会在被调用的栈帧中保存这个寄存器的副本,以便函数返回时能为函数调用者将寄存器恢复。
  • 跳转和优化编译器的问题

    • 优化编译器会导致部分变量存储在CPU寄存器中,在执行跳转命令时,类似于(goto)会发生错误,volatile会避免这种优化
系统限制和选项
系统和进程信息
  • 进程信息:/proc/PID/
    • /proc/PID/fd 打开的文件描述符
    • /proc/PID/task 目录
      • /proc/PID/task/TID 为进程下的每个线程建立的一个目录
  • 系统信息:
文件I/O缓冲
  1. 为什么要有对文件I/O进行缓存?

    采用这样的操作主要是因为磁盘操作的速度太慢,这样不需要等待磁盘的操作。

  2. 控制文件I/O的内核缓存

    1. 设置为完整的同步IO – 直接同步到磁盘
    2. 绕过内核的高速缓存:直接I/O
文件系统
目录与链接
  1. 目录

    • i-node表中,会将目录记录为一种文件类型
    • 作为文件的目录,本质上是一个表格,包含文件名和i-node编号
  2. 硬链接

    定义: 在相同或者不同目录的文件,指向同一个i-node编号,这样就可以实现硬链接

    方法: ln abc xyz

    实质:在对应的目录文件中,多记录一个文件,指向同一个i-node ,i-node有一个字段记录了当前链接的目录的个数,只有当删除所有链接时,该文件的数据块才被删除

  3. 符号链接

    定义:一种特殊的文件类型,数据是另外一个文件的文件名

    方法: ln -s test.txt newtest.txt

    实质:在目录下新建了一个文件,数据为该文件的全路径文件名

  4. 硬链接和软链接的差别

    1. 硬链接由于必须指向同一个文件系统内的i-node,所以只能在同一文件系统中建立硬链接
    2. 不能为目录创建硬链接,容易形成环

信号

  1. 概念和概述

    • 信号是事件发生时对进程的通知机制,也叫软件中断。
    • 通常是,内核为进程产生信号:
      1. 硬件异常
      2. 特殊终端字符 (Ctrl-Z or Ctrl-C)
      3. 发生软件事件
    • 信号的分类:
      1. 标准信号:内核向进程通知事件,编号范围1-31
      2. 实时信号:

    // TO-DO

    // 待完善 执行

  2. 进程的创建

    1. pid_t fork()
      • 对父进程内存相应部分的完全复制
      • 父子进程的文件共享,得益于文件描述符记录表和系统级的打开文件表
      • 写时复制的内存语义,刚fork()的时候,内存都是只读状态,这样共享同样的物理内存页,当需要加载新的程序,或者修改对应的内存块时,会新建新的物理内存块,修改对应的虚拟地址对应的页。
      • vfork()
        • 无需为子进程虚拟内页或页表,和父进程共享内存,知道成功的执行exec()或调用了_exit()退出。
        • 在子进程调用exec()或是调用_exit()之前暂停执行父进程
        • 系统保障子进程先收到CPU的调度
      • fork() 后存在竞争状态,如果有同步的需要,可以用信号,或者进程间通信的方式,进行同步
    2. 进程终止exit()

      • _exit()
      • 分为正常关闭和异常关闭
      • 都会执行多个清理的步骤,包括文件相关,进程间通信相关、内存释放相关和Stdio缓存的刷新
    3. pid_t wait(&status)

      • 当存在未被等待返回的子进程时,会一直阻塞。

      • 当所有的子进程都已被等待返回,则会产生错误 等于 -1, 并且使 errno 为 ECHILD,可以用来等待所有子进程释放

      • 正常返回子进程的pid

      • waitpid() 指定对应的pid,进行等待,补充上述的系统调用的不足之处

      • 孤儿进程和僵尸进程

        • 由于父子进程的时间长度不同,必然会导致一些父进程会先于子进程退出,这样就会由众进程之祖(init)接管。

          这样的进程就是孤儿进程

        • 在父进程执行wait()之前,子进程就已经终止,系统任然允许父进程在之后执行wait()操作,来确定子进程是如何终止的。内核通过将子进程转化为僵尸进程来处理这种情况。子进程会释放大部分的内存,唯一保留的是,内核进程表中的一条记录。

    4. exec() 程序的执行

      将新程序加载到某一进程的内存。

      • 文件描述符和exec()
      • system()的实现
    5. 详述进程创建和程序执行

      • 进程记账 在进程终止时,写入一条记录信息
      • 系统调用clone(),也可以创建新的进程,对步骤的控制更加精准
      • exec()和fork()对进程属性的影响

线程

概述 包含线程的进程内存模型
  • 相对多进程的优势
    1. 多进程数据段和代码段都不复用、内存不共享
    2. fork()来创建子进程的代价比较高
互斥量
  1. 加锁和解锁
  2. 只有一个线程能够加锁,其他的会处于阻塞的状态
条件变量

允许一个线程就某个共享变量的状态通知其他线程,并让其他线程等待(堵塞于)这一通知

  1. 通知和等待条件变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    condition_variable notfull;
    condition_variable notempty;
    mutex lock;

    void deposit(int data){
    // 先获取共享变量的锁
    unique_lock<mutex> lk(lock);
    while(deq.size() == capacity){
    // 当不满足生产条件是,会将这个线程放进 等待状态的线程池,在放进去之前会释放获得的锁
    // 当被唤醒后,依然会检查是否可以生产
    not_full.wait(lk);
    }
    deq.push_back(data);
    lk.unlock();
    not_empty.notify_one();
    }

    notfull.wait(lk);
    notfull.

信号量

一个信号量是一个由内核维护的整数,其值被限制为大于或等于0。在信号量上可以执行各种系统调用:

  1. 将信号量设置为一个绝对值
  2. 当前值的基础上加上一个数量
  3. 当前值得基础上减去一个数量
  4. 等待信号量的值等于0

后面两个操作,可能会导致进程的阻塞。

信号量的系统调用:

  1. semget() 创建信号量
  2. semctl() 初始化信号量
  3. semop() 操作信号量
  4. semctl IPC_RMID 关闭信号量

二元信号量的操作:

  1. 预留p
  2. 释放v

Socket介绍

  1. fd = socket(domain, type, protocal)
    • Domain unix、ip_v4 、ip_v6
    • Type: 流 数据报 分别对应tcp协议和udp协议
流socket
  1. 主动socket (客户端 用connect()发起链接) 和被动socket( 服务器端用listen()将socket设置为被动,及接收请求)
  2. 监听接入连接:int listen(int sockfd, int backlog)
    • sockfd 指定了接收连接的文件socket
    • backlog指定了未决连接的数量,实际上是维护了一个待连接请求的队列。这是由于一个连接只能有一个当前的链接导致的,其他的链接只能阻塞。所以在这个限制内的链接请求会立即成功,因为还维护了一个已连接成功的队列。
  3. 接收连接:int accept(int sockfd, struct sockaddr addr, socklen_t addrlen)
    • 在fd引用的监听流socket上接收一个接入连接。如果没有未决连接就会阻塞
    • 会创建一个新的socket,并且正式这个新的socket会与执行connect()的对等socket进行连接。所以其返回的是已连接的socket的文件描述符。
  4. 连接到对等的socket: int connect(int sockfd, struct sockaddr, len) return 0 success -1 fail

  5. 流socket I/O

    • read() and write () 双向的通信
    • close() 关闭
数据报socket
  1. Socket(): 创建一个数据包socket
  2. Bind(): 允许另外的客户端发送数据报到这里,所以需要绑定一个唯一的地址
  3. Sendto(): 输入指定的地址,发送数据报到指定的地址
  4. Recvfrom(): 在接收数据报的同事,会获得客户端的地址,可以在需要的时候发一个响应,这在客户端为绑定唯一地址的情况下也是允许的。
  5. close()

TCP/IP 网络基础

  1. 物理层
  2. 数据链路层
    • 将IP层的数据报封装为帧进行传输
    • MTU 帧大小的上限
  3. 网络层
    • 本机到目标主机之间IP包的传输
    • IP传输数据报
      • 数据报的大小有上限
      • 这个上限至少有576字节 对于ipv4
    • IP是无连接和不可靠的
    • IP可能会对数据报进行分段
      • ipv4的最大的数据包大小为65535字节 + 40 字节头信息
      • 定义了路径MTU的概念
      • 分段对于客户端来说是透明的,收到后会进行重组
    • IP地址
  4. 传输层
    1. 端口号
      1. 特殊端口 1- 1023
      2. 普通端口
    2. 用户数据包协议
      1. 在数据包前只添加了端口号和一个数据校验和
      2. 无连接和不安全的
    3. 传输控制协议
      1. 流量控制
      2. 慢启动和拥塞控制
Internet Domain
  1. 网络字节序

    多字节整数的存储顺序

    • 大端模式 1234 先存12 再存34
    • 小端模式 1234 先存34 再存12
    • 小端架构是x86架构的机器
    • 其他主要的硬件架构的字节序都是大端架构
    • 网络字节序也是大端的
    • socket的地址结构中的整数,应该按网络字节序进行存储
  2. 数据表示

    对传输数据进行的编码

  3. 主机和服务转换函数

  4. 域名系统(DNS)
深入探讨TCP协议
  1. TCP报文的格式

    1. 源端口号 + 目标端口号 (16 + 16)
    2. 序列号(32)
    3. 确认号(32)
    4. 首部长度 + 保留位 + 控制位 + 窗口大小 (4 + 4 + 8 + 16)
    5. 数据校验和 + 紧急指针 (16 + 16)
    6. 选项
    7. 数据

    控制位:

    1. CWR 减小窗口大小
    2. ECE 拥塞控制回显
    3. URG 紧急包
    4. ACK 确认包标识
    5. PSH
    6. RST 重置连接
    7. SYN 同步信号标识
    8. FIN 完成发送任务
  2. TCP的序列号和确认机制

    确保面向连接和安全的

  3. TCP协议的状态机和状态迁移图

  4. shutdown()支持只关闭一端的通道,变成半双工

  5. TTL 生成时间字段

  6. 为什么TIME_WAIT的时间为2MSL

    1. 防止对方重发可以收到该重发的包
    2. 将上一个连接的包彻底清除
  7. 监控套接字的cmd

    netstat -a

  8. 使用tcpdump来监视TCP流量

其他备选的IO模型

应用大多数都需要2个方面的要求:

  1. 用非阻塞的方式去进行IO操作
  2. 同时检查多个文件描述符,也就是同时处理多个IO

这里有三个满足上述要求的选项:

  1. IO多路复用
  2. 信号驱动I/O
  3. epoll
IO多路复用
  1. int select(int nfds, fd_set readfds, fd_set wfds, fd_set *exceptfdts, struct timeval time_out)

    将自己需要控制的IO,放到对应的读事件,写事件,异常事件的集合中。

    FD_ZERO and FD_SET对集合进行初始化

    FD_ISSET检查是否就绪

  2. int poll(struct pollfd fds[], nfds_t fds_t, timeval timeout)

    fds[] 输入了我们想要监控的fd,以及对应请求的时间,返回中记录了就绪的事件

  3. select 和 poll的缺陷

    1. 内核必须检查所有被指定的文件描述符
    2. 在调用的时候。每次都要讲文件描述符的数据结构拷贝到内核
    3. 调用完成后还必须检查所有的文件描述符是否处于就绪态
信号驱动IO
  1. 为内核发送的通知信号安装一个信号处理函数
  2. 设定文件描述符的属主
  3. 使能非阻塞IO
  4. 使能信号驱动IO O_ASYNC
  5. 调用进程可以执行其他的任务
  6. 信号驱动IO使用的是边沿触发,当事件状态发成改变时,发送信号,中断当前的进程,执行信号处理函数
epoll

epoll由三个系统调用组成

  1. 系统调用epoll_create()创建了一个epoll实例,返回代表该实例的文件描述符
  2. epoll_ctl() 操作和epoll相关联的兴趣列表,增加,删除、修改新的文件描述符到列表中
  3. epoll_wait() 返回就绪列表中的成员