Linux C下如何实现epoll和线程池 (linux c epoll 线程池)

在编写Linux C程序的过程中,epoll和线程池是非常常用的两个技术。epoll是一种高效的I/O多路复用机制,可以监听多个文件描述符的状态,而线程池则是一种处理多个任务的机制,可以节省线程创建和销毁的开销,提高程序效率。本文将介绍如何在Linux C下实现epoll和线程池。

一、epoll的实现

1. 创建一个epoll实例

在使用epoll机制前,首先需要创建一个epoll实例。epoll机制中的epoll_instance_t结构体用于存储epoll实例信息,包括文件描述符、epoll事件数组和其他相关参数。创建epoll实例的代码如下:

“`C

int epoll_fd = epoll_create(1024);

“`

这里epoll_create()函数的参数用于指定epoll实例的大小。这里指定的是1024。

2. 将文件描述符添加到epoll实例中

在使用epoll机制时,需要将需要监听的文件描述符添加到epoll实例中。epoll机制中的epoll_event_t结构体用于存储文件描述符的状态,包括读、写和异常。将文件描述符添加到epoll实例中的代码如下:

“`C

struct epoll_event event;

event.events = EPOLLIN | EPOLLET;

event.data.fd = sockfd;

epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event);

“`

这里的sockfd表示需要监听的文件描述符,event.events表示需要监听的事件类型,EPOLLIN表示读事件,EPOLLET表示边缘触发模式。event.data.fd表示存储文件描述符的结构体。将文件描述符添加到epoll实例中的函数是epoll_ctl()。

3. 轮询epoll实例

将文件描述符添加到epoll实例中后,需要轮询epoll实例中的文件描述符状态。epoll机制中的epoll_wt()函数可以实现这一功能。epoll_wt()函数会阻塞,直到有文件描述符状态发生变化。相关代码如下:

“`C

struct epoll_event events[MAX_EVENTS];

int nfds = epoll_wt(epoll_fd, events, MAX_EVENTS, -1);

for (int i = 0; i

if (events[i].data.fd == sockfd) {

if (events[i].events & EPOLLIN) {

// 有数据可读

}

if (events[i].events & EPOLLOUT) {

// 可写

}

if (events[i].events & EPOLLERR) {

// 出错

}

}

}

“`

这里的MAX_EVENTS表示可以监听的文件描述符数量,epoll_wt()函数的第四个参数为阻塞时间,默认为-1。

二、线程池的实现

1. 创建线程池

线程池的创建过程中,需要定义一个线程池结构体,包括线程池大小、线程数组和任务队列等信息。相关代码如下:

“`C

typedef struct {

pthread_mutex_t lock;

pthread_cond_t cond;

pthread_t *threads;

task_t *queue;

int thread_count;

int queue_size;

int queue_count;

int queue_head;

int queue_tl;

int shutdown;

} threadpool_t;

“`

这里的task_t结构体用于存储任务信息。定义完线程池结构体后,需要初始化线程池,相关代码如下:

“`C

threadpool_t *threadpool_create(int thread_count, int queue_size) {

threadpool_t *pool;

pool = (threadpool_t *)malloc(sizeof(threadpool_t));

pool->thread_count = 0;

pool->queue_count = 0;

pool->queue_head = 0;

pool->queue_tl = 0;

pool->shutdown = 0;

pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * thread_count);

pool->queue = (task_t *)malloc(sizeof(task_t) * queue_size);

pthread_mutex_init(&pool->lock, NULL);

pthread_cond_init(&pool->cond, NULL);

for (int i = 0; i

pthread_create(&pool->threads[i], NULL, thread_process, (void *)pool);

pool->thread_count++;

}

return pool;

}

“`

线程数和任务队列大小都需要在初始化线程池时指定。线程池中的每个线程的执行函数为thread_process(),与任务队列任务处理函数的函数名相同,便于线程与任务的绑定。

2. 将任务添加到线程池中

将任务添加到线程池时,需要将任务信息添加到任务队列中,并唤醒一个线程处理任务,相关代码如下:

“`C

void threadpool_add_task(threadpool_t *pool, task_t *task) {

pthread_mutex_lock(&pool->lock);

pool->queue[pool->queue_tl] = *task;

pool->queue_count++;

if (pool->queue_tl++ == pool->queue_size) {

pool->queue_tl = 0;

}

pthread_cond_signal(&pool->cond);

pthread_mutex_unlock(&pool->lock);

}

“`

任务队列时循环队列,队列满时会从队头覆盖,队列为空时会阻塞等待任务添加。

3. 销毁线程池

销毁线程池时,需要将线程池的所有线程退出,并释放线程池内存空间。相关代码如下:

“`C

void threadpool_destroy(threadpool_t *pool) {

if (pool->shutdown == 1) {

return;

}

pool->shutdown = 1;

pthread_cond_broadcast(&pool->cond);

for (int i = 0; i thread_count; i++) {

pthread_join(pool->threads[i], NULL);

}

free(pool->threads);

free(pool->queue);

pthread_mutex_destroy(&pool->lock);

pthread_cond_destroy(&pool->cond);

free(pool);

}

“`

在销毁线程池时,需要释放线程池内存空间,并调用相关线程和锁的销毁函数进行释放。

综上所述,epoll和线程池是两个常用的技术,能够有效提高程序效率和并发处理能力。在Linux C实现epoll和线程池时,需要理解epoll和线程池的基本原理和实现流程,并结合相关函数和数据结构进行编程实现。

相关问题拓展阅读:

epoll知识点总结

epoll是linux IO多路复用的管理机制,现在是linux平台高性能网络io必要的组件。

理解内核epoll的运行原理,需要从四方面来理解:

1.epoll的数据结构。2.epoll的线程安全。

3.epoll的内核回调。4.epoll的LT与ET。

主要两个结构体 eventpoll 与 epitem。

eventpoll是每一个epoll所对应的,epitem是每一个中闭察IO所对应的事件。

数据结构图下图所示

list用来存储准备就绪的IO,内核IO准备就绪的时候,会执行epoll_event_callback的回调函数,将epitem添加到list中;当epoll_wait激活重新运行的时候,将list的epitem逐一copy到events参数中。

rbtree用来存储所有的io数据,方便快速通过io_fd查找;epoll_ctl执行EPOLL_CTL_ADD操作时,将epitem添加到rbtree中;epoll_ctl执行EPOLL_CTL_DEL操作时,将epitem从retree中删除。

     以下几个包括list操作,rbtree操作,epoll_wait的等待需要加锁。

    list使用最小粒度的spinlock锁,避免多核竞争。

    rbtree的添加使用互斥锁,

    epoll_wait采用pthread_cond_wait;

1.tcp三次握手,对端反馈ack,socket进入rcvd状态,需要将监听的socket的event置为EPOLLIN,此时标识可以进入到accept读取socket数据。

2.established状态时,收到数据,将socket的event置为EPOLLIN状态。

3.established状态时 收到fin,socket进入close_Wait,需要将socket的event设置为EPOLLIN,读取断开信息

4 .   检测到socket的send状态,cwnd >0可以发送的数据,需要将socket置为EPOLLOUT。

LT(水平触发):socket接收缓冲区不为空 有数据可读,读事件一直触发;socket发送缓冲区不满,可以继续写入数据,写事件一直触发。

ET(边缘触发):socket接收缓冲区变化时触发读事件,空的接收缓冲区刚接收到数据时触发读事件;socket发送缓冲区状态发生变化时触发写事件,即满的缓冲区刚空出空间时触发读事件。

LT的处理过程:

    accept一个连接,添加到epoll中监听EPOLLIN事件。

    当EPOLLIN事件到达时,read fd中的数据并处理,

    当需要写出数据时,把数据write到fd中;如果数卖茄据较大,无法一次性写出,那么在epoll中监听EPOLLOUT事件。

    当EPOLLOUT事件到达时,继续把数据write到fd中 ;如果数据写出完毕,那么在epoll中关闭EPOLLOUT事件。

ET的处理过程:

    accept一个连接,添加到epoll中监听EPOLLIN|EPOLLOUT事件

    当EPOLLIN事件到达时,read fd中数据并处理,read需要一直读,直到返回EAGAIN为止

    当需要写出数据时,把数据write到fd中,直到数据全部写完或者write返回EAGAIN

    当EPOLLOUT事件到达时,继续把数据write到fd中,直到数据全部写完,或者write返回EAGAIN

accept要考虑两个问题:

阻塞模式accept存在的问题:TCP连接被客户端夭折,即服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就一直阻塞到accept调用上,直到其他某个客户建立一个新的连接为止。在此期间,服务器 单纯阻塞在accept调用上,就绪队列上其他描述符都得不到处理。解决办法是把监听的套接口设置成非阻塞的,客户端在在服务器端调用accept之前中止某个连接时,accept调用态衫可以立即返回-1。

ET模式accept存在的问题:

    多个连接同时到达,,服务器TCP就行连接瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理,解决办法是,while循环 中accpet调用,处理完accept就绪队列中所有连接后再退出循环。如何知道是否处理完所有连接,accept返回-1并且error设置为errno设置为EAGAIN便是所有连接都处理完。

LT 只要event为EPOLLIN时就能不断调用回调函数

ET 如果从EPOLLOUT变化为EPOLLIN时候,就会触发。

Linux系统I/O模型及select、poll、epoll原理和应用

理解Linux的IO模型之前,首先要了解一些基本概念,才能理解这些IO模型设计的依据

操作系统使用虚拟内旦谈磨存来映射物理内存,对于32位的操作系统来说,虚拟地址空间为4G(2^32)。操作系统的核心是内核,为了保护用户进程不能直接操作内核,保证内核安全,操作系统将虚拟地址空间划分为内核空间和用户空间。内核可以访问全部的地址空间,拥有访问底层硬件设备的权限,普通的应用程序需要访问硬件设备必须通过

系统调用

来实现。

对于Linux系统来说,将虚拟内存的更高1G字节的空间作为内核空间仅供内核使用,低3G字节的空间供用户进程使用,称为用户空间。

又被称为标准I/O,大多数文件系统的默认I/O都是缓存I/O。在Linux系统的缓存I/O机制中,操作系统会将I/O的数据缓存在页缓存(内存)中,也就是数据先被拷贝到内核的缓冲区(内核地址空间),然后才会从内核缓冲区拷贝到应用程序的缓冲区(用户地址空间)。

这种方式很明显的缺点就是数据传输过程中需要再应用程序地址空间和内核空间进行多次数据拷贝操作,这些操作带来的CPU以及内存的开销是非常大的。

由于Linux系统采用的缓存I/O模式,对于一次I/O访问,以读操作举例,数据先会被拷贝到内核缓冲区,然后才会从内核缓冲区拷贝到应用程序的缓存区,当一个read系统调用发生的时候,会经历两个阶段:

正是因为这两个状态,Linux系统才产生了多种不同的网络I/O模式的方案

Linux系统默认情况下所有socke都是blocking的,一个读操作流程如下:

以UDP socket为例,当用户进程调用了recvfrom系统调用,如果数据还没准备好,应用进程被阻塞,内核直到数据到来且将数据从内核缓冲区拷贝到了应用进程缓冲区,然后向用户进程返回结果,用户进程才解除block状态,重新运行起来。

阻塞模行下只是阻塞了当前的应用进程,其他进程还可以执行,不消耗CPU时间,CPU的利用率较高。

Linux可以设置socket为非阻塞的,非阻塞模式下执行一个读操作流程如下:

当用户进程发出recvfrom系统调用时,如果kernel中的数据还没准备好,模斗recvfrom会立即返回一个error结果,不会阻塞用户进程,用户进程收到error时知道数据还没准备好,过一会再调用recvfrom,直到kernel中的数据准备好了,内核就立即将数据拷贝到用户内存然后返回ok,这个过程需要用户进程去轮询内核数据是否准备好。

非阻塞模型下由于要处理更多的系统调用,因此CPU利用率比较低。

应用进程使用sigaction系统调用,内核立即返回,等到kernel数据准备好时会给用户进程发送一个信号,告诉用户进程可以进行IO操作了,然后用户进程再调用IO系统调用如recvfrom,将数据从内核缓冲区拷贝到应用进程。流程如下:

相比于轮询的方式,不需要多次系统调用轮询,信号驱动IO的CPU利用率更高。

异步IO模型与其他模型更大的区别是,异步IO在系统调用返回的时候所有操作都已经完成,应用进程既不需要等待数据准备,也不需要在数据到来后等待数据从内核缓冲区拷贝到用户缓冲区,流程如下:

在数据拷贝完成后,kernel会给用户进程发送一个信号告诉其read操作完成了。

是用select、poll等待数据,可以等待多个socket中的任一个变为可读,这一过程会被阻塞,当某个套接字数据到来时返回,之后再用recvfrom系统调用把数据从内核缓存区复制到用户进程,流程如下:

流程类似阻塞IO,甚至比阻塞IO更差,多使用了一个系统调用,但是IO多路复用更大的侍兄特点是让单个进程能同时处理多个IO事件的能力,又被称为事件驱动IO,相比于多线程模型,IO复用模型不需要线程的创建、切换、销毁,系统开销更小,适合高并发的场景。

select是IO多路复用模型的一种实现,当select函数返回后可以通过轮询fdset来找到就绪的socket。

优点是几乎所有平台都支持,缺点在于能够监听的fd数量有限,Linux系统上一般为1024,是写死在宏定义中的,要修改需要重新编译内核。而且每次都要把所有的fd在用户空间和内核空间拷贝,这个操作是比较耗时的。

poll和select基本相同,不同的是poll没有更大fd数量限制(实际也会受到物理资源的限制,因为系统的fd数量是有限的),而且提供了更多的时间类型。

总结:select和poll都需要在返回后通过轮询的方式检查就绪的socket,事实上同时连的大量socket在一个时刻只有很少的处于就绪状态,因此随着监视的描述符数量的变多,其性能也会逐渐下降。

epoll是select和poll的改进版本,更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll_create()用来创建一个epoll句柄。

epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个就绪链表中管理。

epoll_wait() 可以从就绪链表中得到事件完成的描述符,因此进程不需要通过轮询来获得事件完成的描述符。

当epoll_wait检测到描述符IO事件发生并且通知给应用程序时,应用程序可以不立即处理该事件,下次调用epoll_wait还会再次通知该事件,支持block和nonblocking socket。

当epoll_wait检测到描述符IO事件发生并且通知给应用程序时,应用程序需要立即处理该事件,如果不立即处理,下次调用epoll_wait不会再次通知该事件。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用nonblocking socket,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

【segmentfault】 Linux IO模式及 select、poll、epoll详解

【GitHub】 CyC2023/CS-Notes

linux c epoll 线程池的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于linux c epoll 线程池,Linux C下如何实现epoll和线程池,epoll知识点总结,Linux系统I/O模型及select、poll、epoll原理和应用的信息别忘了在本站进行查找喔。


数据运维技术 » Linux C下如何实现epoll和线程池 (linux c epoll 线程池)