IO模型
1. 阻塞IO
- 定义:在阻塞IO中,当一个IO操作(如读写文件、网络请求等)发起时,如果数据未就绪(例如,没有数据可读或缓冲区满无法写入),则进程或线程会暂停执行,直到IO操作完成。
- 特点:简单直接,但效率低下,特别是在高并发场景下,大量线程或进程可能会因为等待IO操作而阻塞,导致资源浪费。
2. 非阻塞IO
- 定义:非阻塞IO模式下,IO操作会立即返回,而不会阻塞调用者。如果IO操作不能立即完成,则通常会返回一个错误码(如
EAGAIN
或EWOULDBLOCK
),表示IO操作尚未准备好。 - 特点:需要应用程序不断地检查IO状态,即忙等待(busy polling),这可能会消耗大量CPU资源。
fcntl函数
fcntl
函数是 UNIX 和类 UNIX 系统(如 Linux)中的一个系统调用,用于操作文件描述符(file descriptor)的属性。它提供了比标准文件 I/O 函数(如 open
、read
、write
、close
)更细粒度的控制。通过 fcntl
,你可以执行如复制文件描述符、获取/设置文件描述符标志、获取/设置文件锁等操作。
#include <fcntl.h>
#include <unistd.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fd
:要操作的文件描述符。cmd
:要执行的操作命令。这些命令可以是复制文件描述符(如F_DUPFD
)、获取/设置文件描述符标志(如F_GETFD
、F_SETFD
)、获取/设置文件锁(如F_GETLK
、F_SETLK
、F_SETLKW
)等。arg
:根据cmd
的不同,这个参数可能是一个指向struct flock
的指针(用于文件锁操作),或者是一个整数(用于获取/设置文件描述符标志)。对于某些cmd
,此参数可能不被使用
常用的 fcntl
命令
- F_DUPFD:复制文件描述符
fd
到一个新的文件描述符,这个新文件描述符是最小的大于或等于arg
的未使用描述符。 - F_GETFD:获取文件描述符
fd
的标志,并将结果存储在通过arg
指针提供的整数中。 - F_SETFD:设置文件描述符
fd
的标志。arg
是一个整数,指定了要设置的标志。 - F_GETFL:获取文件描述符
fd
的文件状态标志,如只读、只写、同步等,并将结果存储在通过arg
指针提供的整数中。 - F_SETFL:设置文件描述符
fd
的文件状态标志。arg
是一个整数,指定了要设置的标志。 - F_GETLK、F_SETLK、F_SETLKW:这些命令用于文件锁定。
F_GETLK
用于测试锁的存在,F_SETLK
用于设置锁(非阻塞),F_SETLKW
用于设置锁(阻塞,直到锁可用)。
3. 信号驱动IO
- 定义:在这种模式下,当数据准备好时,系统会发送一个信号(如SIGIO)给应用程序。应用程序随后会处理这个信号,执行IO操作。
- 特点:减少了CPU的浪费,因为应用程序不需要持续检查IO状态。但是,信号处理函数的执行可能受到系统对信号处理函数的限制(如异步信号处理的安全性问题)。
4. 并行模型(进程、线程)
- 定义:这不是一种IO模型,而是一种并发执行模型。在这种模型中,多个进程或线程可以同时运行,每个进程或线程可以处理不同的IO操作。
- 特点:可以提高程序的并发处理能力,但也可能导致资源竞争、死锁和上下文切换开销等问题。
5. IO多路复用
- 定义:IO多路复用允许单个进程或线程同时监视多个IO操作。当其中一个IO操作准备好时,它会通知进程或线程进行相应的处理。常见的实现有
select
、poll
和epoll
。- select:可以监视多个文件描述符,但它有监视数量限制(通常为1024),并且当文件描述符数量较大时,效率较低。
- poll:与select类似,但它没有监视数量的限制,但在处理大量文件描述符时效率仍然不高。
- epoll:是Linux特有的,基于事件的IO多路复用技术,比select和poll更高效,特别是在处理大量并发连接时。
- 特点:通过减少线程或进程的数量,降低了上下文切换的开销,并提高了IO操作的效率。
练习:非阻塞IO
写端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo1",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
int fd_w = open("myfifo1",O_WRONLY);
if(-1 == fd_w)
{
perror("open");
return 1;
}
while(1)
{
char buf[128]="hello, fifo test";
write(fd_w,buf,strlen(buf));
sleep(3);
}
return 0;
}
读端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo1",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
int fd_r = open("myfifo1",O_RDONLY);
if(-1 == fd_r)
{
perror("open");
return 1;
}
int flag = fcntl(fd_r,F_GETFL);
fcntl(fd_r,F_SETFL,flag|O_NONBLOCK);
flag = fcntl( 0,F_GETFL);
fcntl(0,F_SETFL,flag|O_NONBLOCK);
while(1)
{
char buf[128]={0};
if(read(fd_r,buf,sizeof(buf))>0)
{
printf("fifo:%s\n",buf);
}
bzero(buf,sizeof(buf));
if(fgets(buf,sizeof(buf),stdin))
{
printf("terminal:%s\n",buf);
}
}
return 0;
}
练习:信号驱动IO
读端:写端同上
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
int fd_r;
void handle(int num)
{
char buf[128]={0};
read(fd_r,buf,sizeof(buf));
printf("fifo:%s\n",buf);
return ;
}
int main(int argc, char *argv[])
{
signal(SIGIO,handle);
int ret = mkfifo("myfifo1",0666);
if(-1 == ret)
{
if( EEXIST!= errno )
{
perror("mkfifo");
return 1;
}
}
fd_r = open("myfifo1",O_RDONLY);
if(-1 == fd_r)
{
perror("open");
return 1;
}
//给管道设置信号驱动
int flag = fcntl(fd_r,F_GETFL);
fcntl(fd_r,F_SETFL,flag| O_ASYNC);
//如果有写管道,本进程作为sigio信号的接收者
fcntl(fd_r,F_SETOWN,getpid());
while(1)
{
char buf[128]={0};
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
printf("terminal:%s\n",buf);
}
return 0;
}
IO多路复用
1、定义和作用
定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力
作用:应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标的输入、中断信号等等事件,再比如web服务器如nginx,需要同时处理来来自N个客户端的事件。
2、为什么要用IO多路复用
逻辑控制流在时间上的重叠叫做并发 ,而CPU单核在同一时刻只能做一件事情,一种解决办法是对CPU进行时分复用(多个事件流将CPU切割成多个时间片,不同事件流的时间片交替进行)。在计算机系统中,我们用线程或者进程来表示一条执行流,通过不同的线程或进程在操作系统内部的调度,来做到对CPU处理的时分复用。这样多个事件流就可以并发进行,不需要一个等待另一个太久,在用户看起来他们似乎就是并行在做一样。
使用并发处理的成本:线程/进程创建成本,CPU切换不同线程/进程成本 Context Switch,多线程的资源竞争
有没有一种可以在单线程/进程中处理多个事件流的方法呢?一种答案就是IO多路复用。因此IO多路复用解决的本质问题是在用更少的资源完成更多的事。
3、IO多路复用的优势
- 资源利用率高:通过单线程或进程处理多个IO事件,减少了线程或进程的创建和销毁的开销,也减少了CPU在不同线程或进程之间的切换开销。
- 编程模型简单:开发者可以在单线程或进程中处理所有IO事件,避免了多线程或进程编程中的复杂性,如同步、互斥和死锁等问题。
- 扩展性好:由于单线程或进程处理IO事件,因此可以轻松扩展到处理成千上万的并发连接。
IO 多路复用 ===》并发服务器 ===》TCP协议
3、select循环服务器 ===> 用select函数来动态检测有数据流动的文件描述符
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
功能:完成指定描述符集合中有效描述符的动态检测。
该函数具有阻塞等待功能,在函数执行完毕后
目标测试集合中将只保留最后有数据的描述符。
参数:nfds 描述符的上限值,一般是链接后描述符的最大值+1;
readfds 只读描述符集
writefds 只写描述符集
exceptfds 异常描述符集
以上三个参数都是 fd_set * 的描述符集合类型
timeout 检测超时 如果是NULL表示一直检测不超时 。
返回值:超时 0
失败 -1
成功 >0
为了配合select函数执行,有如下宏函数:
void FD_CLR(int fd, fd_set *set);
功能:将指定的set集合中编号为fd的描述符号删除。
int FD_ISSET(int fd, fd_set *set);
功能:判断值为fd的描述符是否在set集合中,
如果在则返回真,否则返回假。
void FD_SET(int fd, fd_set *set);
功能:将指定的fd描述符,添加到set集合中。
void FD_ZERO(fd_set *set);
功能:将指定的set集合中所有描述符删除。
select poll epoll的区别
1. select
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的调用一般要注意几点:
① readfds等是指针结果参数,会被函数修改,所以一般会另外定义一个allread_fdset,保持全部要监听读的句柄,将它的拷贝传递给select函数,返回可读的句柄集合,类型fdset支持赋值运算符=;
② 要注意计算nfds,当新增监听句柄时比较容易修改,当减少监听句柄时较麻烦些,如果要精确修改需要遍历或者采用最大堆等数据结构维护这个句柄集,以方便的找到第二大的句柄,或者干脆在减少监听句柄时不管nfds;
③ timeout如果为NULL表示阻塞等,如果timeout指向的时间为0,表示非阻塞,否则表示select的超时时间;
④ select返回-1表示错误,返回0表示超时时间到没有监听到的事件发生,返回正数表示监听到的所有事件数(包括可读,可写,异常),通常在处理事件时 会利用这个返回值来提高效率,避免不必要的事件触发检查。(比如总共只有一个事件,已经在可读集合中处理了一个事件,则可写和异常就没必要再去遍历句柄集 判断是否发生事件了);
⑤ Linux的实现中select返回时会将timeout修改为剩余时间,所以重复使用timeout需要注意。
select的缺点在于:
① 由于描述符集合set的限制,每个set最多只能监听FD_SETSIZE(在Linux上是1024)个句柄(不同机器可能不一样);
② 返回的可读集合是个fdset类型,需要对所有的监听读句柄一一进行FD_ISSET的测试来判断是否可读;
③ nfds的存在就是为了解决select的效率问题(select遍历nfds个文件描述符,判断每个描述符是否是自己关心的,对关心的描述符判断是否发生事件)。但是解决不彻底,比如如果只监听0和1000两个句柄,select需要遍历1001个句柄来检查事件。
3. epoll
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll 解决了select和poll的几个性能上的缺陷:①不限制监听的描述符个数(poll也是),只受进程打开描述符总数的限制;②监听性能不随着监听描述 符数的增加而增加,是O(1)的,不再是轮询描述符来探测事件,而是由描述符主动上报事件;③使用共享内存的方式,不在用户和内核之间反复传递监听的描述 符信息;④返回参数中就是触发事件的列表,不用再遍历输入事件表查询各个事件是否被触发。
epoll显著提高性能的前提是:监听大量描述符,并且每次触发事件的描述符文件非常少。
epoll的另外区别是:①epoll创建了描述符,记得close;②支持水平触发和边沿触发。