Linux IO 之 IO与网络模型
atomic 原子变量: x86在多核环境下,多核竞争数据总线时,提供Lock指令进行锁总线操作。保证“读-修改-写”的操作在芯片级的原子性。
spinlock 自旋锁: 自旋锁将当前线程不停地执行循环体,而不改变线程的运行状态,在CPU上实现忙等,以此保证响应速度更快。这种类型的线程数不断增加时,性能明显下降。所以自旋锁保护的临界区必须小,操作过程必须短。
semaphore 信号量: 信号量用于保护有限数量的临界资源,信号量在获取和释放时,通过自旋锁保护,当有中断会把中断保存到eflags寄存器,最后再恢复中断。
mutex 互斥锁: 为了控制同一时刻只有一个线程进入临界区,让无法进入临界区的线程休眠。
rw-lock 读写锁: 读写锁,把读操作和写操作分别进行加锁处理,减小了加锁粒度,优化了读大于写的场景。
preempt 抢占
- 时间片用完后调用schedule函数。
- 由于IO等原因自己主动调用schedule。
- 其他情况,当前进程被其他进程替换的时候。
per-cpu 变量
linux为解决cpu 各自使用的L2 cache 数据与内存中的不一致的问题。
RCU机制 (Read, Copy, Update)
用于解决多个CPU同时读写共享数据的场景。它允许多个CPU同时进行写操作,不使用锁,并且实现垃圾回收来处理旧数据。
内存屏障 memory-barrier
程序运行过程中,对内存访问不一定按照代码编写的顺序来进行。
- 编译器对代码进行优化。
- 多cpu架构存在指令乱序访问内存的可能。
I/O 与网络模型
介绍各种各样的I/O模型,包括以下场景:
- 阻塞 & 非阻塞
- 多路复用
- Signal IO
- 异步 IO
- libevent
现实生活中的场景复杂,Linux CPU和IO行为,他们之间互相等待。例如,阻塞的IO可能会让CPU暂停。
I/O模型很难说好与坏,只能说在某些场景下,更适合某些IO模型。其中,1、4 更适合块设备,2、3 更适用于字符设备。
为什么硬盘没有所谓的 多路复用,libevent,signal IO?
因为select(串口), epoll(socket) 这些都是在监听事件,所以各种各样的IO模型,更多是描述字符设备和网络socket的问题。但硬盘的文件,只有读写,没有 epoll这些。这些IO模型更多是在字符设备,网络socket的场景。
为什么程序要选择正确的IO模型?
蓝色代表:cpu,红色代表:io
如上图,某个应用打开一个图片文件,先需要100ms初始化,接下来100ms读这个图片。那打开这个图片就需要200ms。
但是 是否可以开两个线程,同时做这两件事?
如上图,网络收发程序,如果串行执行,CPU和IO会需要互相等待。
为什么CPU和IO可以并行?因为一般硬件,IO通过DMA,cpu消耗比较小,在硬件上操作的时间更长。CPU和硬盘是两个不同的硬件。
再比如开机加速中systemd使用的readahead功能:
第一次启动过程,读的文件,会通过Linux inotify监控linux内核文件被操作的情况,记录下来。第二次启动,后台有进程直接读这些文件,而不是等到需要的时候再读。
I/O模型会深刻影响应用的最终性能,阻塞 & 非阻塞 、异步 IO 是针对硬盘, 多路复用、signal io、libevent 是针对字符设备和 socket。
简单的IO模型
当一个进程需要读 键盘、触屏、鼠标时,进程会阻塞。但对于大量并发的场景,阻塞IO无法搞定,也可能会被信号打断。
内核程序等待IO,gobal fifo read不到
一般情况select返回,会调用 if signal_pending,进程会返回 ERESTARTSYS;此时,进程的read 返回由singal决定。有可能返回(EINTR),也有可能不返回。
demo:
|
|
一个阻塞的IO,在睡眠等IO时Ready,但中途被信号打断,linux响应信号,read/write请求阻塞。
配置信号时,在SA_FLAG是不是加“自动”,SA_RESTART指定 被阻塞的IO请求是否重发,并且应用中可以捕捉。加了SA_RESTART重发,就不会返回出错码EINTR。
没有加SA_RESTART重发,就会返回出错码(EINTR),这样可以检测read被信号打断时的返回。
但Linux中有一些系统调用,即便你加了自动重发,也不能自动重发。man signal.
当使用阻塞IO时,要小心这部分。
多进程、多线程模型
当有多个socket消息需要处理,阻塞IO搞不定,有一种可能是多个进程/线程,每当有一个连接建立(accept socket),都会启动一个线程去处理新建立的连接。但是,这种模型性能不太好,创建多进程、多线程时会有开销。
经典的C10K问题,意思是 在一台服务器上维护1w个连接,需要建立1w个进程或者线程。那么如果维护1亿用户在线,则需要1w台服务器。
IO多路复用,则是解决以上问题的场景。
总结:多进程、多线程模型企图把每一个fd放到不同的线程/进程处理,避免阻塞的问题,从而引入了进程创建\撤销,调度的开销。能否在一个线程内搞定所有IO? -- 这就是多路复用
的作用。
多路复用
异步IO
Linux中
不要把aio串起来,
基于epoll等api进行上层的封装,再基于事件编程。某个事件成立了,就开始去做某件事。
libevent
就像MFC一样,界面上的按钮,VC会产生一个on_button,调对应的函数。是一种典型的事件循环。
本质上还是用了epoll,只是基于事件编程。
点赞