同步,阻塞,非阻塞
先来谈谈这个。
很多人一直搞不清,从概念上去解释的话,我也很难解释清楚。
不过知乎上有个回答很nice。我引用过来。
同步异步,阻塞非阻塞的区别
正常的来说,一段数据流从网络到我们自己设定的数组中,需要经历以下几个阶段
- 数据到端口
- 内核把数据从端口拷贝到内核缓存区
- 用户调用read让内核把缓存区的数据拷贝到我们设定的数组中
同步和异步的区别在实际中就是用户和内核的交互上。
同步的话就是我们主动的问内核数据来了吗,数据准备好了吗,如果内核高速我们数据已经到缓冲区了,我们还需要自己再去把缓冲区的数据拷过来。
异步就是内核把数据准备好了,拷贝到了用户空间后,之后主动调用我们设置的回掉函数。
阻塞和非阻塞在实际中是用户线程的IO和内核交互上。
阻塞就是我们写或者读内核数据时必须等待全部写完或者必须有数据让我们读到。
非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
下面结合代码来看看常用的IO模型是什么架构的。
阻塞IO
同步阻塞模型其实最常见也是最容易理解的。
无论在Java
还是c
的网络编程中都是bind
-> listen
-> accept
。
下面是一个简单的echo
程序,把接收到的返回回去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <unistd.h> #include <sys/types.h> #include <netdb.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <arpa/inet.h>
#define BACKLOG 1024 #define BUFFERSIZE 8192 int main(int argc, char *argv[]) { int server_fd, conn_fd; unsigned short port = 1080; struct sockaddr_in addr_s; struct sockaddr_in addr_c; server_fd = socket(AF_INET, SOCK_STREAM, 0); memset(&addr_s, 0, sizeof(addr_c)); addr_s.sin_family = AF_INET; addr_s.sin_addr.s_addr = inet_addr("127.0.0.1"); addr_s.sin_port = htons(port); int ret = bind(server_fd, (struct sockaddr*)&addr_s, sizeof(addr_c)); char buffer[BUFFERSIZE]; int addr_clen = sizeof(addr_c); listen(server_fd, BACKLOG); while(1) { conn_fd = accept(server_fd, (struct sockaddr*)&addr_c, (socklen_t *) &addr_clen); int size = read(conn_fd, buffer, 8192); write(conn_fd, buffer, size); close(conn_fd); } return 0; }
|
这里的while(1)
中,如果没有连接,那么程序就一直等待accept
,直到有一个连接产生。
等到有一个连接到了,然后去调用read
和write
函数去响应这个请求。
但是在处理这个read
的时候如果对方一直没有输入,那么read
就会一直等下去。
那么read
的行为就是阻塞。
同样的,对于write
,如果发送缓冲区没有空间或者空间不足的话,write操作会直接阻塞住等待有足够的空间。
假设第一个连接停在了read
或者write
,那么下面的连接就无法进行处理了。
所以一般会进行fork
调用去处理请求。
1 2 3 4 5 6 7
| if(fork() == 0) { close(server_fd); int size = read(conn_fd, buffer, 8192); send(conn_fd, buffer, size, 0); close(conn_fd); return 0; }
|
这样可以处理小批量的并发请求,但是大了就不行了。
因为linux
进程是需要资源,相对来说开销较大。
就像Java
中对每一个请求都开一个线程去处理一样。
非阻塞IO
上面提到阻塞IO的问题在read
和write
函数。
在linux
或者mac
下有一个系统调用。
1 2
| #include <fcntl.h> int fcntl(int fildes, int cmd, ...);
|
使用这个我们可以改变文件描述符的性质。
比如使它成为非阻塞的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int set_no_blocking(int fd) { int flags; if ((flags = fcntl(fd, F_GETFL, 0)) == -1) { log_err("fcntl error\n"); exit(1); } flags |= O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) == -1) { log_err("fcntl error\n"); exit(1); } return 1; }
|
这样设置之后,read
调用会立即返回。接收缓冲区中有数据时,与阻塞socket有数据的情况是一样的。
如果接收缓冲区中没有数据,则返回错误号为EWOULDBLOCK,表示该操作本来应该阻塞的,但是由于本socket为非阻塞的socket,因此立刻返回,遇到这样的情况,可以在下次接着去尝试读取。
如果返回值是其它负值,则表明读取错误。。
write也会立即返回。
在发送缓冲区没有空间时会直接返回错误号EWOULDBLOCK,表示没有空间可写数据。
如果错误号是别的值,则表明发送失败。
如果发送缓冲区中有足够空间或者是不足以拷贝所有待发送数据的空间的话,则拷贝前面N个能够容纳的数据,返回实际拷贝的字节数。
IO复用
异步的最普遍的实现方式是回调。
但是有回掉的并不就是异步的。
比如Libevent中事件回调,还有netty。
下面选自我的tinyhttp仓库,地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
char* index_home = ""; struct event_base *base; int main(int argc, char *argv[]) { argv[1] = "127.0.0.1"; argv[2] = "4000"; argv[3] = "/Users/zhuyichen/fortest/tinydemo/v3.bootcss.com/"; index_home = argv[3]; if (chdir(index_home) == -1) { perror("index_home : "); exit(1); } int listener; listener = socket_bind_listen(argv[1], argv[2]); if (listener == -1) { exit(1); } set_no_blocking(listener); log_info("start listen in host %s port %s ...\n", argv[1], argv[2]); base = event_base_new(); struct event* ev_listen = event_new(base, listener, EV_READ | EV_PERSIST, on_accept, NULL); event_base_set(base, ev_listen); event_add(ev_listen, NULL); event_base_dispatch(base); event_base_free(base); } void on_accept(int serverfd, short events, void *arg) { int connfd = accept(serverfd, NULL, 0); set_no_blocking(connfd); }
|
重点在以下代码中
1 2 3
| struct event* ev_listen = event_new(base, listener, EV_READ | EV_PERSIST, on_accept, NULL); event_base_set(base, ev_listen); event_add(ev_listen, NULL);
|
我们创建了一个ev_listen
事件用来监听socket
描述符上的READ
事件,设置回调函数为on_accept
。
这样只要listener
文件句柄上产生了新的连接,就是回调on_accept
函数,在on_accept
函数中进行accept
。
这就是一个异步的例子。
很多人以为这就是异步,但是上面我们提到,异步的特点就是,操作系统会自己把数据从内核缓冲区拷贝到用户区,仔细看就会发现,libevent还是需要我们自己去进行read的,只是我们read的时候不会遇到缓冲区为空的情况,说到底,还是同步的读。
同步IO
我们再来谈谈同步的IO。
其实同步异步非阻塞并不是相互独立的
我们说同步就是内核的数据是否准备好需要我们自己去询问和如果准备好了还是需要我们自己去拷贝到用户空间。
按照这个条件,其实前两种阻塞IO和非阻塞IO和IO复用都是同步IO。
包括select
poll
epoll
函数,都是同步的,因为我们还是需要自己区拷贝数据到用户空间。
真正的异步IO这一层是系统帮我们做的。