一,阻塞IO与非阻塞IO
简介:
在 C 语言中,阻塞 I/O 和非阻塞 I/O 是两种不同的输入 / 输出操作方式,它们在程序的行为和性能方面有很大的区别。
一、阻塞 I/O
-  概念: - 当一个进程进行阻塞 I/O 操作时,如果数据尚未准备好或者输出缓冲区已满,进程会被阻塞,暂停执行,直到 I/O 操作完成。
- 例如,当使用read函数从一个文件描述符读取数据时,如果没有数据可读,进程会一直等待,直到有数据到达或者文件描述符被关闭。
 

-  特点: - 简单直观:编程模型相对简单,容易理解和实现。
- 同步操作:进程在进行 I/O 操作时会等待操作完成,因此是一种同步的操作方式。
- 低并发性:由于进程在进行 I/O 操作时会被阻塞,因此在一个单线程程序中,只能同时进行一个 I/O 操作,降低了系统的并发性。
 
-  示例代码: 
#include <stdio.h>
#include <unistd.h>
int main() {
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    char buffer[1024];
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
    if (bytesRead == -1) {
        perror("read");
        return 1;
    }
    close(fd);
    return 0;
}
在这个例子中,如果test.txt文件中没有数据可读,read函数会阻塞进程,直到有数据可读或者文件描述符被关闭。
二、非阻塞 I/O
-  概念: - 非阻塞 I/O 允许进程在进行 I/O 操作时不会被阻塞。如果数据尚未准备好或者输出缓冲区已满,I/O 函数会立即返回一个错误码,表示操作无法立即完成。
- 进程可以通过轮询的方式不断检查 I/O 状态,直到数据准备好或者操作可以完成。
 

-  特点: - 高并发性:进程在进行 I/O 操作时不会被阻塞,因此可以同时进行多个 I/O 操作,提高了系统的并发性。
- 复杂编程模型:需要进程不断地进行轮询,增加了编程的复杂性。
- 可能浪费 CPU 时间:如果数据一直不可用,进程会不断地进行轮询,浪费 CPU 时间。
 
-  示例代码: 
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main() {
    int fd = open("test.txt", O_RDONLY | O_NONBLOCK);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    char buffer[1024];
    ssize_t bytesRead = 0;
    while ((bytesRead = read(fd, buffer, sizeof(buffer))) == -1 && errno == EAGAIN) {
        // 文件不可读,继续轮询
    }
    if (bytesRead > 0) {
        // 处理读取到的数据
    } else {
        if (bytesRead == 0) {
            // 到达文件末尾
        } else {
            perror("read");
        }
    }
    close(fd);
    return 0;
}
在这个例子中,使用O_NONBLOCK标志打开文件,使文件描述符处于非阻塞模式。如果文件中没有数据可读,read函数会立即返回-1,并且errno被设置为EAGAIN,表示文件不可读。进程可以通过不断地轮询来检查文件是否可读,直到有数据可读或者文件描述符被关闭。
三、阻塞 I/O 和非阻塞 I/O 的选择
-  应用场景: - 阻塞 I/O 适用于简单的程序,其中 I/O 操作相对较少,并且不需要高并发性。例如,一个命令行工具,只需要从标准输入读取数据并进行处理,然后输出结果。
- 非阻塞 I/O 适用于需要高并发性的程序,其中多个 I/O 操作可以同时进行。例如,一个网络服务器,需要同时处理多个客户端的连接请求,并且不能因为一个客户端的 I/O 操作而阻塞其他客户端的请求处理。
 
-  性能考虑: - 阻塞 I/O 在 I/O 操作完成之前会阻塞进程,因此可能会导致程序的响应时间较长。但是,由于进程在进行 I/O 操作时不会消耗 CPU 时间,因此在 I/O 操作频繁的情况下,可能会比非阻塞 I/O 更高效。
- 非阻塞 I/O 需要进程不断地进行轮询,因此会消耗一定的 CPU 时间。但是,由于进程在进行 I/O 操作时不会被阻塞,因此可以同时进行多个 I/O 操作,提高了系统的并发性。在 I/O 操作不频繁的情况下,非阻塞 I/O 可能会比阻塞 I/O 更高效。
 
总之,阻塞 I/O 和非阻塞 I/O 是两种不同的输入 / 输出操作方式,它们在程序的行为和性能方面有很大的区别。在选择使用哪种方式时,需要根据具体的应用场景和性能要求进行考虑。
二,多路复用IO-select
在 C 语言中,多路复用 I/O(I/O multiplexing)是一种可以同时监视多个文件描述符(file descriptor)的输入 / 输出状态的技术。其中,select函数是一种常用的实现多路复用 I/O 的方法。
简介

 
 
一、select函数的概念和用法
 
 
- 函数原型:
   int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
-  参数说明: - nfds:要监视的文件描述符的范围,通常设置为最高文件描述符值加 1。
- readfds、- writefds、- exceptfds:分别是指向要监视的可读、可写和异常文件描述符集合的指针。可以为- NULL,表示不监视相应类型的文件描述符。
- timeout:指定等待的时间限制。可以为- NULL,表示无限期等待;或者设置一个特定的时间值,表示等待的最长时间。
 
-  返回值: - 返回值表示就绪的文件描述符的数量。如果在等待时间内没有任何文件描述符就绪,select返回 0。如果发生错误,返回 -1,并设置errno。
 
- 返回值表示就绪的文件描述符的数量。如果在等待时间内没有任何文件描述符就绪,
-  
  单进程可以处理,但是需要不断检测客户端是否发出 IO 请求,需要不断占用 cpu ,消耗 cpu 资源

二、使用步骤
- 初始化文件描述符集合: 
  - 使用fd_set类型的变量来表示文件描述符集合。可以使用FD_ZERO宏初始化一个空集合,使用FD_SET宏将特定的文件描述符添加到集合中。
 
- 使用
   fd_set readfds;
   FD_ZERO(&readfds);
   FD_SET(socket_fd, &readfds);
- 调用select函数:- 将初始化后的文件描述符集合作为参数传递给select函数,并设置适当的超时时间。
 
- 将初始化后的文件描述符集合作为参数传递给
   struct timeval timeout;
   timeout.tv_sec = 5;
   timeout.tv_usec = 0;
   int ready = select(nfds, &readfds, NULL, NULL, &timeout);
- 检查就绪的文件描述符: 
  - 根据select的返回值,检查哪些文件描述符就绪。可以使用FD_ISSET宏来测试特定的文件描述符是否在就绪集合中。
 
- 根据
   if (ready > 0) {
       if (FD_ISSET(socket_fd, &readfds)) {
           // 处理可读的文件描述符
       }
   } else if (ready == 0) {
       // 超时
   } else {
       // 错误处理
   }
三、示例代码
以下是一个使用select函数实现简单服务器的示例,该服务器可以同时处理多个客户端连接:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/time.h>
#define PORT 8888
#define MAX_CLIENTS 10
void handleClient(int client_fd) {
    char buffer[1024];
    ssize_t bytesRead;
    while ((bytesRead = read(client_fd, buffer, sizeof(buffer))) > 0) {
        // 处理客户端请求
        write(client_fd, buffer, bytesRead);
    }
    close(client_fd);
}
int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    fd_set readfds;
    int max_fd;
    int i;
    // 创建服务器套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    // 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    // 绑定服务器套接字
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }
    // 监听连接请求
    if (listen(server_fd, MAX_CLIENTS) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 初始化文件描述符集合
    FD_ZERO(&readfds);
    FD_SET(server_fd, &readfds);
    max_fd = server_fd;
    while (1) {
        fd_set tmpfds = readfds;
        int ready = select(max_fd + 1, &tmpfds, NULL, NULL, NULL);
        if (ready == -1) {
            perror("select");
            exit(EXIT_FAILURE);
        }
        if (FD_ISSET(server_fd, &tmpfds)) {
            // 有新的连接请求
            client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
            if (client_fd == -1) {
                perror("accept");
                continue;
            }
            FD_SET(client_fd, &readfds);
            if (client_fd > max_fd) {
                max_fd = client_fd;
            }
            printf("New client connected.\n");
        } else {
            // 处理已连接的客户端
            for (i = 0; i <= max_fd; i++) {
                if (FD_ISSET(i, &tmpfds)) {
                    if (i!= server_fd) {
                        handleClient(i);
                        FD_CLR(i, &readfds);
                        if (i == max_fd) {
                            while (FD_ISSET(max_fd, &readfds) == 0 && max_fd > server_fd) {
                                max_fd--;
                            }
                        }
                    }
                }
            }
        }
    }
    close(server_fd);
    return 0;
}在这个例子中,服务器使用select函数来监视服务器套接字和已连接的客户端套接字。当有新的连接请求时,服务器接受连接并将新的客户端套接字添加到文件描述符集合中。当有客户端发送数据时,服务器读取数据并将其回显给客户端。
四、注意事项
- 文件描述符限制:select函数的最大文件描述符数量通常受到系统限制。可以使用FD_SETSIZE宏来查看系统支持的最大文件描述符数量。
- 性能问题:select函数在每次调用时都需要重新设置文件描述符集合,并且在返回时需要遍历所有的文件描述符来确定哪些是就绪的。这可能会导致性能问题,特别是在处理大量文件描述符时。
- 超时处理:可以使用select函数的timeout参数来设置超时时间,以避免无限期地等待。如果超时时间到达,select将返回 0,表示没有文件描述符就绪。
在 C 语言中,poll是另一种实现多路复用 I/O 的方法。与select相比,poll在一些方面有改进。
三,多路复用IO-poll
简介:
 
 
一、poll函数的概念和用法
 
 
- 函数原型:
   int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-  参数说明: - fds:是一个- pollfd结构数组的指针,每个结构表示一个要监视的文件描述符及其事件。
- nfds:是要监视的文件描述符数组的长度。
- timeout:指定等待的时间限制,以毫秒为单位。可以为负值,表示无限期等待;为 0 表示立即返回;为正值表示等待指定的时间。
 
-  返回值: - 返回值表示就绪的文件描述符的数量。如果在等待时间内没有任何文件描述符就绪,poll返回 0。如果发生错误,返回 -1,并设置errno。
 
- 返回值表示就绪的文件描述符的数量。如果在等待时间内没有任何文件描述符就绪,
二、pollfd结构
 
 
pollfd结构通常定义如下:
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 要监视的事件
    short revents;  // 实际发生的事件
};
其中,events成员用于指定要监视的事件类型,revents成员在poll返回时被设置为实际发生的事件类型。
常见的事件类型有:
- POLLIN:表示文件描述符可读。
- POLLOUT:表示文件描述符可写。
- POLLPRI:表示有紧急数据可读。
- POLLERR:表示发生错误。
- POLLHUP:表示挂起。
三、使用步骤
- 定义pollfd结构数组并初始化:
   struct pollfd fds[10];
   fds[0].fd = socket_fd;
   fds[0].events = POLLIN;
- 调用poll函数:
   int ready = poll(fds, 10, -1);
- 检查就绪的文件描述符:
   if (ready > 0) {
       if (fds[i].revents & POLLIN) {
           // 处理可读的文件描述符
       }
   } else if (ready == 0) {
       // 超时
   } else {
       // 错误处理
   }
四、示例代码
以下是一个使用poll实现简单服务器的示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#define PORT 8888
#define MAX_CLIENTS 10
void handleClient(int client_fd) {
    char buffer[1024];
    ssize_t bytesRead;
    while ((bytesRead = read(client_fd, buffer, sizeof(buffer))) > 0) {
        // 处理客户端请求
        write(client_fd, buffer, bytesRead);
    }
    close(client_fd);
}
int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    struct pollfd fds[MAX_CLIENTS + 1];
    int nfds = 1;
    // 创建服务器套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    // 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    // 绑定服务器套接字
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }
    // 监听连接请求
    if (listen(server_fd, MAX_CLIENTS) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 初始化文件描述符数组
    fds[0].fd = server_fd;
    fds[0].events = POLLIN;
    while (1) {
        int ready = poll(fds, nfds, -1);
        if (ready == -1) {
            perror("poll");
            exit(EXIT_FAILURE);
        }
        if (fds[0].revents & POLLIN) {
            // 有新的连接请求
            client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
            if (client_fd == -1) {
                perror("accept");
                continue;
            }
            if (nfds == MAX_CLIENTS + 1) {
                fprintf(stderr, "Too many clients.\n");
                close(client_fd);
            } else {
                fds[nfds].fd = client_fd;
                fds[nfds].events = POLLIN;
                nfds++;
                printf("New client connected.\n");
            }
        } else {
            // 处理已连接的客户端
            for (int i = 1; i < nfds; i++) {
                if (fds[i].revents & POLLIN) {
                    handleClient(fds[i].fd);
                    fds[i].fd = -1;
                    fds[i].events = 0;
                    if (i == nfds - 1) {
                        nfds--;
                    }
                }
            }
        }
    }
    close(server_fd);
    return 0;
}
在这个例子中,服务器使用poll来监视服务器套接字和已连接的客户端套接字。当有新的连接请求时,服务器接受连接并将新的客户端套接字添加到pollfd结构数组中。当有客户端发送数据时,服务器读取数据并将其回显给客户端。
五、poll与select的比较
 
 
- 可监视的文件描述符数量:poll没有最大文件描述符数量的限制,而select通常受到系统限制。
- 参数传递:poll使用pollfd结构数组,不需要像select那样每次重新设置文件描述符集合,减少了一些开销。
- 可移植性:select在不同的平台上更具可移植性,而poll在一些特定的系统上可能表现更好。
总之,poll是一种实现多路复用 I/O 的有效方法,在某些情况下比select更具优势。但在实际应用中,还可以考虑使用更高效的多路复用技术,如epoll。
四,多路复用IO-epoll
在 C 语言中,epoll是一种高效的 I/O 多路复用机制,它克服了传统的select和poll函数在处理大量文件描述符时的性能瓶颈。
简介:
 
 
 
 
 一、epoll的概念和特点
 
 
-  epoll的工作原理:- epoll通过在内核中维护一个事件表,将需要监视的文件描述符及其感兴趣的事件注册到这个事件表中。
- 当文件描述符上有事件发生时,内核会将这些事件通知给应用程序,应用程序可以根据这些通知进行相应的 I/O 操作。
 
-  与 select和poll的比较:- select和- poll在每次调用时都需要遍历所有的文件描述符,检查它们是否有事件发生,这种方式在处理大量文件描述符时效率低下。
- epoll只需要在文件描述符状态发生变化时才会通知应用程序,避免了不必要的遍历,因此在处理大量文件描述符时具有更高的性能。
 
-  epoll的事件触发模式:- epoll支持两种事件触发模式:水平触发(Level Triggered,LT)和边缘触发(Edge Triggered,ET)。
- 在水平触发模式下,只要文件描述符上有事件发生,epoll就会不断地通知应用程序,直到应用程序对该事件进行处理。
- 在边缘触发模式下,只有当文件描述符的状态从不可读 / 不可写变为可读 / 可写时,epoll才会通知应用程序。这种模式需要应用程序在一次通知中尽可能多地处理事件,以避免丢失事件。
 
二、使用epoll的步骤
 
 
-  创建 epoll实例:- 使用epoll_create函数创建一个epoll实例,该函数返回一个文件描述符,用于后续的epoll操作。
- 函数原型:int epoll_create(int size);
- 参数size是一个提示性参数,表示epoll实例可以处理的最大文件描述符数量。这个参数在现代 Linux 内核中已经被忽略,但仍然需要提供一个大于 0 的值。
 
- 使用
-  注册文件描述符和事件: - 使用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_event结构的指针,用于指定要监视的事件类型和相关的数据。
 
 
- 使用
-  等待事件发生: - 使用epoll_wait函数等待epoll实例上的事件发生。
- 函数原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数说明: 
    - epfd是- epoll实例的文件描述符。
- events是一个指向- epoll_event结构数组的指针,用于存储发生的事件。
- maxevents是- events数组的大小,表示最多可以返回的事件数量。
- timeout是等待事件发生的超时时间,以毫秒为单位。可以设置为- -1表示无限期等待。
 
 
- 使用
-  处理事件: - 当epoll_wait函数返回时,应用程序可以根据events数组中的事件进行相应的 I/O 操作。
- epoll_event结构中的- events成员表示发生的事件类型,可以是- EPOLLIN(可读事件)、- EPOLLOUT(可写事件)等。
 
- 当
三、示例代码
以下是一个使用epoll实现简单服务器的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define PORT 8888
#define MAX_EVENTS 10
void handleClient(int client_fd) {
    char buffer[1024];
    ssize_t bytesRead;
    while ((bytesRead = read(client_fd, buffer, sizeof(buffer))) > 0) {
        // 处理客户端请求
        write(client_fd, buffer, bytesRead);
    }
    close(client_fd);
}
int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    struct epoll_event event, events[MAX_EVENTS];
    int epoll_fd, nfds;
    // 创建服务器套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    // 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    // 绑定服务器套接字
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }
    // 监听连接请求
    if (listen(server_fd, SOMAXCONN) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }
    // 将服务器套接字添加到 epoll 实例中,监视可读事件
    event.events = EPOLLIN;
    event.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
        perror("epoll_ctl");
        exit(EXIT_FAILURE);
    }
    while (1) {
        // 等待事件发生
        nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }
        // 处理发生的事件
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == server_fd) {
                // 有新的连接请求
                client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
                if (client_fd == -1) {
                    perror("accept");
                    continue;
                }
                // 将新的客户端套接字添加到 epoll 实例中,监视可读事件
                event.events = EPOLLIN;
                event.data.fd = client_fd;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
                    perror("epoll_ctl");
                    close(client_fd);
                }
            } else {
                // 处理客户端的请求
                handleClient(events[i].data.fd);
                // 从 epoll 实例中删除客户端套接字
                if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL) == -1) {
                    perror("epoll_ctl");
                }
            }
        }
    }
    close(server_fd);
    close(epoll_fd);
    return 0;
}
在这个例子中,服务器使用epoll来监视服务器套接字和已连接的客户端套接字。当有新的连接请求时,服务器接受连接并将新的客户端套接字添加到epoll实例中。当有客户端发送数据时,服务器读取数据并将其回显给客户端。
四、注意事项
-  错误处理: - 在使用epoll函数时,要注意检查返回值并进行适当的错误处理。
- 如果epoll_create1、epoll_ctl或epoll_wait函数返回错误,应该根据错误码进行相应的处理。
 
- 在使用
-  事件触发模式: - 根据应用程序的需求选择合适的事件触发模式。水平触发模式相对简单,但可能会导致频繁的通知;边缘触发模式需要应用程序更加小心地处理事件,以避免丢失事件。
 
-  资源管理: - 在使用完epoll实例后,应该及时关闭对应的文件描述符,以释放系统资源。
 
- 在使用完
总之,epoll是一种高效的 I/O 多路复用机制,在处理大量文件描述符时具有明显的优势。通过正确地使用epoll,可以提高应用程序的性能和并发性。




















