io复用
几种处理客户端的io请求的方式
方式
- 阻塞 + 进程/线程
- 非阻塞 + 忙轮询
- 复用io
- 信号、信号量处理
- 异步io
它们各自的一些缺点
- 阻塞:虽然很少占用CPU资源,但是它不能很好地处理多个客户端同时发起io请求
- 进程:虽然在多任务设计模式中,用进程能做到对资源的独立管理,同时子进程也能独立处理多个任务,但是它们的的内存开销也相对较大
- 线程:虽然它多任务处理的效率比进程快很多,但是越多的线程,CPU上下文频繁切换,负载沉重
- 非阻塞:非常占用CPU的宝贵时间片
- 信号、信号量:面对少量客户端可行,客户端一旦多了就非常困难地去平衡多个客户端与服务器之间的关系
- 异步io:还未了解
为什么要引入io复用?
因为:
从多种处理多客户io请求的几种方式上看,要解决内存开销、CPU负载的同时,也能解决接收多客户io的请求,二十世纪八十年代就提出了io复用的select,后来epoll是select的加强版,io复用能在一定程度上解决上述问题
io复用的方式
select
select相关函数的原型
//nfds:能监视最大的套接字,readfds:读取对应的套接字
//writefd:一般很少用,exceptfds:异常处理,timeout:超时处理
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set); //剔除内核队列里对于的套接字
int FD_ISSET(int fd, fd_set *set); //判断是否有数据可读
void FD_SET(int fd, fd_set *set); //添加套接字到内核队列里
void FD_ZERO(fd_set *set); //将内核队列的位清零
select的原理
一个fd_set (int)类型的,变量名为:readfds
内核会在这个变量基础上通过判断这32个位的0和1,来记录所要监测套接字(包括服务/客户端)的状态,最多能监测1024个,其中应该是包含了标准输入输出和错误,但它在内核里边具体是怎样实现的,我没深究。
select使用的框架/步骤
- 用FD_ZERO将readfds的位全置为0
- 用FD_SET将客户端套接字添加到队列里
- while(1)中内核不断select监测套接字的状态,但要注意
- readfds要用一个临时变量代替,因为客户端的请求,这个变量随时发生位的改变,之前的那些客户端很可能因此而被"遗忘",造成程序崩溃
- for循环中用FD_ISSET判断是否有客户端请求
- 然后就是客户端建立起新的连接就FD_SET,断开就FD_CLR,剩下的就是读写操作了
demo
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <unistd.h>
#define RET_ERR (-1)
int main(int argc, char **argv)
{
int fd;
int s_fd;
int c_fd;
int n_recv;
char readBuf[64];
socklen_t addrlen;
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
fd_set readfds, cur_readfds;
if(argc != 3)
{
perror("argc");
return RET_ERR;
}
addrlen = sizeof(struct sockaddr_in);
memset(&s_addr, 0, addrlen);
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET, argv[1], &s_addr.sin_addr);
if((bind(s_fd, (struct sockaddr *)&s_addr, addrlen)) == -1)
{
perror("bind");
return RET_ERR;
}