C 语言异步多路复用
C 语言异步多路复用基础概念
什么是异步多路复用
在传统的编程模型中,一个程序通常是顺序执行的,在执行某个 I/O 操作(如读取文件、接收网络数据等)时,程序会被阻塞,直到该操作完成。而异步多路复用则提供了一种机制,允许程序在等待多个 I/O 操作完成的同时,不被阻塞,可以继续执行其他任务。
具体来说,异步多路复用技术可以监控多个文件描述符(例如套接字、文件等),当其中任何一个文件描述符准备好进行 I/O 操作(读或写)时,系统会通知程序,程序就可以对相应的文件描述符进行操作,而无需一直等待。
异步多路复用的优势
- 提高资源利用率:在等待 I/O 操作完成的过程中,程序不再被阻塞,可以执行其他任务,充分利用 CPU 资源。例如,在一个网络服务器程序中,可能同时有多个客户端连接请求。如果使用传统的阻塞 I/O 方式,服务器在处理一个客户端请求时,其他客户端的请求就只能等待。而异步多路复用技术可以让服务器同时监控多个客户端的连接,当有客户端发送数据时,及时进行处理,提高了服务器对资源的利用率。
- 增强程序的并发处理能力:能够同时处理多个 I/O 操作,使程序可以更好地应对高并发场景。以一个文件服务器为例,多个用户可能同时请求下载文件。通过异步多路复用,服务器可以同时监控多个文件描述符,在不同用户的请求到来时,迅速响应,而不是依次处理每个请求,大大提高了并发处理能力。
常用的异步多路复用模型
- select:这是最早出现的异步多路复用模型,它允许程序监控一组文件描述符,当其中任何一个文件描述符准备好进行 I/O 操作时,select 函数会返回。select 函数的原型如下:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
:监控的文件描述符集合中的最大文件描述符加 1。readfds
:监控读操作的文件描述符集合。writefds
:监控写操作的文件描述符集合。exceptfds
:监控异常情况的文件描述符集合。timeout
:设置等待的超时时间,如果为 NULL,则一直阻塞,直到有文件描述符准备好。
- poll:poll 函数与 select 函数类似,但在实现和使用上有所不同。poll 的原型为:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
:一个结构体数组,每个元素包含要监控的文件描述符、监控的事件类型等信息。nfds
:数组中元素的个数。timeout
:等待的超时时间,单位为毫秒。
- epoll:这是 Linux 特有的异步多路复用机制,在处理大量文件描述符时性能优于 select 和 poll。epoll 有两种工作模式:水平触发(LT)和边缘触发(ET)。epoll 的相关函数主要有
epoll_create
、epoll_ctl
和epoll_wait
。
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_create
:创建一个 epoll 实例,size
参数已被忽略,但必须大于 0。epoll_ctl
:用于控制某个 epoll 实例所监控的文件描述符的事件。op
参数指定操作类型,如添加、修改或删除文件描述符的监控事件。epoll_wait
:等待所监控的文件描述符上有事件发生。events
用于返回发生事件的文件描述符及其事件类型,maxevents
是events
数组的大小,timeout
为等待的超时时间。
使用 select 实现异步多路复用
select 基本原理
select 函数通过遍历所监控的文件描述符集合,检查每个文件描述符是否准备好进行相应的 I/O 操作。当调用 select 函数时,内核会将进程挂起,直到有文件描述符准备好或者超时。一旦 select 返回,程序需要再次遍历文件描述符集合,以确定哪些文件描述符真正准备好进行操作。
select 代码示例
以下是一个简单的使用 select 实现同时监听标准输入和一个套接字的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
// 填充服务器地址结构
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定套接字
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
FD_SET(sockfd, &read_fds);
int max_fd = sockfd > STDIN_FILENO? sockfd : STDIN_FILENO;
char buffer[BUFFER_SIZE];
while (1) {
fd_set tmp_fds = read_fds;
int activity = select(max_fd + 1, &tmp_fds, NULL, NULL, NULL);
if (activity < 0) {
perror("select error");
break;
} else if (activity > 0) {
if (FD_ISSET(STDIN_FILENO, &tmp_fds)) {
// 标准输入有数据
fgets(buffer, BUFFER_SIZE, stdin);
printf("Read from stdin: %s", buffer);
}
if (FD_ISSET(sockfd, &tmp_fds)) {
// 套接字有数据
socklen_t len = sizeof(cliaddr);
int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
}
}
}
close(sockfd);
return 0;
}
在上述代码中,我们创建了一个 UDP 套接字并绑定到指定端口。然后使用 select 同时监控标准输入(STDIN_FILENO
)和套接字。当标准输入有数据可读或者套接字接收到数据时,程序会相应地进行处理。
select 的局限性
- 文件描述符数量限制:在一些系统中,select 所能监控的文件描述符数量有一定限制,通常为 1024 个。虽然可以通过修改系统参数来提高这个限制,但这并不是一个理想的解决方案。
- 性能问题:select 需要遍历所有监控的文件描述符来确定哪些文件描述符准备好,当文件描述符数量较多时,这种遍历操作会带来较大的性能开销。
使用 poll 实现异步多路复用
poll 基本原理
poll 函数通过一个 pollfd
结构体数组来管理要监控的文件描述符及其事件。与 select 不同,poll 没有文件描述符数量的硬限制(理论上只受系统资源限制)。当调用 poll 时,内核会检查每个 pollfd
结构体所对应的文件描述符是否有相应的事件发生,然后返回发生事件的文件描述符数量。程序需要遍历 pollfd
数组来确定具体哪些文件描述符发生了事件。
poll 代码示例
下面是一个使用 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 BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
// 填充服务器地址结构
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定套接字
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
struct pollfd fds[2];
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
fds[1].fd = sockfd;
fds[1].events = POLLIN;
char buffer[BUFFER_SIZE];
while (1) {
int activity = poll(fds, 2, -1);
if (activity < 0) {
perror("poll error");
break;
} else if (activity > 0) {
if (fds[0].revents & POLLIN) {
// 标准输入有数据
fgets(buffer, BUFFER_SIZE, stdin);
printf("Read from stdin: %s", buffer);
}
if (fds[1].revents & POLLIN) {
// 套接字有数据
socklen_t len = sizeof(cliaddr);
int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
}
}
}
close(sockfd);
return 0;
}
在这个示例中,我们使用 pollfd
结构体数组来监控标准输入和套接字的读事件。当有事件发生时,程序会根据 revents
字段判断是哪个文件描述符发生了事件,并进行相应处理。
poll 与 select 的比较
- 文件描述符数量限制:poll 没有像 select 那样的文件描述符数量硬限制,在处理大量文件描述符时更具优势。
- 性能:虽然 poll 也需要遍历
pollfd
数组来确定发生事件的文件描述符,但在一些系统中,其性能比 select 略好,尤其是在文件描述符数量较多的情况下。不过,当文件描述符数量非常大时,poll 的性能也会受到一定影响。
使用 epoll 实现异步多路复用
epoll 基本原理
epoll 采用了一种事件通知机制,它在内核中维护一个事件表,通过 epoll_ctl
函数将需要监控的文件描述符及其事件添加到这个事件表中。当有文件描述符准备好进行 I/O 操作时,内核会将该事件添加到一个就绪队列中。epoll_wait
函数会从这个就绪队列中获取发生事件的文件描述符,而不需要像 select 和 poll 那样遍历所有监控的文件描述符。
epoll 水平触发(LT)模式代码示例
#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 BUFFER_SIZE 1024
#define EPOLL_SIZE 10
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
// 填充服务器地址结构
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定套接字
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
int epfd = epoll_create(EPOLL_SIZE);
if (epfd < 0) {
perror("epoll_create failed");
close(sockfd);
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.data.fd = STDIN_FILENO;
event.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event) < 0) {
perror("epoll_ctl add stdin failed");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
event.data.fd = sockfd;
event.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) < 0) {
perror("epoll_ctl add sockfd failed");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
struct epoll_event events[EPOLL_SIZE];
char buffer[BUFFER_SIZE];
while (1) {
int num = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if (num < 0) {
perror("epoll_wait error");
break;
}
for (int i = 0; i < num; i++) {
if (events[i].data.fd == STDIN_FILENO) {
// 标准输入有数据
fgets(buffer, BUFFER_SIZE, stdin);
printf("Read from stdin: %s", buffer);
} else if (events[i].data.fd == sockfd) {
// 套接字有数据
socklen_t len = sizeof(cliaddr);
int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
}
}
}
close(sockfd);
close(epfd);
return 0;
}
在水平触发模式下,只要文件描述符对应的缓冲区还有数据可读(或可写),epoll_wait
就会一直通知程序。
epoll 边缘触发(ET)模式代码示例
#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 BUFFER_SIZE 1024
#define EPOLL_SIZE 10
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
// 填充服务器地址结构
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定套接字
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
int epfd = epoll_create(EPOLL_SIZE);
if (epfd < 0) {
perror("epoll_create failed");
close(sockfd);
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.data.fd = STDIN_FILENO;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event) < 0) {
perror("epoll_ctl add stdin failed");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
event.data.fd = sockfd;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) < 0) {
perror("epoll_ctl add sockfd failed");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
struct epoll_event events[EPOLL_SIZE];
char buffer[BUFFER_SIZE];
while (1) {
int num = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if (num < 0) {
perror("epoll_wait error");
break;
}
for (int i = 0; i < num; i++) {
if (events[i].data.fd == STDIN_FILENO) {
// 标准输入有数据
while (read(STDIN_FILENO, buffer, BUFFER_SIZE) > 0) {
printf("Read from stdin: %s", buffer);
}
} else if (events[i].data.fd == sockfd) {
// 套接字有数据
socklen_t len = sizeof(cliaddr);
while (recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len) > 0) {
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
}
}
}
}
close(sockfd);
close(epfd);
return 0;
}
在边缘触发模式下,只有当文件描述符的状态从不可读(或不可写)变为可读(或可写)时,epoll_wait
才会通知程序。因此,在边缘触发模式下,程序需要一次性将缓冲区中的数据读取(或写入)完,否则可能会丢失后续的事件通知。
epoll 的优势
- 高性能:epoll 采用事件通知机制,避免了 select 和 poll 中遍历所有文件描述符的操作,在处理大量文件描述符时性能优势明显。
- 灵活的工作模式:epoll 提供了水平触发和边缘触发两种工作模式,用户可以根据具体需求选择合适的模式,以更好地优化程序性能。
异步多路复用在实际项目中的应用
网络服务器
在网络服务器开发中,异步多路复用技术被广泛应用。例如,一个 HTTP 服务器可能同时接收来自多个客户端的连接请求。通过异步多路复用,服务器可以监控所有客户端套接字的状态,当有客户端发送数据时,及时处理请求,提高服务器的并发处理能力和响应速度。以下是一个简单的基于 epoll 的 HTTP 服务器示例框架:
#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 8080
#define BUFFER_SIZE 1024
#define EPOLL_SIZE 100
void handle_http_request(int client_fd) {
char buffer[BUFFER_SIZE];
int n = recv(client_fd, buffer, BUFFER_SIZE, 0);
if (n < 0) {
perror("recv error");
return;
}
buffer[n] = '\0';
// 处理 HTTP 请求逻辑,例如解析请求头、返回响应等
char response[] = "HTTP/1.1 200 OK\r\nContent - Type: text/html\r\n\r\n<html><body>Hello, World!</body></html>";
send(client_fd, response, strlen(response), 0);
}
int main() {
int sockfd;
struct sockaddr_in servaddr;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
// 填充服务器地址结构
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定套接字
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 监听套接字
if (listen(sockfd, 10) < 0) {
perror("listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
int epfd = epoll_create(EPOLL_SIZE);
if (epfd < 0) {
perror("epoll_create failed");
close(sockfd);
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.data.fd = sockfd;
event.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) < 0) {
perror("epoll_ctl add sockfd failed");
close(sockfd);
close(epfd);
exit(EXIT_FAILURE);
}
struct epoll_event events[EPOLL_SIZE];
while (1) {
int num = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if (num < 0) {
perror("epoll_wait error");
break;
}
for (int i = 0; i < num; i++) {
if (events[i].data.fd == sockfd) {
// 有新的客户端连接
int client_fd = accept(sockfd, NULL, NULL);
if (client_fd < 0) {
perror("accept error");
continue;
}
event.data.fd = client_fd;
event.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event) < 0) {
perror("epoll_ctl add client_fd failed");
close(client_fd);
continue;
}
} else {
// 客户端有数据
handle_http_request(events[i].data.fd);
close(events[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
}
}
}
close(sockfd);
close(epfd);
return 0;
}
在这个示例中,服务器使用 epoll 监控监听套接字和客户端套接字。当有新的客户端连接时,将其加入 epoll 监控列表;当客户端有数据发送时,处理 HTTP 请求并返回响应,然后关闭连接并从 epoll 中删除该客户端套接字。
文件 I/O 处理
在一些需要同时处理多个文件 I/O 操作的应用中,异步多路复用也能发挥作用。例如,一个文件索引服务可能需要同时读取多个文件的内容并建立索引。通过异步多路复用,可以监控多个文件描述符,当某个文件准备好进行读取时,及时读取数据,提高处理效率。以下是一个简单的示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#define FILE_COUNT 3
#define BUFFER_SIZE 1024
int main() {
int fds[FILE_COUNT];
fds[0] = open("file1.txt", O_RDONLY);
fds[1] = open("file2.txt", O_RDONLY);
fds[2] = open("file3.txt", O_RDONLY);
if (fds[0] < 0 || fds[1] < 0 || fds[2] < 0) {
perror("open file failed");
for (int i = 0; i < FILE_COUNT; i++) {
if (fds[i] > 0) {
close(fds[i]);
}
}
exit(EXIT_FAILURE);
}
fd_set read_fds;
FD_ZERO(&read_fds);
for (int i = 0; i < FILE_COUNT; i++) {
FD_SET(fds[i], &read_fds);
}
char buffer[BUFFER_SIZE];
while (1) {
fd_set tmp_fds = read_fds;
int activity = select(fds[FILE_COUNT - 1] + 1, &tmp_fds, NULL, NULL, NULL);
if (activity < 0) {
perror("select error");
break;
} else if (activity > 0) {
for (int i = 0; i < FILE_COUNT; i++) {
if (FD_ISSET(fds[i], &tmp_fds)) {
int n = read(fds[i], buffer, BUFFER_SIZE);
if (n < 0) {
perror("read error");
} else if (n == 0) {
// 文件读取完毕
printf("File %d has been read completely.\n", i + 1);
close(fds[i]);
FD_CLR(fds[i], &read_fds);
} else {
buffer[n] = '\0';
printf("Read from file %d: %s", i + 1, buffer);
}
}
}
}
int all_closed = 1;
for (int i = 0; i < FILE_COUNT; i++) {
if (FD_ISSET(fds[i], &read_fds)) {
all_closed = 0;
break;
}
}
if (all_closed) {
break;
}
}
for (int i = 0; i < FILE_COUNT; i++) {
if (fds[i] > 0) {
close(fds[i]);
}
}
return 0;
}
在这个示例中,我们使用 select 监控多个文件描述符,当某个文件有数据可读时,读取数据并进行相应处理,直到所有文件都读取完毕。
异步多路复用的注意事项
文件描述符管理
在使用异步多路复用技术时,需要妥善管理文件描述符。例如,在添加或删除文件描述符到监控集合时,要确保操作的正确性。在使用 epoll 时,当一个文件描述符对应的连接关闭后,需要及时使用 epoll_ctl
将其从 epoll 实例中删除,否则可能会导致资源泄漏或程序异常。
超时处理
合理设置超时时间是很重要的。如果设置的超时时间过长,可能会导致程序在某些情况下长时间等待,影响响应速度;如果设置的超时时间过短,可能会导致一些正常的 I/O 操作还未完成就被认为超时。在网络编程中,对于一些网络不稳定的场景,需要根据实际情况动态调整超时时间。
事件处理逻辑
在处理异步多路复用返回的事件时,要确保事件处理逻辑的正确性和完整性。例如,在边缘触发模式下,要一次性将缓冲区中的数据读取或写入完,避免丢失事件通知。同时,对于不同类型的事件(如读事件、写事件、异常事件等),要分别进行合理的处理。
跨平台兼容性
不同操作系统对异步多路复用的支持和实现可能存在差异。例如,epoll 是 Linux 特有的机制,在其他操作系统(如 Windows)上无法使用。如果需要开发跨平台的应用程序,需要考虑使用兼容性更好的 select 或 poll,或者使用一些跨平台的网络编程库(如 libuv)来实现异步多路复用功能。
综上所述,C 语言中的异步多路复用技术为程序提供了强大的并发处理能力,通过合理选择和使用不同的异步多路复用模型,并注意相关的注意事项,可以开发出高效、稳定的应用程序,尤其是在网络编程和 I/O 密集型应用中。无论是开发网络服务器、文件处理工具还是其他需要处理多个并发 I/O 操作的程序,异步多路复用技术都是非常重要的工具。