五种IO模型
阻塞IO
阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
例如:A在钓鱼时,会一直盯着鱼漂,当鱼漂动时,拉动鱼竿,其余的时间都在盯着鱼漂。
非阻塞IO
非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。
例如:B也来钓鱼,B一遍玩手机一遍看着鱼漂,还时不时的问A钓没钓到鱼。B不是一直在等还可以玩手机的等。
信号驱动
信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
例:此时C也来钓鱼,C将鱼竿上系了一个铃铛,C在看书,当铃铛响了C开始拉动鱼竿。C就像是信号驱动IO一样。
多路转接
IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
例:此时D拿着20根鱼竿来钓鱼,D来回看着就可以了。20根鱼竿鱼上钩的几率远大于之前的3个人。
异步IO
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
例:E带着手下来钓鱼,E说:你在这钓,把桶钓满了拿回来给我。E然后开车走了。E根本不用在这里等待。
总结:
任何IO都包括2个步骤:1是等,2是拷贝
读IO=等待读事件就绪+内核数据拷贝到用户空间
写IO=等待写事件就绪+用户空间数据拷贝到内核空间
高级IO的本质:尽可能减少等的时间比重
同步和异步通信
同步概念:
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果
异步概念:
异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
还需注意多进程和多线程同步和异步之间的区别:
进程/线程同步也是进程/线程之间直接的制约关系
是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候。
I/O多路转接之select
系统提供select函数来实现多路复用输入/输出模型.
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
select函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout)
参数说明:
- 参数nfds是需要监视的最大的文件描述符值+1;
- rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
- 参数timeout为结构timeval,用来设置select()的等待时间
参数timeout取值:
- NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了件;
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
fd_set结构
用命令vim /usr/include/sys/select.h
查看源码:
53 /* The fd_set member is required to be an array of longs. */
54 typedef long int __fd_mask;
64 typedef struct
65 {
66 /* XPG4.2 requires this member name. Otherwise avoid the name
67 from the global namespace. */
68 #ifdef __USE_XOPEN
69 __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
70 # define __FDS_BITS(set) ((set)->fds_bits)
71 #else
72 __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
73 # define __FDS_BITS(set) ((set)->__fds_bits)
74 #endif
75 } fd_set;
select的过程:
简单的select服务器
将创建套接字单独封装成一个类
Sock.hpp
1 #pragma once
2
3
4 #include <iostream>
5 #include <string>
6
7 #include <sys/socket.h>
8 #include <netinet/in.h>
9 #include <sys/types.h>
10 #include <arpa/inet.h>
11 #include <unistd.h>
12 #include <stdlib.h>
13 #include <strings.h>
14 #include <sys/select.h>
15
16 using namespace std;
17
18 class Sock{
19 public:
20 static int Socket(){
21 int sock = socket(AF_INET, SOCK_STREAM, 0);
22 if(sock < 0){
23 cerr << "socket error" << endl;
24 exit(2);
25 }
26 return sock;
27 }
28 static void Bind(int sock, int port){
29 struct sockaddr_in local;
30 bzero(&local, sizeof(local));
31
32 local.sin_family = AF_INET;
33 local.sin_port = htons(port);
34 local.sin_addr.s_addr = htonl(INADDR_ANY);
35 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
36 cerr << "bind error" << endl;
37 exit(3);
38 }
39
40 }
41 static void Listen(int sock){
42 if(listen(sock, 5) < 0){
43 cerr << "listen error" << endl;
44 exit(4);
45 }
46
47 }
48 static int Accept(int sock){
49 struct sockaddr_in peer;
50 socklen_t len = sizeof(peer);
51 int fd = accept(sock, (struct sockaddr*)&peer, &len);
52 if(fd < 0){
53 cerr << "accpet error" << endl;
54 }
55
56 return fd;
57 }
58 static void Setsockopt(int sock){
59 int opt = 1;
60 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
61 }
62 };
selectServer.hpp
1 #pragma once
2
3 #include"Sock.hpp"
4 #include<sys/select.h>
5 #define NUM (sizeof(fd_set)*8)
6 #define DFL_FD -1
7
8
9 //#define DFL_PORT 8080
10 #define NUM (sizeof(fd_set)*8)
11 #define DFL_FD -1
12
13 class SelectServer {
14 private:
15 int lsock;
16 int port;
17 int fd_array[NUM];
18 public:
19 SelectServer(int _p = 8080) :port(_p)
20 {}
21 void InitServer(){
22 //初始化数组来保存已经打开的文件描述符
W> 23 for (int i = 0; i < NUM; i++) {
24 fd_array[i] = DFL_FD;
25 }
26 lsock = Sock::Socket();
27 Sock::Setsockopt(lsock);
28 Sock::Bind(lsock, port);
29 Sock::Listen(lsock);
30
31 fd_array[0] = lsock;//将第一个元素设置成监听套接字
32 }
33 void AddFd2Array(int sock){
34 int i = 0;
W> 35 for (; i < NUM; i++){
36 if (fd_array[i] == DFL_FD) {
37 break;
38 }
39 }
W> 40 if (i >= NUM) {
41 cerr << "fd array is full, close sock" << endl;
42 close(sock);
43 }
44 else{
45 fd_array[i] = sock;
46 cout << "fd: " << sock << " add to select ..." << endl;
47
48 }
49
50 }
51 void DefFdFromArray(int index){
W> 52 if (index >= 0 && index < NUM){
53 fd_array[index] = DFL_FD;
54
55 }
56 }
57 void HandlerEvents(fd_set* rfds){
58 //先判断文件描述符是否合法
W> 59 for (int i = 0; i < NUM; i++){
60 if(fd_array[i] == DFL_FD){
61 continue;
62 }//分为监听套接字和普通的套接字
63 if(FD_ISSET(fd_array[i], rfds)) {
64 if (fd_array[i] == lsock) {
65
66 int sock = Sock::Accept(lsock);
67 if (sock >= 0){
68 cout << "get a new link ..." << endl;
69 AddFd2Array(sock);//监听套接字不能直接就读数据,要先添加到数组中
70 //有可能连接建立好后不发数据了
71 }
72
73 }
74 else{
75 char buf[10240];
76 ssize_t s = recv(fd_array[i], buf, sizeof(buf), 0);
77 if (s > 0) {
78 buf[s] = 0;
79 cout << "client# " << buf << endl;
80 }
81 else if (s == 0) {
82 cout << "clien quit" << endl;
83 //客户端退出关闭文件描述符和将位图中的清0
84 close(fd_array[i]);
85 DefFdFromArray(i);}
86 else {}
87 }
88 }
89
90 }
91 }
92 void Start(){
93 int maxfd = DFL_FD;
94 for (;;) {
95 fd_set rfds;
96 FD_ZERO(&rfds);//在栈上先将位图清0
97 cout << "fd_array: ";
W> 98 for (int i = 0; i < NUM; i++) {
99 if (fd_array[i] != DFL_FD) {
100 cout << fd_array[i] << " ";
101 FD_SET(fd_array[i], &rfds);
102 if (maxfd < fd_array[i]){
103 maxfd = fd_array[i];//更新最大的maxfd
104 }
105 }
106 }
107 cout << endl;
108 cout << "begin select ..." << endl;
109 switch (select(maxfd + 1, &rfds, nullptr, nullptr, nullptr)){
110 //select的3种返回值
111 case 0:
112 cout << "timeout ..." << endl;
113 break;
114 case -1:
115 cerr << "select error!" << endl;
116 break;
117 default:
118 //事件就绪
119 HandlerEvents(&rfds);
120 break;
121 }
122 }
123 }
124 ~SelectServer()
125 {}
126
127 };
select.cc
1 #include "selectServer.hpp"
2
3 void Usage(string proc)
4 {
5 cout <<"Usage:\n\t"<< proc << " port" << endl;
6
7 }
8
9 int main(int argc, char *argv[])
10 {
11 cout << sizeof(fd_set)*8 << endl;
12 if(argc != 2)
13 {
14 Usage(argv[0]);
15 exit(1);
16 }
17 SelectServer *ssvr = new SelectServer(atoi(argv[1]));
18 ssvr->InitServer();
19 ssvr->Start();
20
21 return 0;
22 }
下面来测试一下:
在用手机测试一下:
总结
select就绪条件
读就绪
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误
写就绪
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0
- socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
- socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
select特点
可监控的文件描述符个数取决与sizeof(fd_set)的值.服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则服务器上支持的最大文件描述符是512*8=4096.
将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
- 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
- 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
select缺点
每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
select支持的文件描述符数量太小
本篇文章到这就结束了,后面还会有poll,epoll的讲解。