写这个的原因也很简单,很早以前我会用select,就靠这个在之前的公司里面混了几年,有几个项目都是用的select。现在来看不算啥,不过当时也算高科技了。
后面已知看到说epoll性能更好云云,想试试也很久了,今天终于提起精神试试。。
代码其实还是很简单,如下:
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
// 设置文件描述符为非阻塞模式
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl");
return -1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl");
return -1;
}
return 0;
}
int main() {
// 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
// 假设我们有两个文件描述符:stdin和一个假设的fd
int fd1 = STDIN_FILENO; // 标准输入
int fd2 = open("/path/to/some/file", O_RDONLY); // 打开一个文件
if (fd2 == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 设置文件描述符为非阻塞模式
set_nonblocking(fd1);
set_nonblocking(fd2);
// 将文件描述符添加到epoll实例中
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 监听输入事件和边缘触发
event.data.fd = fd1;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd1, &event) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
/*event.data.fd = fd2;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd2, &event) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}*/
// 事件循环
struct epoll_event events[10];
while (1) {
int num_fds = epoll_wait(epoll_fd, events, 10, -1);
if (num_fds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < num_fds; ++i) {
if (events[i].events & EPOLLIN) {
char buf[512];
int bytes_read = read(events[i].data.fd, buf, sizeof(buf));
if (bytes_read == -1) {
perror("read");
} else if (bytes_read == 0) {
// EOF
printf("EOF on fd %d\n", events[i].data.fd);
close(events[i].data.fd);
} else {
printf("Read %d bytes on fd %d: %.*s\n", bytes_read, events[i].data.fd, bytes_read, buf);
}
}
}
}
close(epoll_fd);
close(fd2);
return 0;
}
这个代码是gpt4.0来的,本来以为可以的,结果还是不行。目前的GPT只要稍微偏一点或者复杂一点,出来的代码都没法用。能编过但是没法运行,会报说没有权限。
epoll_ctl: Operation not permitted
查了下原来是epoll_ctl 支持管道,FIFO,套接字,POSIX消息队列,终端,设备等,但是不支持普通文件或目录的fd。把上面的fd2屏蔽了就行了。
其实从应用来看吧,和select几乎是一模一样,就是换了点名字。
再试了一个很简单epoll和共享内存连用的,使用消息进行通知,感觉还可以。
client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define SHM_NAME "my_shm"
#define SHM_SIZE 1024
#define FIFO_NAME "/tmp/my_fifo"
int create_shared_memory() {
int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (fd == -1) {
perror("shm_open");
exit(EXIT_FAILURE);
}
if (ftruncate(fd, SHM_SIZE) == -1) {
perror("ftruncate");
exit(EXIT_FAILURE);
}
return fd;
}
void write_to_shared_memory(int fd, const char *message) {
void *shm_ptr = mmap(0, SHM_SIZE, PROT_WRITE, MAP_SHARED, fd, 0);
if (shm_ptr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
strcpy((char *)shm_ptr, message);
munmap(shm_ptr, SHM_SIZE);
}
int main() {
int shm_fd = create_shared_memory();
const char *message = "Hello from shared memory!";
write_to_shared_memory(shm_fd, message);
int fd = open(FIFO_NAME, O_WRONLY);
write(fd, "1", 1);
close(fd);
close(shm_fd);
return 0;
}
server
#include <sys/epoll.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define SHM_NAME "my_shm"
#define SHM_SIZE 1024
#define FIFO_NAME "/tmp/my_fifo"
void read_from_shared_memory(int fd) {
void *shm_ptr = mmap(0, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
if (shm_ptr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
printf("Read from shared memory: %s\n", (char *)shm_ptr);
munmap(shm_ptr, SHM_SIZE);
}
int main() {
int shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open");
exit(EXIT_FAILURE);
}
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.events = EPOLLIN;
int fd = open(FIFO_NAME, O_RDONLY);
event.data.fd = fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
struct epoll_event events[10];
while (1) {
int num_fds = epoll_wait(epoll_fd, events, 10, -1);
if (num_fds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < num_fds; ++i) {
if (events[i].events & EPOLLIN) {
printf("receive shm message.\n");
char buf[1];
if (read(events[i].data.fd, buf, 1) > 0) {
read_from_shared_memory(shm_fd);
}
}
}
}
close(epoll_fd);
close(fd);
close(shm_fd);
return 0;
}
运行的话要先
mkfifo /tmp/my_fifo
touch my_shm
这里还是只是搞了一些简单的用法。
如何区别不同的事件呢?
收到event的时候,判断fd类型即可。
fd == events[i].data.fd
再说说水平模式(Level Triggered, LT)和边缘模式(Edge Triggered, ET),这部分我就没试了,总结一点网上的资料。
两个就是数据来的时候,都会报事件。但是水平模式下,只要数据没读取完,就会一直报。水平模式也就是select的模式,开始用LT就好。
最后说说epoll和select区别:
select:适用于文件描述符数量较少的场景,select是 POSIX 标准的一部分,所有具有良好的可移植性,但性能较差。select 有一个文件描述符数量的限制,通常为 1024。这意味着如果要监控的文件描述符超过 1024,必须调整系统参数或使用epoll。每次调用 select都需要将文件描述符集合从用户态复制到内核态,然后在内核中遍历文件描述符集合,这在文件描述符数量较多时会导致性能下降。
epoll:使用一个事件表在内核中记录文件描述符,因此在监控大量文件描述符时性能更高。事件通知采用回调机制,避免了每次调用时的遍历。没有select那样的文件描述符数量限制,能高效地处理大规模的文件描述符。在水平模式下多了一个边缘模式,用得好效率更高。但epoll仅在 Linux 上可用。
参考: