fd_set 和 pollfd 的趣味解读 🐧🌟
亲爱的读者朋友们,今天我们要探索的是 Linux 世界中两个重要的小伙伴:fd_set 和 pollfd。它们分别是 select 和 poll 函数的得力助手,帮助我们高效管理文件描述符集合。让我们一起来了解它们的神奇之处吧!😊
fd_set 里的小位数组 🤓
fd_set 是一个位数组数据结构,用于表示文件描述符集合。每个文件描述符都有一个对应的位,表示它是否包含在集合中。来看一下 fd_set 的数据结构吧:
typedef struct {
unsigned long fds_bits[FD_SETSIZE / (8 * sizeof(unsigned long))];
} fd_set;
fds_bits:位数组,表示文件描述符集合。FD_SETSIZE:定义集合中可以包含的最大文件描述符数量,通常是 1024。
相关宏定义 🛠️
有了这些宏,操作 fd_set 简直轻松无比:
-
清空集合 (
FD_ZERO)#define FD_ZERO(set) (memset(set, 0, sizeof(fd_set)))- 🧽 清空大法好!:将
fd_set中所有位清零,表示集合中没有文件描述符。
- 🧽 清空大法好!:将
-
添加文件描述符 (
FD_SET)#define FD_SET(fd, set) ((set)->fds_bits[(fd) / (8 * sizeof(unsigned long))] |= (1UL << ((fd) % (8 * sizeof(unsigned long)))))- ➕ 添加小伙伴:将文件描述符添加到
fd_set中,通过位运算将对应位置为 1。
- ➕ 添加小伙伴:将文件描述符添加到
-
移除文件描述符 (
FD_CLR)#define FD_CLR(fd, set) ((set)->fds_bits[(fd) / (8 * sizeof(unsigned long))] &= ~(1UL << ((fd) % (8 * sizeof(unsigned long)))))- ➖ 移除小伙伴:从
fd_set中移除文件描述符,通过位运算将对应位置为 0。
- ➖ 移除小伙伴:从
-
检查文件描述符 (
FD_ISSET)#define FD_ISSET(fd, set) (((set)->fds_bits[(fd) / (8 * sizeof(unsigned long))] & (1UL << ((fd) % (8 * sizeof(unsigned long))))) != 0)- 🔍 检查小伙伴:检查文件描述符是否在
fd_set中,通过位运算检查对应位是否为 1。
- 🔍 检查小伙伴:检查文件描述符是否在
示例代码 🎉
来看一个完整的例子,展示如何使用 fd_set 和相关宏:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
int main() {
fd_set read_fds;
struct timeval timeout;
int fd1 = 0; // 标准输入
int fd2 = 3; // 假设另一个文件描述符,如 socket
// 初始化 timeout
timeout.tv_sec = 5; // 5 秒超时
timeout.tv_usec = 0;
// 清空 read_fds 集合
FD_ZERO(&read_fds);
// 添加文件描述符到 read_fds 集合
FD_SET(fd1, &read_fds);
FD_SET(fd2, &read_fds);
// 获取最大文件描述符值
int max_fd = (fd1 > fd2) ? fd1 : fd2;
// 调用 select 函数,等待事件发生
int result = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
if (result == -1) {
perror("select");
exit(EXIT_FAILURE);
} else if (result == 0) {
printf("Timeout occurred! No data after 5 seconds.\n");
} else {
// 检查哪个文件描述符有数据可读
if (FD_ISSET(fd1, &read_fds)) {
printf("Data is available on fd1 (stdin).\n");
// 处理 fd1 的数据
}
if (FD_ISSET(fd2, &read_fds)) {
printf("Data is available on fd2.\n");
// 处理 fd2 的数据
}
}
return 0;
}
pollfd 数组的小故事 📚✨
pollfd 是 poll 函数的好帮手,用于监控多个文件描述符的事件。我们来看看 pollfd 的结构体定义:
struct pollfd {
int fd; // 文件描述符
short events; // 感兴趣的事件掩码
short revents; // 实际发生的事件掩码
};
poll 函数介绍 📞
poll 函数可以同时监控多个文件描述符的多个事件类型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- fds:指向
pollfd结构体数组的指针,每个结构体描述一个文件描述符及其事件。 - nfds:数组中结构体的数量。
- timeout:等待事件发生的超时时间(毫秒)。
常见的事件 🚥
POLLIN:有数据可读。POLLOUT:写操作不会阻塞。POLLERR:发生错误。POLLHUP:挂起。POLLNVAL:描述符不合法。
示例代码 🎈
以下是一个使用 poll 监控两个文件描述符的例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
#define TIMEOUT 5000 // 超时时间,毫秒
int main() {
struct pollfd fds[2];
int ret;
// 监控标准输入(文件描述符 0)
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
// 监控一个假设的文件描述符 3(如 socket)
fds[1].fd = 3;
fds[1].events = POLLIN;
// 调用 poll 函数
ret = poll(fds, 2, TIMEOUT);
if (ret == -1) {
perror("poll");
exit(EXIT_FAILURE);
} else if (ret == 0) {
printf("Timeout occurred! No data after %d milliseconds.\n", TIMEOUT);
} else {
if (fds[0].revents & POLLIN) {
printf("Data is available on stdin (fd 0).\n");
// 处理标准输入的数据
}
if (fds[1].revents & POLLIN) {
printf("Data is available on fd 3.\n");
// 处理 fd 3 的数据
}
if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) {
printf("Error on stdin (fd 0).\n");
}
if (fds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) {
printf("Error on fd 3.\n");
}
}
return 0;
}
epoll 内核事件表的大智慧 🧠✨
最后,我们来谈谈 epoll,它是 select 和 poll 的进化版,能够更高效地管理大量文件描述符。epoll 提供了出色的性能,特别适合高并发环境。
epoll 的三大操作 🛠️
使用 epoll 主要涉及三个系统调用:
epoll_create1:创建一个新的epoll实例。epoll_ctl:向epoll实例中添加、修改或删除文件描述符。epoll_wait:等待事件发生并返回事件。
epoll 的数据结构 🌟
epoll_event 结构体
struct epoll_event {
uint32_t events; // 事件类型
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events:表示感兴趣的事件类型,如EPOLLIN、EPOLLOUT等。data:用于存储用户数据,通常是文件描述符或指针。
epoll 的系统调用 🌍
epoll_create1
创建一个新的 epoll 实例,返回一个 epoll 文件描述符:
int epoll_create1(int flags);
flags:可以是 0 或EPOLL_CLOEXEC。
epoll_ctl
向 epoll 实例中添加、修改或删除文件描述符:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll实例的文件描述符。op:操作类型,如EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL。fd:要监控的文件描述符。event:描述事件和用户数据的结构体。
epoll_wait
等待事件发生并返回事件:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:epoll实例的文件描述符。events:用来返回事件的数组。maxevents:数组中可以容纳的最大事件数量。timeout:等待事件发生的超时时间,单位为毫秒。
示例代码 ✨
以下是一个使用 epoll 监控两个文件描述符的示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#define MAX_EVENTS 10
#define TIMEOUT 5000 // 超时时间,毫秒
int main() {
int epfd;
struct epoll_event ev, events[MAX_EVENTS];
int nfds, fd1 = 0, fd2 = 3; // 标准输入和一个假设的文件描述符
// 创建 epoll 实例
epfd = epoll_create1(0);
if (epfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
// 添加 fd1 到 epoll 实例
ev.events = EPOLLIN;
ev.data.fd = fd1;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev) == -1) {
perror("epoll_ctl: fd1");
exit(EXIT_FAILURE);
}
// 添加 fd2 到 epoll 实例
ev.events = EPOLLIN;
ev.data.fd = fd2;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev) == -1) {
perror("epoll_ctl: fd2");
exit(EXIT_FAILURE);
}
// 等待事件发生
nfds = epoll_wait(epfd, events, MAX_EVENTS, TIMEOUT);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
// 处理发生的事件
for (int n = 0; n < nfds; ++n) {
if (events[n].data.fd == fd1) {
printf("Data is available on stdin (fd 0).\n");
// 处理标准输入的数据
} else if (events[n].data.fd == fd2) {
printf("Data is available on fd 3.\n");
// 处理 fd 3 的数据
}
}
// 关闭 epoll 实例
close(epfd);
return 0;
}
总结 🎓
fd_set、pollfd 和 epoll 这三位 Linux 小伙伴各有各的妙用。fd_set 适合少量文件描述符,pollfd 能处理更多场景,而 epoll 则是高并发环境中的性能之王。根据你的需求选择合适的工具,将助你在 Linux 网络编程的道路上事半功倍!🌟