一、IO模型
1.分类
在UNIX/Linux下主要有4种I/O 模型:
阻塞I/O:最常用、最简单、效率最低
非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
I/O 多路复用:允许同时对多个I/O进行控制
信号驱动I/O:一种异步通信模型------底层驱动专栏中详细讲
2.阻塞IO
以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,进程会被挂起,一直阻塞,直到缓冲区中有内容了,内核会唤醒该进程,读完内容后继续向下执行。
写操作也是会阻塞的,当缓冲区满了,就阻塞了,当缓冲区中有足够的空间接收这次写了就能解除阻塞。一般情况下,对于阻塞的问题,考虑的都是读的阻塞。
示例:以写阻塞为例
//写端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
int fd = open("my_fifo", O_WRONLY);
if(-1 == fd){
perror("open error");
exit(-1);
}
int count = 0;
while(1){
if(-1 == write(fd, "hello world", 11)){
perror("write error");
exit(-1);
}
count++;
printf("count = %d\n", count);
}
close(fd);
return 0;
}
//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(){
int fd = open("my_fifo", O_RDONLY);
char buff[11] = {0};
read(fd, buff, 11);
while(1);//防止管道破裂
close(fd);
return 0;
}
3.非阻塞IO
以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,相当于告诉内核,不要将这个进程挂起,而是立即给我返回一个错误。
一般的带有阻塞属性的函数,默认方式都是阻塞IO。对于recv recvfrom 等函数,是可以通过参数来设置成非阻塞的。如:recv 的 MSG_DONTWAIT,recvfrom 的 MSG_DONTWAIT,waitpid 的 WNOHANG等。但是对于 read 等函数,默认方式就是阻塞,如果想使用read实现非阻塞,需要用到 fcntl() 来修改文件描述符的状态。
fcntl函数说明:
int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置或获取文件描述符的状态
#include <unistd.h>
#include <fcntl.h>
参数:
@fd: 文件描述符
@cmd: 要控制的指令
F_GETFL 获取文件描述符的状态
F_SETFL 设置文件描述符的状态 O_NONBLOCK 非阻塞
@arg: 可变参
具体需不需要取决于第二个参数是什么,
如果第二个参数是 F_GETFL 就不需要
如果第二个参数是 F_SETFL 就需要
返回值: F_GETFL 返回的就是文件描述符的状态
F_SETFL 成功返回0 失败返回-1
示例:使用管道时,注意,写端未打开,读端的open会阻塞,需要设置成非阻塞才能读到。
//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd1 = open("fifo1", O_RDONLY);
if(-1 == fd1){
perror("open error");
exit(-1);
}
int fd2 = open("fifo2", O_RDONLY);
if(-1 == fd2){
perror("open error");
exit(-1);
}
int fd3 = open("fifo3", O_RDONLY);
if(-1 == fd3){
perror("open error");
exit(-1);
}
//将文件描述符 fd1 fd2 fd3 设置成非阻塞
int flag = fcntl(fd1, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd1, F_SETFL, flag);
flag = fcntl(fd2, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd2, F_SETFL, flag);
flag = fcntl(fd3, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd3, F_SETFL, flag);
char buff1[128] = {0};
char buff2[128] = {0};
char buff3[128] = {0};
while(1){
read(fd1, buff1, 128);
printf("buff1 = %s\n", buff1);
memset(buff1, 0, 128);
read(fd2, buff2, 128);
printf("buff2 = %s\n", buff2);
memset(buff2, 0, 128);
read(fd3, buff3, 128);
printf("buff3 = %s\n", buff3);
memset(buff3, 0, 128);
//sleep(1);//为了演示现象用的 防止刷屏
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
//写端(三个写端一样的)
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(){
int fd = open("fifo1", O_WRONLY);
if(-1 == fd){
perror("open error");
exit(-1);
}
char buff[128] = {0};
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';
if(-1 == write(fd, buff, 128)){
perror("write error");
exit(-1);
}
memset(buff, 0, 128);
}
close(fd);
return 0;
}
4.IO多路复用
使用阻塞的方式处理多个阻塞函数,相互之间会有影响,有时不可取。如果使用非阻塞,有需要写一个循环轮询每个函数,十分占用CPU,也不可取。使用多进程、多线程也可以解决这个问题,但是要考虑资源的回收及安全问题,比较麻烦。比较好的一种方式,是使用 IO 多路复用。
IO多路复用的基本思想:
先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
select函数说明:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:IO多路复用
#include <sys/select.h>
参数:
@nfds: 最大的文件描述符+1
@readfds: 要监控的读文件描述符集合 我们一般考虑读
@writefds: 要监控的写文件描述符集合
@exceptfds: 要监控的异常文件描述符集合
@timeout: 超时时间
有值:阻塞的时间,超时后 select会立即返回
0: 非阻塞
NULL:永久阻塞
返回值: 成功返回已经就绪的文件描述符的个数,超时返回0,失败返回-1
注:FD_SETSIZE:select 能监视的最大的文件描述符个数是1024
文件描述符相关函数:
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); //将集合清空
示例:
//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
int main(){
int fd1 = open("fifo1", O_RDONLY);
int fd2 = open("fifo2", O_RDONLY);
int fd3 = open("fifo3", O_RDONLY);
int max_fd = 0;//保存最大的文件描述符
//构建要监视的文件描述符集合
fd_set readfds;//保存初始的
fd_set readfds_temp;//给select用的
FD_ZERO(&readfds);//清空
FD_ZERO(&readfds_temp);//清空
//将要监视的文件描述符添加进集合
FD_SET(fd1, &readfds);
max_fd = (max_fd>fd1?max_fd:fd1);
FD_SET(fd2, &readfds);
max_fd = (max_fd>fd2?max_fd:fd2);
FD_SET(fd3, &readfds);
max_fd = (max_fd>fd3?max_fd:fd3);
char buff1[128] = {0};
char buff2[128] = {0};
char buff3[128] = {0};
while(1){
//注意:每次select返回都会将没有准备好的文件描述符在表中擦除
//所以每次要重新将文件描述符添加到集合中
readfds_temp = readfds;
if(-1 == select(max_fd+1, &readfds_temp, NULL, NULL, NULL)){
perror("select error");
exit(-1);
}
if(FD_ISSET(fd1, &readfds_temp)){
read(fd1, buff1, 128);
printf("buff1 = [%s]\n", buff1);
memset(buff1, 0, 128);
}
if(FD_ISSET(fd2, &readfds_temp)){
read(fd2, buff2, 128);
printf("buff2 = [%s]\n", buff2);
memset(buff2, 0, 128);
}
if(FD_ISSET(fd3, &readfds_temp)){
read(fd3, buff3, 128);
printf("buff3 = [%s]\n", buff3);
memset(buff3, 0, 128);
}
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
//写端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(){
int fd = open("fifo1", O_WRONLY);
if(-1 == fd){
perror("open error");
exit(-1);
}
char buff[128] = {0};
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';
if(-1 == write(fd, buff, 128)){
perror("write error");
exit(-1);
}
memset(buff, 0, 128);
}
close(fd);
return 0;
}
二、服务器模型
1.概念
服务器模型主要有两种:
循环服