Skip to content

Commit a3e19a0

Browse files
authored
Create Linux IO 之 IO与网络模型.md
1 parent c5cb770 commit a3e19a0

File tree

1 file changed

+295
-0
lines changed

1 file changed

+295
-0
lines changed

Diff for: 文章/Linux IO 之 IO与网络模型.md

+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
## Linux内核针对不同并发场景的工具实现
2+
3+
![image](https://user-images.githubusercontent.com/87457873/127649604-761ead2e-8c72-4876-a52f-8310f3c3b49c.png)
4+
5+
### atomic 原子变量
6+
7+
x86在多核环境下,多核竞争数据总线时,提供Lock指令进行锁总线操作。保证“读-修改-写”的操作在芯片级的原子性。
8+
9+
### spinlock 自旋锁
10+
11+
自旋锁将当前线程不停地执行循环体,而不改变线程的运行状态,在CPU上实现忙等,以此保证响应速度更快。这种类型的线程数不断增加时,性能明显下降。所以自旋锁保护的临界区必须小,操作过程必须短。
12+
13+
### semaphore 信号量
14+
15+
信号量用于保护有限数量的临界资源,信号量在获取和释放时,通过自旋锁保护,当有中断会把中断保存到eflags寄存器,最后再恢复中断。
16+
17+
### mutex 互斥锁
18+
19+
为了控制同一时刻只有一个线程进入临界区,让无法进入临界区的线程休眠。
20+
21+
### rw-lock 读写锁
22+
23+
读写锁,把读操作和写操作分别进行加锁处理,减小了加锁粒度,优化了读大于写的场景。
24+
25+
### preempt 抢占
26+
27+
* 时间片用完后调用schedule函数。
28+
* 由于IO等原因自己主动调用schedule。
29+
* 其他情况,当前进程被其他进程替换的时候。
30+
31+
### per-cpu 变量
32+
33+
linux为解决cpu 各自使用的L2 cache 数据与内存中的不一致的问题。
34+
35+
### RCU机制 (Read, Copy, Update)
36+
37+
用于解决多个CPU同时读写共享数据的场景。它允许多个CPU同时进行写操作,不使用锁,并且实现垃圾回收来处理旧数据。
38+
39+
![image](https://user-images.githubusercontent.com/87457873/127649772-3121520b-8989-4176-a472-43719b64eb10.png)
40+
41+
### 内存屏障 memory-barrier
42+
43+
程序运行过程中,对内存访问不一定按照代码编写的顺序来进行。
44+
45+
* 编译器对代码进行优化。
46+
* 多cpu架构存在指令乱序访问内存的可能。
47+
48+
## I/O 与网络模型
49+
50+
介绍各种各样的I/O模型,包括以下场景:
51+
52+
* 阻塞 & 非阻塞
53+
* 多路复用
54+
* Signal IO
55+
* 异步 IO
56+
* libevent
57+
58+
现实生活中的场景复杂,Linux CPU和IO行为,他们之间互相等待。例如,阻塞的IO可能会让CPU暂停。
59+
60+
I/O模型很难说好与坏,只能说在某些场景下,更适合某些IO模型。其中,1、4 更适合块设备,2、3 更适用于字符设备。
61+
62+
为什么硬盘没有所谓的 多路复用,libevent,signal IO?
63+
64+
> 因为select(串口), epoll(socket) 这些都是在监听事件,所以各种各样的IO模型,更多是描述字符设备和网络socket的问题。但硬盘的文件,只有读写,没有 epoll这些。
65+
> 这些IO模型更多是在字符设备,网络socket的场景。
66+
67+
### 为什么程序要选择正确的IO模型?
68+
69+
蓝色代表:cpu,红色代表:io
70+
71+
![image](https://user-images.githubusercontent.com/87457873/127649947-6c89dd89-5076-4aca-af0a-69f154cf26b4.png)
72+
73+
如上图,某个应用打开一个图片文件,先需要100ms初始化,接下来100ms读这个图片。那打开这个图片就需要200ms。
74+
75+
但是 是否可以开两个线程,同时做这两件事?
76+
77+
![image](https://user-images.githubusercontent.com/87457873/127649983-79730c12-91e9-462f-8abf-5fbf9634bd08.png)
78+
79+
如上图,网络收发程序,如果串行执行,CPU和IO会需要互相等待。<br>
80+
为什么CPU和IO可以并行?因为一般硬件,IO通过DMA,cpu消耗比较小,在硬件上操作的时间更长。CPU和硬盘是两个不同的硬件。
81+
82+
再比如开机加速中systemd使用的readahead功能:<br>
83+
第一次启动过程,读的文件,会通过Linux inotify监控linux内核文件被操作的情况,记录下来。第二次启动,后台有进程直接读这些文件,而不是等到需要的时候再读。
84+
85+
![image](https://user-images.githubusercontent.com/87457873/127650021-0bed9b00-4448-4c7a-b9a4-ccff573a50ea.png)
86+
87+
I/O模型会深刻影响应用的最终性能,阻塞 & 非阻塞 、异步 IO 是针对硬盘, 多路复用、signal io、libevent 是针对字符设备和 socket。
88+
89+
### 简单的IO模型
90+
91+
![image](https://user-images.githubusercontent.com/87457873/127650047-8bb138e2-f652-4115-8ea6-0498b3e1be1a.png)
92+
93+
当一个进程需要读 键盘、触屏、鼠标时,进程会阻塞。但对于大量并发的场景,阻塞IO无法搞定,也可能会被信号打断。
94+
95+
内核程序等待IO,gobal fifo read不到
96+
97+
一般情况select返回,会调用 if signal_pending,进程会返回 ERESTARTSYS;此时,进程的read 返回由singal决定。有可能返回(EINTR),也有可能不返回。
98+
99+
#### demo:
100+
101+
```c
102+
#include <stdio.h>
103+
#include <stdlib.h>
104+
#include <unistd.h>
105+
#include <signal.h>
106+
#include <sys/types.h>
107+
#include <errno.h>
108+
#include <string.h>
109+
110+
static void sig_handler(int signum)
111+
{
112+
printf("int handler %d\n", signum);
113+
}
114+
115+
int main(int argc, char **argv)
116+
{
117+
char buf[100];
118+
ssize_t ret;
119+
struct sigaction oldact;
120+
struct sigaction act;
121+
122+
act.sa_handler = sig_handler;
123+
act.sa_flags = 0;
124+
// act.sa_flags |= SA_RESTART;
125+
sigemptyset(&act.sa_mask);
126+
if (-1 == sigaction(SIGUSR1, &act, &oldact)) {
127+
printf("sigaction failed!/n");
128+
return -1;
129+
}
130+
131+
bzero(buf, 100);
132+
do {
133+
ret = read(STDIN_FILENO, buf, 10);
134+
if ((ret == -1) && (errno == EINTR))
135+
printf("retry after eintr\n");
136+
} while((ret == -1) && (errno == EINTR));
137+
138+
if (ret > 0)
139+
printf("read %d bytes, content is %s\n", ret, buf);
140+
return 0;
141+
}
142+
```
143+
144+
![image](https://user-images.githubusercontent.com/87457873/127650094-8544cd30-d8de-4773-8a5c-b5d865115dce.png)
145+
146+
一个阻塞的IO,在睡眠等IO时Ready,但中途被信号打断,linux响应信号,read/write请求阻塞。<br>
147+
配置信号时,在SA_FLAG是不是加“自动”,SA_RESTART指定 被阻塞的IO请求是否重发,并且应用中可以捕捉。加了SA_RESTART重发,就不会返回出错码EINTR。<br>
148+
没有加SA_RESTART重发,就会返回出错码(EINTR),这样可以检测read被信号打断时的返回。<br>
149+
150+
但Linux中有一些系统调用,即便你加了自动重发,也不能自动重发。man signal.
151+
152+
![image](https://user-images.githubusercontent.com/87457873/127650145-bcad6c25-0a99-4158-af47-7ead22941fb2.png)
153+
154+
当使用阻塞IO时,要小心这部分。
155+
156+
![image](https://user-images.githubusercontent.com/87457873/127650167-86e622e8-4e04-437d-9e29-aa3cacc78968.png)
157+
158+
### 多进程、多线程模型
159+
160+
当有多个socket消息需要处理,阻塞IO搞不定,有一种可能是多个进程/线程,每当有一个连接建立(accept socket),都会启动一个线程去处理新建立的连接。但是,这种模型性能不太好,创建多进程、多线程时会有开销。
161+
162+
经典的C10K问题,意思是 在一台服务器上维护1w个连接,需要建立1w个进程或者线程。那么如果维护1亿用户在线,则需要1w台服务器。
163+
164+
IO多路复用,则是解决以上问题的场景。
165+
166+
总结:多进程、多线程模型企图把每一个fd放到不同的线程/进程处理,避免阻塞的问题,从而引入了进程创建\撤销,调度的开销。能否在一个线程内搞定所有IO? -- 这就是多路复用的作用。
167+
168+
### 多路复用
169+
170+
![image](https://user-images.githubusercontent.com/87457873/127650193-d91a7751-d9be-43e4-ace1-d153f18d82e7.png)
171+
172+
#### select
173+
174+
select:效率低,性能不太好。不能解决大量并发请求的问题。
175+
176+
它把1000个fd加入到fd_set(文件描述符集合),通过select监控fd_set里的fd是否有变化。如果有一个fd满足读写事件,就会依次查看每个文件描述符,那些发生变化的描述符在fd_set对应位设为1,表示socket可读或者可写。
177+
178+
Select通过轮询的方式监听,对监听的FD数量 t通过FD_SETSIZE限制。
179+
180+
两个问题:
181+
182+
1、select初始化时,要告诉内核,关注1000个fd, 每次初始化都需要重新关注1000个fd。前期准备阶段长。<br>
183+
2、select返回之后,要扫描1000个fd。 后期扫描维护成本大,CPU开销大。
184+
185+
#### epoll
186+
187+
epoll :在内核中的实现不是通过轮询的方式,而是通过注册callback函数的方式。当某个文件描述符发现变化,就主动通知。成功解决了select的两个问题,“epoll 被称为解决 C10K 问题的利器。”
188+
189+
1、select的“健忘症”,一返回就不记得关注了多少fd。api 把告诉内核等哪些文件,和最终监听哪些文件,都是同一个api。而epoll,告诉内核等哪些文件 和具体等哪些文件分开成两个api,epoll的“等”返回后,还是知道关注了哪些fd。<br>
190+
2、select在返回后的维护开销很大,而epoll就可以直接知道需要等fd。
191+
192+
![image](https://user-images.githubusercontent.com/87457873/127650306-b4419ff5-164e-4678-80f4-6f839ad44245.png)
193+
194+
![image](https://user-images.githubusercontent.com/87457873/127650321-094aa0ad-c49b-46d9-8100-4b7c3cc60f53.png)
195+
196+
epoll获取事件的时候,无须遍历整个被侦听的描述符集,只要遍历那些被内核I/O事件异步唤醒而加入就绪队列的描述符集合。
197+
198+
epoll_create: 创建epoll池子。<br>
199+
epoll_ctl:向epoll注册事件。告诉内核epoll关心哪些文件,让内核没有健忘症。<br>
200+
epoll_wait:等待就绪事件的到来。专门等哪些文件,第2个参数 是输出参数,包含满足的fd,不需要再遍历所有的fd文件。<br>
201+
202+
![image](https://user-images.githubusercontent.com/87457873/127650368-39c111d9-a074-4f0b-befd-3c0e9a29867b.png)
203+
204+
如上图,epoll在CPU的消耗上,远低于select,这样就可以在一个线程内监控更多的IO。
205+
206+
```c
207+
#include <stdio.h>
208+
#include <unistd.h>
209+
#include <fcntl.h>
210+
#include <sys/types.h>
211+
#include <sys/epoll.h>
212+
#include <sys/stat.h>
213+
214+
static void call_epoll(void)
215+
{
216+
int epfd, fifofd, pipefd;
217+
struct epoll_event ev, events[2];
218+
int ret;
219+
220+
epfd = epoll_create(2);
221+
if (epfd < 0) {
222+
perror("epoll_create()");
223+
return;
224+
}
225+
226+
ev.events = EPOLLIN|EPOLLET;
227+
228+
fifofd = open("/dev/globalfifo", O_RDONLY, S_IRUSR);
229+
printf("fifo fd:%d\n", fifofd);
230+
ev.data.fd = fifofd;
231+
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fifofd, &ev);
232+
233+
pipefd = open("pipe", O_RDONLY|O_NONBLOCK, S_IRUSR);
234+
printf("pipe fd:%d\n", pipefd);
235+
ev.data.fd = pipefd;
236+
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd, &ev);
237+
238+
while(1) {
239+
ret = epoll_wait(epfd, events, 2, 50000);
240+
if (ret < 0) {
241+
perror("epoll_wait()");
242+
} else if (ret == 0) {
243+
printf("No data within 50 seconds.\n");
244+
} else {
245+
int i;
246+
for(i=0;i<ret;i++) {
247+
char buf[100];
248+
read(events[i].data.fd, buf, 100);
249+
printf("%s is available now:, %s\n",
250+
events[i].data.fd==fifofd? "fifo":"pipe", buf);
251+
}
252+
}
253+
}
254+
_out:
255+
close(epfd);
256+
}
257+
258+
int main()
259+
{
260+
call_epoll();
261+
return 0;
262+
}
263+
```
264+
总结:epoll是几乎是大规模并行网络程序设计的代名词,一个线程里可以处理大量的tcp连接,cpu消耗也比较低。很多框架模型,nginx, nodejs, 底层均使用epoll实现。
265+
266+
#### signal IO
267+
268+
目前在linux中很少被用到,Linux内核某个IO事件ready,通过kill出一个signal,应用程序在signal IO上绑定处理函数。
269+
270+
![image](https://user-images.githubusercontent.com/87457873/127650438-dafaa967-0eb9-4886-9a90-4200fd3d2708.png)
271+
272+
kernel发现设备读写事件变化,调用一个 kill fa_sync ,应用程序绑定signal_io上的事件。
273+
274+
![image](https://user-images.githubusercontent.com/87457873/127650461-1e5fc79c-496e-4c7e-a1d4-dfa0c922644e.png)
275+
276+
#### 异步IO
277+
278+
![image](https://user-images.githubusercontent.com/87457873/127650494-6b63c04c-7b7e-4afa-9f8e-d37f36046651.png)
279+
280+
Linux中
281+
282+
不要把aio串起来,
283+
284+
基于epoll等api进行上层的封装,再基于事件编程。某个事件成立了,就开始去做某件事。
285+
286+
#### libevent
287+
288+
![image](https://user-images.githubusercontent.com/87457873/127650540-a0005da6-290f-464a-a14b-f05088b9c9d8.png)
289+
290+
就像MFC一样,界面上的按钮,VC会产生一个on_button,调对应的函数。是一种典型的事件循环。
291+
292+
本质上还是用了epoll,只是基于事件编程。
293+
294+
![image](https://user-images.githubusercontent.com/87457873/127650560-697e2b4e-61ab-4e43-960f-67d6f2aa2b1a.png)
295+

0 commit comments

Comments
 (0)