目录
1、IO多路复用模型是什么
2、应用程序
2.1 select
2.1.1 select的特点
2.1.2 select的使用
2.1.3 fd_set操作函数
2.1.4 fd_set表的结构
2.1.5 应用程序:监听两个文件
2.2 poll
2.2.1 poll 的特点
2.2.2 poll的使用
2.2.3 应用程序:监听一个文件
2.3 epoll
2.3.1 epoll 的特点
2.3.2 epoll_create()
2.3.3 epoll_ctl()
2.3.4 epoll_wait()
2.3.5 应用程序:同时监听十个文件
3、驱动程序
3.1 poll操作函数
3.2 虚拟文件系统层的实现
3.2.1 select 的系统调用
3.2.2 poll的系统调用
4、一些知识点理解
1、IO多路复用模型是什么
在同一个APP应用程序同时监听多个硬件的数据,此时就需要使用I0多路复用机制中的select/poll/epoll来完成多个文件描述符的监听的过程,如果所有的文件描述符对应的数据都没有准备好,进程休眠。如果有一个或者多个硬件的数据准备好了,就会唤醒这个休眠的进程。select/poll/epoll 就会返回,返回后从表中找到数据准备好的文件描述符,从对应的文件描述符中将数据拿到即可。
2、应用程序
2.1 select
2.1.1 select的特点
1. select 监听的最大文件描述是1024个
2. select有清空表的过程,需要反复从用户空间向内核空间拷贝表,效率低
3.当有文件描述符的对应驱动的数据准备好的时候,需要再次遍历找到准备好的文件描述符,效率低
2.1.2 select的使用
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
//nfds:所要监视的这三类文件描述集合中,最大文件描述符加1
//readfds、writefds、exceptfds:分别代表读表,写表,异常表
//timeout:超时时间struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微妙 */
};
2.1.3 fd_set操作函数
void FD_ZERO(fd_set *set) //清空fd_set集合,即让fd_set集合不再包含任何文件句柄。
void FD_SET(int fd, fd_set *set) //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *set) //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *set) //检测fd在fdset集合中的状态是否变化,//当检测到fd状态发生变化时返回真,否则,返回假//(也可以认为集合中指定的文件描述符是否可以读写)。
2.1.4 fd_set表的结构
select最多能监听1024个文件描述符
typedef __kernel_fd_set fd_set;typedef struct { //__FD_SETSIZE / (8 * sizeof(long) = 32unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;#define __FD_SETSIZE 1024
2.1.5 应用程序:监听两个文件
int main(int argc, char **argv)
{int fd1, fd2;int ret;fd_set rfds;char buf[128] = {0};fd1 = open("/dev/mycdev0", O_RDWR | O_NONBLOCK);if (fd1 < 0){PRINT_ERR("open mycdev error");}fd2 = open("/dev/input/mouse0", O_RDWR | O_NONBLOCK);if (fd2 < 0){PRINT_ERR("open mouse error");}while(1) {FD_ZERO(&rfds);FD_SET(fd1,&rfds);FD_SET(fd2,&rfds);ret = select(fd2+1,&rfds,NULL,NULL,NULL); if(ret == -1) {PRINT_ERR("select error");}if(FD_ISSET(fd1, &rfds)) {memset(buf,0,sizeof(buf));read(fd1, buf, sizeof(buf));printf("mycdev:buf = %s\n", buf);}if(FD_ISSET(fd2, &rfds)) {memset(buf,0,sizeof(buf));read(fd2, buf, sizeof(buf));printf("mouse:buf = %s\n", buf);} }close(fd1);close(fd2);return 0;
}
2.2 poll
2.2.1 poll 的特点
1. poll监听的最大文件描述没有个数限制
2. poll没有清空表的过程效率高
3.当有文件描述符的对应驱动的数据准备好的时候,需要再次遍历找到准备好的文件描述符,效率低
2.2.2 poll的使用
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
//fds:要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd类型的
//nfds:poll函数要监视的文件描述符数量。
//timeout:超时时间,单位为ms。//返回值:返回 revents域中不为0的 pollfd结构体个数,也就是发生事件或错误的文件描述符数量
// 0,超时
// -1,发生错误,并且设置 errno为错误类型struct pollfd { int fd; /* 文件描述符 */ short events; /* 请求的事件 */ short revents; /* 返回的事件 */
};
//fd:要监视的文件描述符,如果fd无效的话那么 events监视事件也就无效,并且 revents返回 0。
//events:要监视的事件
event可监视的事件类型如下:
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
2.2.3 应用程序:监听一个文件
int main(int argc, char *argv[])
{struct pollfd fds[1];int fd, ret;char buf[128];fd = open(argv[1], O_RDWR | O_NONBLOCK); /* 非阻塞打开 */if(fd < 0) {PRINT_ERR("open error");while(1) {fds[0].fd = fd; fds[0].events = POLLIN;ret = poll(&fds, 1, 500); /* 超时500ms */if ((ret == 1) && (fds[0].revents & POLLIN)) { memset(buf, 0, sizeof(buf));read(fd, buf, sizeof(buf));printf("data = %s\n", buf);}}close(fd);return 0;
}
2.3 epoll
2.3.1 epoll 的特点
1. epoll监听的最大文件描述没有个数限制
2. epoll没有清空表的过程效率高
3. epoll当在休眠的时候,如果有驱动的数据准备好,epoll能 直接拿到准备好的文件描述符,不需要遍历,效率高。
2.3.2 epoll_create()
int epoll_create(int size)
//size 从 Linux2.6.8开始此参数已经没有意义了,随便填写一个大于 0的值就可以。
//返回值 epoll句柄,如果为 -1的话表示创建失败。
2.3.3 epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd:要操作的epoll句柄,也就是使用epoll_create函数创建的epoll句柄。
//op:表示要对 epfd(epoll句柄)进行的操作,可以设置为:
// EPOLL_CTL_ADD 向epfd添加文件参数fd表示的描述符。
// EPOLL_CTL_MOD 修改参数fd的event事件。
// EPOLL_CTL_DEL 从epfd中删除fd描述符。
//fd:要监视的文件描述符。
//event:要监视的事件类型,为 epoll_event结构体类型指针struct epoll_event { uint32_t events; /* epoll事件 */ epoll_data_t data; /* 用户数据 */
};typedef union epoll_data {void *ptr;int fd; //一般来说,用这个uint32_t u32;uint64_t u64;
} epoll_data_t;
event可选的数据有以下:
EPOLLIN 有数据可以读取。
EPOLLOUT 可以写数据。EPOLLPRI 有紧急的数据需要读取。
EPOLLERR 指定的文件描述符发生错误。
EPOLLHUP 指定的文件描述符挂起。
EPOLLET 设置 epoll为边沿触发,默认触发模式为水平触发。
EPOLLONESHOT 一次性的监视,当监视完成以后还需要再次监视某个 fd,那么就需要将
fd重新添加到 epoll里面。
2.3.4 epoll_wait()
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
//epfd:epoll的文件描述符
//events:返回准备好的事件结构体的首地址
//maxevents:最大的文件描述符的个数
//timeout:超时时间,单位为 ms。
// >0:毫秒超时
// =0:
// -1:
//返回值: 0,超时
// -1,失败
// 其他值,准备就绪的文件描述符数量
2.3.5 应用程序:同时监听十个文件
int main(int argc, char *argv[])
{int fd, ret;int epfd;struct epoll_event event; struct epoll_event revents[10];char buf[128];if((epfd == epoll_create(10)) == -1)PRINT_ERR("epoll create error"); for (int i = 1; i < argc; i++) {if ((fd = open(argv[i], O_RDWR | O_NONBLOCK)) == -1)PRINT_ERR("open error");event.events = EPOLLIN;event.data.fd = fd;if(epoll_ctl(epfd, EPOLL_CTL_ADD,fd,&event))PRINT_ERR("epoll ctl error");}while(1) {ret = epoll_wait(epfd, revents,10,-1);if (ret == -1) PRINT_ERR("epoll wait error");for(int i=0; i<ret;i++){if(revents[i].events & EPOLLIN) {memset(buf, 0, sizeof(buf));read(revents[i].data.fd, buf, sizeof(buf));printf("fd = %d,data = %s\n", revents[i].data.fd, buf);}}}for (int i = 0; i < getdtablesize(); i++) {close(i);}return 0;
}
3、驱动程序
3.1 poll操作函数
一般来说,都是在中断函数中去唤醒,但是写个中断又有点麻烦,所以这里采用的是另起一个进程,在write函数中去唤醒进程。
//定义等待队列头
wait_queue_head_t wq;
int condition = 0;ssize_t mycdev_read(struct file *file, char __user * ubuf, size_t size, loff_t * offs)
{//将condition设置为假condition = 0;return size;
}ssize_t mycdev_write(struct file *file, const char __user * ubuf, size_t size, loff_t * offs)
{//唤醒condition = 1;wake_up_interruptible(&wq);return size;
}unsigned int mycdev_poll (struct file *file, struct poll_table_struct *wait)
{unsigned int mask = 0;poll_wait(file,&wq,wait);if(condition) mask |= POLLIN; return mask;
}const struct file_operations fops = {.read = mycdev_read,.write = mycdev_write,.poll = mycdev_poll,};static int __init mycdev_init(void)
{//初始化等待队列头init_waitqueue_head(&wq);
}
3.2 虚拟文件系统层的实现
poll函数不可能在驱动中阻塞,要不然调用了一个poll就会阻塞,但是我们会同时监听好多个文件,所以堵塞的地方是在VFS层
3.2.1 select 的系统调用
//应用层
ret = select(fd2 + 1, &rfds, NULL, NULL, NULL);//select的调用流程
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,fd_set __user *, exp, struct timeval __user *, tvp)
}ret = core_sys_select(n, inp, outp, exp, to);
}int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,fd_set __user *exp, struct timespec64 *end_time)
{//1、校验最大文件描述符的值//2、在内核空间分配六个表的内存,将用户空间的表拷贝到内核空间fds.in fds.out fds.ex// 将保存准备好的文件描述符的表先清空//3、在do_select中遍历文件描述符,去看哪个表准备好了ret = do_select(n, & fds, end_time);//4、如果准备好了,将准备好的文件描述符通过copy_to_user拷贝到用户空间}int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{//1、校验最大的文件描述符//2、初始化一个函数,在这个函数内会定义等待队列项,将等待队列项放入等待队列头后// 这个函数会被驱动回调//在这个函数中并没有创建一个等待队列头,是驱动向VFS提交的等待队列头poll_initwait(&table); //3、从文件描述符i中取到了file结构体,然后调用到了驱动中的poll函数//fd->fd_array[fd]->file->fops->poll//循环遍历文件描述符,得到驱动给VFS返回的maskf = fdget(i);if (f.file) {const struct file_operations *f_op;f_op = f.file->f_op;if (f_op->poll) {mask = (*f_op->poll)(f.file, wait);}}//4、如果mask为假代表驱动的数据没有准备好,反之准备好了if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack))
}//可以看到传入的参数是文件描述符,但是返回值是fd,可以从fd中取到file结构体
static inline struct fd fdget(unsigned int fd)
{return __to_fd(__fdget(fd));
}struct fd {struct file *file;unsigned int flags;
};
void poll_initwait(struct poll_wqueues *pwq)
{init_poll_funcptr(&pwq->pt, __pollwait);
}static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,poll_table *p)
{struct poll_table_entry *entry = poll_get_entry(pwq);
}struct poll_table_entry {struct file *filp;unsigned long key;wait_queue_t wait;wait_queue_head_t *wait_address;
};typedef struct __wait_queue wait_queue_t;//这个就是在IO阻塞有提到过的等待队列项
struct __wait_queue {unsigned int flags;void *private;wait_queue_func_t func;struct list_head task_list;
};
int poll_schedule_timeout(struct poll_wqueues *pwq, int state,ktime_t *expires, unsigned long slack)
{rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS);
}int __sched schedule_hrtimeout_range(ktime_t *expires, u64 delta,const enum hrtimer_mode mode)
{return schedule_hrtimeout_range_clock(expires, delta, mode,CLOCK_MONOTONIC);
}schedule_hrtimeout_range_clock(ktime_t *expires, u64 delta,const enum hrtimer_mode mode, int clock)
{//放弃CPUschedule();
}
3.2.2 poll的系统调用
//poll的调用流程
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,int, timeout_msecs)
{ret = do_sys_poll(ufds, nfds, to);
}int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,struct timespec64 *end_time)
{poll_initwait(&table);fdcount = do_poll(head, &table, end_time);
}void poll_initwait(struct poll_wqueues *pwq)
{//pollinit看下部分init_poll_funcptr(&pwq->pt, __pollwait);
}static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{//qproc = __pollwaitpt->_qproc = qproc;pt->_key = ~0UL; /* all events enabled */
}static int do_poll(struct poll_list *list, struct poll_wqueues *wait,struct timespec64 *end_time)
{do_pollfd(pfd, pt, &can_busy_loop, busy_flag));poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack)
}static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait,bool *can_busy_poll,unsigned int busy_flag)
{struct fd f = fdget(fd);mask = POLLNVAL;if (f.file) {mask = DEFAULT_POLLMASK;if (f.file->f_op->poll) {mask = f.file->f_op->poll(f.file, pwait);}return mask;}
}//可以看到传入的参数是文件描述符,但是返回值是fd,可以从fd中取到file结构体
static inline struct fd fdget(unsigned int fd)
{return __to_fd(__fdget(fd));
}struct fd {struct file *file;unsigned int flags;
};
void poll_initwait(struct poll_wqueues *pwq)
{init_poll_funcptr(&pwq->pt, __pollwait);
}static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,poll_table *p)
{struct poll_table_entry *entry = poll_get_entry(pwq);
}struct poll_table_entry {struct file *filp;unsigned long key;wait_queue_t wait;wait_queue_head_t *wait_address;
};typedef struct __wait_queue wait_queue_t;//这个就是在IO阻塞有提到过的等待队列项
struct __wait_queue {unsigned int flags;void *private;wait_queue_func_t func;struct list_head task_list;
};
int poll_schedule_timeout(struct poll_wqueues *pwq, int state,ktime_t *expires, unsigned long slack)
{rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS);
}int __sched schedule_hrtimeout_range(ktime_t *expires, u64 delta,const enum hrtimer_mode mode)
{return schedule_hrtimeout_range_clock(expires, delta, mode,CLOCK_MONOTONIC);
}schedule_hrtimeout_range_clock(ktime_t *expires, u64 delta,const enum hrtimer_mode mode, int clock)
{//放弃CPUschedule();
}
4、一些知识点理解
1、select/poll/epoll打开方式都为非阻塞方式打开
open(argv[i], O_RDWR | O_NONBLOCK)
2、epoll是最牛的IO多路复用的机制,所以基本上实际开发中要用的就是epoll
3、阻塞与非阻塞是对于文件而言的,而不是指read、write等的属性。
4、设备驱动的poll()本身不会阻塞,但是poll()、select()和epoll()系统调用则会阻塞地等待文件描述符集合中的至少一个可访问或超时。
5、同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO
6、非阻塞IO,它只有是检查无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间(等着水将水杯装满),因此它还是同步IO。
7、阻塞IO模型(进程在内核状态下等待)使用recv的默认参数一直等数据直到拷贝到用户空间,这段时间内进程始终阻塞。
8、select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。