操作系统
操作系统
运行模式
用户态
- 用户态是操作系统中较低权限的运行级别。
- 在用户态下运行的程序不能直接访问硬件资源,也不能执行某些特定的操作,如修改操作系统内核的内存区域。
- 用户态通常用于运行普通应用程序,这些程序对系统的控制受到限制。
内核态
- 内核态是操作系统中较高权限的运行级别。
- 在内核态下运行的程序可以直接访问所有硬件资源,执行系统调用,以及对操作系统内核进行操作。
- 内核态通常用于操作系统内核和某些关键的系统程序。
切换
内核态和用户态之间切换的意义
- 安全性:通过限制用户态程序的权限,可以防止恶意软件或错误操作对系统造成严重破坏。
- 稳定性:当用户态程序出现问题时,它不会直接影响到操作系统内核的稳定性,因为内核在更高的权限级别上运行。
- 资源管理:操作系统内核可以控制对硬件资源的访问,合理分配资源给不同的用户态程序。
- 多任务处理:操作系统通过在内核态进行任务调度,可以在多个用户态程序之间切换,实现多任务处理。
进程
进程、线程、协程
进程
- 资源分配最小单位
进程有自己独立的代码空间和内存空间,每个进程下的线程共享进程的内存空间 - 进程之间相互隔离
OS将物理内存地址分为多个虚拟地址,每个进程分配虚拟地址。并且物理地址是分开的做到了隔离,防止A进程写入到B进程。 - 父子进程的关系
- 父进程可以使用fork创建一个子进程,并且可以使用if(fork()==0)让子进程执行任务
- 父子进程的虚拟地址是不同的,但是实际的内存地址指向同一位置
- fork操作是cow的,这意味着如果没有写入的操作,那么读取的都是同一片地址,如果有写入操作,才会给子进程copy一份父进程的资源到新的内存地址
- 一般fork操作都会伴随着exec操作,exec操作会用新的程序替换当前的正文段、数据段、堆栈(也就是分配新的物理空间),并在执行完成后自动退出
- 除此外,父进程会给子进程copy一份文件描述符
线程
- 执行任务最小单位
- 线程之间共享内存地址
协程
- 线程内部有多个协程
- 特点
- 协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)
- 对于某些操作来说(比如yield),线程需要进行系统调用,但是协程不需要,减少了用户态内核态的切换开销
- 一个线程里可以运行多个协程,但本质是串行的
- 协程拥有自己的寄存器上下文和操作栈。
- 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和操作栈,
- 直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
进程通信
IPC
Inner Process Communication 进程间通信
为什么要进程间通信?
因为进程之间是相互隔离的,完成某些需要协作的任务就需要通信
进程通信方式
管道
本质是OS内核开辟的一段可以共享的内存
- 无名管道
- pipe(int[2])
- 没有名字的管道
- 下标0表示读端
- 特点
- 只能单向通信
- 也是一个文件,有文件描述符(读端和写端)
- pipe(int[2])
- 有名管道
- 通过mkfifo()调用 有名字的管道
- 由于有名字,所以可以使用open方式获得其文件描述符,所以可以用于非亲缘进程之间通信
- 但是存在一个问题,就是只能单向通信,如果要双向通信,需要用两个管道
- 不适合网状通信
共享内存
OS开辟的一大块内存
- 最快的通信方式,适合传递大量数据
- 共享内存和管道的区别
- 管道小,共享内存大
- 管道慢,共享内存块
- 管道自己会阻塞,但是共享内存需要我们自己处理
信号量
锁机制的一种方式,可以传输少量信息
信号
- 信号是一条小的消息,由内核或者其它进程生成并发送至目标进程,目标进程可以根据该信号来做出响应。
- 如何发送信号
- 键盘输入
- kill命令
- kill函数
- alarm函数
消息队列
- 就是一个双向链表
- 每一个结点有两个部分
- 消息编号
- 消息正文
- 可以很好的实现网状通信
Socket
网络通信
进程调度
调度准则
待补充…
调度算法
待补充…
内存管理
待补充…
异常
待补充…
文件描述符
结构
0,1,2分别表示标准输入、标准输出、标准错误
如何分配
扫描文件描述符表中,没有被使用的数值最小的下标,作为新打开文件的文件描述符
作用
屏蔽了底层的管道,IO,把他们全部当做文件来处理
fork + exec
- fork 操作会copy父进程的内存和文件描述符表
- exec执行操作回替换调用者的执行内存,并且执行完成后自行关闭
- 因此经常使用fork+exec进行操作
重定向
重定向的实现原理
为什么要有重定向?
- 分离输出:可以将程序的输出重定向到一个文件中,而不是显示到屏幕上,这对于日志记录和数据收集非常有用。
- 从文件中读取输入:可以将程序的输入重定向,使其从文件中读取,而不是从标准输入读取。
- 错误处理:可以将错误信息重定向到特定的文件或设备,以便于后续分析和调试。
- 管道:通过重定向,可以将一个程序的输出作为另一个程序的输入,这是Unix哲学中“组合简单命令行工具”的一部分。
- 控制I/O流向:在脚本或程序中,重定向可以用来控制I/O的流向,使得程序的行为更加灵活和可预测。
原理
比如 这样一个命令 cat < input.txt 将input.txt文件的内容作为cat 命令的参数执行
执行是这样的
1 | char *argv[2]; |
- 创建一个参数数组:
["cat", 0](此时的 0 代表着标准输入) fork创建了一个子进程,这个子进程会 copy 父进程的内存,即子进程也会拥有argv这个指针以及 0、1、2 的文件描述符- 当
fork()函数返回 0,代表if的执行体让子进程去执行 - 关闭了 0 号文件描述符,将会回收 0(标准输入)的资源;之后
open打开文件,这个文件描述符就是当前最小的一个数字,也就是刚刚回收的 0(此时的 0 代表着input.txt文件的 fd,注意父进程的 fd 表不会被改变) exec执行cat命令,会占用当前的内存,即会替换子进程的内存,去执行cat命令
管道
实现原理
基本概念
- 匿名管道(Anonymous Pipe):是最原始的管道形式,它通常是在创建子进程时由内核自动建立的。匿名管道只存在于内存中,没有与文件系统中的文件直接关联。
- 命名管道(Named Pipe):与匿名管道不同,命名管道存在于文件系统中,可以通过文件系统的路径进行访问。它们允许不相关的进程以先进先出(FIFO)的方式来通信。
用途
- 进程间通信(IPC):管道是进程间通信的一种方式,允许父子进程或不同进程组之间的数据传递。
- 命令行串联:在Unix和类Unix系统的shell中,可以使用管道符(
|)将多个命令串联起来,使得前一个命令的输出成为后一个命令的输入。 - 数据过滤:管道常用于数据过滤和文本处理,如使用
grep、sort等命令处理ls或其他命令的输出。
实现原理
fork + dup + exec
dup 命令是用于复制文件描述符的POSIX标准函数,它属于低级I/O操作的一部分。
1 | int p[2]; |
多路复用
IO多路复用是一种提高IO效率的技术,允许一个进程能够同时监控多个IO操作(如网络套接字、文件IO等),并在IO事件发生时及时进行处理,从而提高系统的性能和响应速度
主要作用
- 减少系统调用次数:IO多路复用通过一次系统调用监听多个IO事件,避免了频繁的系统调用,减少了内核态和用户态之间的切换开销,提高了系统的效率。
- 提高并发处理能力:通过IO多路复用,一个进程能够同时监听多个IO事件,只要有任何一个IO事件就绪,进程就能及时响应,从而实现并发处理多个IO操作的能力。
- 节省资源:相比于多线程或多进程模型,IO多路复用不需要创建多个线程或进程来处理不同的IO事件,节省了系统资源,提高了系统的稳定性和可靠性。
- 适用于高并发场景:在高并发的网络编程中,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表示一直轮询
缺点
- 数量只有1024
- 需要频繁的进行内核态和用户态的copy
- fd_set不能重用
- 需要遍历所有的fd_set
poll
结构体
1 | struct pollfd{ |
将事件分为普通数据、优先级带数据、高优先级数据三种
- 普通数据:正规的TCP数据,所有的UDP数据
- 优先级带数据(带内数据):TCP数据包中有额外参数的数据
- 高优先级数据(带外数据):传输一些紧急数据,比如Ctrl+c
存在的问题
- 需要频繁的进行内核态和用户态的copy
- 需要遍历所有的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 不会立即再次触发事件。你需要继续读取或写入数据,直到再次触发边缘触发事件。
- 边缘触发通常要求更谨慎的处理,但可以提供更高的性能,因为它避免了反复触发事件,直到状态发生变化
- 水平触发(默认)LT
epoll_wait
- 系统调用,陷入内核,阻塞等待事件发生
解决poll存在的问题
- 需要频繁的进行内核态和用户态的copy而epoll不需要,只需要copy一次
- 需要遍历所有的epollfd epoll的epoll_wait 函数有返回值,返回发生事件的fd数量