操作系统

操作系统

运行模式

用户态

  • 用户态是操作系统中较低权限的运行级别。
  • 在用户态下运行的程序不能直接访问硬件资源,也不能执行某些特定的操作,如修改操作系统内核的内存区域。
  • 用户态通常用于运行普通应用程序,这些程序对系统的控制受到限制。

内核态

  • 内核态是操作系统中较高权限的运行级别。
  • 在内核态下运行的程序可以直接访问所有硬件资源,执行系统调用,以及对操作系统内核进行操作。
  • 内核态通常用于操作系统内核和某些关键的系统程序。

切换

内核态和用户态之间切换的意义

  • 安全性:通过限制用户态程序的权限,可以防止恶意软件或错误操作对系统造成严重破坏。
  • 稳定性:当用户态程序出现问题时,它不会直接影响到操作系统内核的稳定性,因为内核在更高的权限级别上运行。
  • 资源管理:操作系统内核可以控制对硬件资源的访问,合理分配资源给不同的用户态程序。
  • 多任务处理:操作系统通过在内核态进行任务调度,可以在多个用户态程序之间切换,实现多任务处理。

进程

进程、线程、协程

进程

  • 资源分配最小单位
    进程有自己独立的代码空间和内存空间,每个进程下的线程共享进程的内存空间
  • 进程之间相互隔离
    OS将物理内存地址分为多个虚拟地址,每个进程分配虚拟地址。并且物理地址是分开的做到了隔离,防止A进程写入到B进程。
  • 父子进程的关系
    • 父进程可以使用fork创建一个子进程,并且可以使用if(fork()==0)让子进程执行任务
    • 父子进程的虚拟地址是不同的,但是实际的内存地址指向同一位置
    • fork操作是cow的,这意味着如果没有写入的操作,那么读取的都是同一片地址,如果有写入操作,才会给子进程copy一份父进程的资源到新的内存地址
    • 一般fork操作都会伴随着exec操作,exec操作会用新的程序替换当前的正文段、数据段、堆栈(也就是分配新的物理空间),并在执行完成后自动退出
    • 除此外,父进程会给子进程copy一份文件描述符

线程

  • 执行任务最小单位
  • 线程之间共享内存地址

协程

  • 线程内部有多个协程
  • 特点
    • 协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)
    • 对于某些操作来说(比如yield),线程需要进行系统调用,但是协程不需要,减少了用户态内核态的切换开销
    • 一个线程里可以运行多个协程,但本质是串行的
    • 协程拥有自己的寄存器上下文和操作栈。
      • 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和操作栈,
      • 直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

进程通信

IPC

Inner Process Communication 进程间通信

为什么要进程间通信?
因为进程之间是相互隔离的,完成某些需要协作的任务就需要通信

进程通信方式

管道

本质是OS内核开辟的一段可以共享的内存

  • 无名管道
    • pipe(int[2])
      • 没有名字的管道
      • 下标0表示读端
    • 特点
      • 只能单向通信
      • 也是一个文件,有文件描述符(读端和写端)
  • 有名管道
    • 通过mkfifo()调用 有名字的管道
    • 由于有名字,所以可以使用open方式获得其文件描述符,所以可以用于非亲缘进程之间通信
    • 但是存在一个问题,就是只能单向通信,如果要双向通信,需要用两个管道
    • 不适合网状通信
共享内存

OS开辟的一大块内存

  • 最快的通信方式,适合传递大量数据
  • 共享内存和管道的区别
    • 管道小,共享内存大
    • 管道慢,共享内存块
    • 管道自己会阻塞,但是共享内存需要我们自己处理
信号量

锁机制的一种方式,可以传输少量信息

信号
  • 信号是一条小的消息,由内核或者其它进程生成并发送至目标进程,目标进程可以根据该信号来做出响应。
  • 如何发送信号
    • 键盘输入
    • kill命令
    • kill函数
    • alarm函数
消息队列
  • 就是一个双向链表
  • 每一个结点有两个部分
    • 消息编号
    • 消息正文
  • 可以很好的实现网状通信
Socket

网络通信

进程调度

调度准则

待补充…

调度算法

待补充…

内存管理

待补充…

异常

待补充…

文件描述符

结构

0,1,2分别表示标准输入、标准输出、标准错误

如何分配

扫描文件描述符表中,没有被使用的数值最小的下标,作为新打开文件的文件描述符

作用

屏蔽了底层的管道,IO,把他们全部当做文件来处理

fork + exec

  • fork 操作会copy父进程的内存和文件描述符表
  • exec执行操作回替换调用者的执行内存,并且执行完成后自行关闭
  • 因此经常使用fork+exec进行操作

重定向

重定向的实现原理

为什么要有重定向?

  1. 分离输出:可以将程序的输出重定向到一个文件中,而不是显示到屏幕上,这对于日志记录和数据收集非常有用。
  2. 从文件中读取输入:可以将程序的输入重定向,使其从文件中读取,而不是从标准输入读取。
  3. 错误处理:可以将错误信息重定向到特定的文件或设备,以便于后续分析和调试。
  4. 管道:通过重定向,可以将一个程序的输出作为另一个程序的输入,这是Unix哲学中“组合简单命令行工具”的一部分。
  5. 控制I/O流向:在脚本或程序中,重定向可以用来控制I/O的流向,使得程序的行为更加灵活和可预测。

原理

比如 这样一个命令 cat < input.txt 将input.txt文件的内容作为cat 命令的参数执行

执行是这样的

1
2
3
4
5
6
7
8
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
close(0);
open("input.txt", O_RDONLY);
exec("cat", argv);
}
  1. 创建一个参数数组:["cat", 0](此时的 0 代表着标准输入)
  2. fork 创建了一个子进程,这个子进程会 copy 父进程的内存,即子进程也会拥有 argv 这个指针以及 0、1、2 的文件描述符
  3. fork() 函数返回 0,代表 if 的执行体让子进程去执行
  4. 关闭了 0 号文件描述符,将会回收 0(标准输入)的资源;之后 open 打开文件,这个文件描述符就是当前最小的一个数字,也就是刚刚回收的 0(此时的 0 代表着 input.txt 文件的 fd,注意父进程的 fd 表不会被改变)
  5. exec 执行 cat 命令,会占用当前的内存,即会替换子进程的内存,去执行 cat 命令

管道

实现原理

基本概念

  1. 匿名管道(Anonymous Pipe):是最原始的管道形式,它通常是在创建子进程时由内核自动建立的。匿名管道只存在于内存中,没有与文件系统中的文件直接关联。
  2. 命名管道(Named Pipe):与匿名管道不同,命名管道存在于文件系统中,可以通过文件系统的路径进行访问。它们允许不相关的进程以先进先出(FIFO)的方式来通信。

用途

  1. 进程间通信(IPC):管道是进程间通信的一种方式,允许父子进程或不同进程组之间的数据传递。
  2. 命令行串联:在Unix和类Unix系统的shell中,可以使用管道符(|)将多个命令串联起来,使得前一个命令的输出成为后一个命令的输入。
  3. 数据过滤:管道常用于数据过滤和文本处理,如使用grepsort等命令处理ls或其他命令的输出。

实现原理

fork + dup + exec

dup 命令是用于复制文件描述符的POSIX标准函数,它属于低级I/O操作的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int p[2];
char*argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
// 此时的fd:0、1、2、3、4
if(fork() == 0) {
// 子进程此时的fd:0、1、2、3、4
close(0);// 1、2、3、4
dup(p[0]);// 0、1、2、3、4 (此后的0表示管道的读端)
close(p[0]);//0、1、2、4
close(p[1]);//0、1、2
exec("/bin/wc", argv);
} else {
// 父进程此时的fd:0、1、2、3、4
close(p[0]); // 0、1、2、4
write(p[1], "hello world\n", 12); // 由写端写入数据 hello world
close(p[1]); // 0、1、2
}

多路复用

IO多路复用是一种提高IO效率的技术,允许一个进程能够同时监控多个IO操作(如网络套接字、文件IO等),并在IO事件发生时及时进行处理,从而提高系统的性能和响应速度

主要作用

  1. 减少系统调用次数:IO多路复用通过一次系统调用监听多个IO事件,避免了频繁的系统调用,减少了内核态和用户态之间的切换开销,提高了系统的效率。
  2. 提高并发处理能力:通过IO多路复用,一个进程能够同时监听多个IO事件,只要有任何一个IO事件就绪,进程就能及时响应,从而实现并发处理多个IO操作的能力。
  3. 节省资源:相比于多线程或多进程模型,IO多路复用不需要创建多个线程或进程来处理不同的IO事件,节省了系统资源,提高了系统的稳定性和可靠性。
  4. 适用于高并发场景:在高并发的网络编程中,IO多路复用能够更好地管理和处理大量的IO事件,提高系统的网络性能和吞吐量。

select

参数有五个

int select(int maxfdp1, fd_set*readset, fd_set*writeset, fd_set *exceptset, const struct timeval *timeout)

  • maxfdp1要轮询的文件描述符的个数
  • 中间三个都是set,分别表示读、写、异常,如果不关心,就可以设置为null,比如rset 是一个1024bit的bitmap,每一位对应一个文件描述符的监听位,如果有数据,这个监听位就会变成1
  • 最后一个参数是超时时间,设为null表示等待直到有一个文件描述符准备好,设置值表示超时时间,设置0表示一直轮询

缺点

  1. 数量只有1024
  2. 需要频繁的进行内核态和用户态的copy
  3. fd_set不能重用
  4. 需要遍历所有的fd_set

poll

结构体

1
2
3
4
5
struct pollfd{
int df; //要轮询的文件描述符fd
short events; //关心的fd事件:普通数据可读、优先级带数据可读等等
short revents; //fd上发生的事件
}

将事件分为普通数据、优先级带数据、高优先级数据三种

  • 普通数据:正规的TCP数据,所有的UDP数据
  • 优先级带数据(带内数据):TCP数据包中有额外参数的数据
  • 高优先级数据(带外数据):传输一些紧急数据,比如Ctrl+c

存在的问题

  1. 需要频繁的进行内核态和用户态的copy
  2. 需要遍历所有的pollfd

epoll

三个函数

epoll_create

创建epoll对象

  • 使用红黑树与双向链表的结构
    • 红黑树存放关心的文件描述符fd
    • 双向链表存放发生相关事件数据的fd
epoll_ctl
  • 添加要监听的fd,及关心的事件,此时就copy epoll对象到内核中
  • 触发方式有两种
    • 水平触发(默认)LT
      • 事情可以处理,也可以不处理
      • 同时支持 Blocking 和 No-Blocking
      • 只要文件描述符上还有数据可读或可写,就会触发事件。
      • 当你从 epoll_wait 返回后,如果文件描述符上还有数据可读或可写,下一次调用 epoll_wait 仍然会立即返回该文件描述符上的事件,直到你采取措施来处理这些事件,即使你没有读取或写入数据。
      • 水平触发适用于处理长时间处于可读或可写状态的连接或套接字。
    • 边缘触发 ET
      • 事件必须处理
      • 只支持No-Blocking
      • 减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高
      • 仅当文件描述符上的状态发生变化时(例如,从不可读到可读或从不可写到可写),epoll 才会触发事件。
      • 边缘触发要求你在处理事件时采取措施,以确保不丢失数据。一旦你处理了一次事件并且文件描述符仍然处于可读或可写状态,epoll 不会立即再次触发事件。你需要继续读取或写入数据,直到再次触发边缘触发事件。
      • 边缘触发通常要求更谨慎的处理,但可以提供更高的性能,因为它避免了反复触发事件,直到状态发生变化
epoll_wait
  • 系统调用,陷入内核,阻塞等待事件发生

解决poll存在的问题

  1. 需要频繁的进行内核态和用户态的copy而epoll不需要,只需要copy一次
  2. 需要遍历所有的epollfd epoll的epoll_wait 函数有返回值,返回发生事件的fd数量