五种IO模型
- 阻塞IO,在内核将数据准备好之前,系统会一直在等待,所有的套接字,默认都是阻塞方式
- 非阻塞IO:如果内核还没有将数据准备好,系统会调用仍然直接返回,并且返回EWOULDBLOCK错误码,非阻塞IO需要程序员循环的方式反复尝试读写文件描述符,看数据是否准备好,这个过程称为轮询,,这对CPU来说是很大的浪费
- 信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
- IO多路转接:阻塞式的灯,但同时等待多个文件描述符的就绪状态
- 异步IO:由内核在数据拷贝完成时,通知应用程序拷贝完成
任何IO过程中,都包含两步:第一步是等待,第二步是数据拷贝,实际应用中,等待消耗的时间往往都大于数据拷贝的时间,如果我们要提高IO的效率,就需要将等待的比重降低,让等待的时间尽量少。
IO多路转接之select
概念
select系统调用是用来让程序监视多个文件描述符的状态变化;程序停在select处等待,直到被监视的文件描述符有一个或者多个的状态发生了变化。
函数原型:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:
- nfsd是需要监听的最大文件描述符+1;
- fd_set是一个结构体,rdset, wrset, exset分别对应于需要检测的可读文件描述符集合,可写文件描述符集合及异常文件描述符集合;
timeval是一个结构体,timeout设置select的等待时间:
- NULL表示select没有timeout,select将一直被阻塞,直到有某个文件描述上发生了事件;
- 0表示仅检测描述符集合的状态,然后立即返回,并不等待事件发生;
- 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
fd_set这个结构是一个整数数组,或者说是位图,位图中对应的位来表示要监视的文件描述符,既然是位图,那么我们就一定有一组专门的接口来操作位图
void FD_CLR(int fd, fd_set *set); //用来清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); // ⽤用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // ⽤用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // ⽤用来清除描述词组set的全部位
函数返回值:
- 执行成功则返回文件描述词状态已改变的个数
- 返回0则代表在描述此状态改变前已超过timeout时间,没有返回
- 当有错误发生时则返回-1,错误原意存于errno,此时参数readfds, writefds, exceptfds和timeout的值不可预测
//常用代码段:
fd_set readset;
FD_SET(fd, &readset);
select(fd+1, &readset, NULL, NULL, NULL);
if(FD_ISSET(fd, readset)) {.....}
select的执行过程
举一个例子,假如fd_set的长度为1个字节,其中每一个比特位表示一个文件描述符
- 执行fd_set set; FD_ZERO(&set); set8位表示为0000, 0000
- 若fd=5,执行FD_SET,set变为0001, 0000
- 若再加入fd=2,fd=1,则set变为0001, 0011
- 执行select(6, &set, 0, 0, 0, 0)阻塞等待
- 若fd=1,fd=2上都发生了可读事件,则select返回,set变为0000, 0011(fd=5没有发生事件被清空)
至此,我们已经大致明白了select的执行过程和select函数的使用方法,那么什么样的事件是读就绪,什么事件是写就绪呢?
读就绪:
- socket内核中,接受缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
- socket TCP通信中,对端关闭连接,此时对该socket读,则返回0;
- 监听的socket上有新的连接请求
- socket上有未处理的错误
写就绪
- socket内核中,发送缓冲区中的可用字节数(空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写该文件描述符,并且返回值大于0
- socket的写操作被关闭,对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号;
- socket使用非阻塞connect连接成功或失败后
- socket上有未读取的错误
异常就绪
socket上收到带外数据
总结select的特点
- 可监控的文件描述符个数有限,取决于sizeof(fd_set),我的电脑上sizeof(fd_set)=128,也就是说最多可以支持128*8个文件描述符
- 需要一个数据结构array保存select监控集中的fd
(1)select返回后,array作为源数据,遍历数组中的每一个数据,进行FDISSET判断是不是在fd_set中
(2)select返回后会把以前假如的但没哟事件发生的fd清空,所以每次开始select前都要重新从array照片那个取得fd逐一加入,同时还要取得fd最大值maxfd,用于select的第一个参数。- 虽然有很多缺点,但是相比于多进程、多线程版本,其效果还是非常高的。
select的缺点
- 每次调用select,需要手动设置fd集合,接口的使用角度来看不方便
- 每次调用select,需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时很大
- 每次调用select,都要在内核遍历传递进来的所有fd,这个开销在fd很多时很大
- select支持的文件描述符量太少
select编写的服务器
// 仅将客户端发送的消息回显,并没有做其他处理
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<string.h>
#define MAX_FDS sizeof(fd_set)*8
#define INIT_DATA -1
static void InitArray(int arr[], int num)
{
int i=0;
for(; i<num; i++)
arr[i] = INIT_DATA;
}
static int addSockToArray(int arr[], int num, int fd)
{
int i=0;
for(; i<num; i++)
{
if(arr[i] < 0)
{
arr[i] = fd;
return i;
}
}
return -1;
}
int setArrayToFdSet(int arr[], int num, fd_set* rfds)
{
int i=0;
int max_fd = INIT_DATA;
for(; i<num; i++)
{
if(arr[i] >= 0)
{
FD_SET(arr[i], rfds);
if(max_fd < arr[i])
max_fd = arr[i];
}
}
return max_fd;
}
int startup(int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("sock");
exit(2);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(port);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sock, 5) < 0)
{
perror("listen");
exit(4);
}
return sock;
}
static void serviceIO(int arr[], int num, fd_set* rfds)
{
int i = 0;
for(; i<num; i++)
{
if(arr[i] > INIT_DATA)
{
int fd = arr[i];
if(i==0 && FD_ISSET(arr[i], rfds))//listen_sock ready
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(fd, (struct sockaddr*)&client, &len);
if(sock < 0)
{
perror("accept");
continue;
}
printf("get a new client [%s:%d]\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
if(addSockToArray(arr, num, sock) == -1)
{
close(sock);
}
}
else if(i != 0 && FD_ISSET(arr[i], rfds))
{ // normal fd ready
char buf[10240];
ssize_t s = read(fd, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client:> %s\n", buf);
}
else if(s == 0)
{
close(fd);
arr[i] = INIT_DATA;
printf("client quit\n");
}
else
{
perror("read");
close(fd);
arr[i] = INIT_DATA;
}
}
else
{
//do nothing not ready
}
}
}
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("Usage: %s [port]\n", argv[0]);
return 1;
}
int listen_sock = startup(atoi(argv[1]));
int fd_array[MAX_FDS];
InitArray(fd_array, MAX_FDS);
addSockToArray(fd_array, MAX_FDS, listen_sock);
fd_set rfds;
for(;;)
{
FD_ZERO(&rfds);
int max_fd = setArrayToFdSet(fd_array, MAX_FDS, &rfds);
struct timeval timeout = {3, 0};
switch(select(max_fd+1, &rfds, NULL, NULL, NULL))
{
case -1:
perror("select");
break;
case 0:
printf("time out...\n");
break;
default:
serviceIO(fd_array, MAX_FDS, &rfds);
break;
}
}
return 0;
}