当前位置: 首页 > news >正文

一文读懂select、poll、epoll的用法

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

一、select实现

1.1.基本概念

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。

IO多路复用适用如下场合:

  1. 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  2. 当一个客户同时处理多个接口时,而这种情况是可能的,但很少出现。
  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

1.2.select函数

该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:

#include <sys/select.h>
#include <sys/time.h>int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就绪描述符的数目,超时返回0,出错返回-1

函数参数介绍如下:

(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。

因为文件描述符是从0开始的。

(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

  • void FD_ZERO(fd_set *fdset); //清空集合
  • void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
  • void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
  • int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写

(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。

(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

1.3.测试程序

写一个TCP回射程序,程序的功能是:客户端向服务器发送信息,服务器接收并原样发送给客户端,客户端显示出接收到的信息。

服务端程序如下:

1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 #include <errno.h>5 #include <netinet/in.h>6 #include <sys/socket.h>7 #include <sys/select.h>8 #include <sys/types.h>9 #include <netinet/in.h>10 #include <arpa/inet.h>11 #include <unistd.h>12 #include <assert.h>13 14 #define IPADDR      "127.0.0.1"15 #define PORT        878716 #define MAXLINE     102417 #define LISTENQ     518 #define SIZE        1019 20 typedef struct server_context_st21 {22     int cli_cnt;        /*客户端个数*/23     int clifds[SIZE];   /*客户端的个数*/24     fd_set allfds;      /*句柄集合*/25     int maxfd;          /*句柄最大值*/26 } server_context_st;27 static server_context_st *s_srv_ctx = NULL;28 /*===========================================================================29  * ==========================================================================*/30 static int create_server_proc(const char* ip,int port)31 {32     int  fd;33     struct sockaddr_in servaddr;34     fd = socket(AF_INET, SOCK_STREAM,0);35     if (fd == -1) {36         fprintf(stderr, "create socket fail,erron:%d,reason:%s\n",37                 errno, strerror(errno));38         return -1;39     }40 41     /*一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。*/42     int reuse = 1;43     if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {44         return -1;45     }46 47     bzero(&servaddr,sizeof(servaddr));48     servaddr.sin_family = AF_INET;49     inet_pton(AF_INET,ip,&servaddr.sin_addr);50     servaddr.sin_port = htons(port);51 52     if (bind(fd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1) {53         perror("bind error: ");54         return -1;55     }56 57     listen(fd,LISTENQ);58 59     return fd;60 }61 62 static int accept_client_proc(int srvfd)63 {64     struct sockaddr_in cliaddr;65     socklen_t cliaddrlen;66     cliaddrlen = sizeof(cliaddr);67     int clifd = -1;68 69     printf("accpet clint proc is called.\n");70 71 ACCEPT:72     clifd = accept(srvfd,(struct sockaddr*)&cliaddr,&cliaddrlen);73 74     if (clifd == -1) {75         if (errno == EINTR) {76             goto ACCEPT;77         } else {78             fprintf(stderr, "accept fail,error:%s\n", strerror(errno));79             return -1;80         }81     }82 83     fprintf(stdout, "accept a new client: %s:%d\n",84             inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);85 86     //将新的连接描述符添加到数组中87     int i = 0;88     for (i = 0; i < SIZE; i++) {89         if (s_srv_ctx->clifds[i] < 0) {90             s_srv_ctx->clifds[i] = clifd;91             s_srv_ctx->cli_cnt++;92             break;93         }94     }95 96     if (i == SIZE) {97         fprintf(stderr,"too many clients.\n");98         return -1;99     }
100
101 }
102 
103 static int handle_client_msg(int fd, char *buf) 
104 {
105     assert(buf);
106     printf("recv buf is :%s\n", buf);
107     write(fd, buf, strlen(buf) +1);
108     return 0;
109 }
110 
111 static void recv_client_msg(fd_set *readfds)
112 {
113     int i = 0, n = 0;
114     int clifd;
115     char buf[MAXLINE] = {0};
116     for (i = 0;i <= s_srv_ctx->cli_cnt;i++) {
117         clifd = s_srv_ctx->clifds[i];
118         if (clifd < 0) {
119             continue;
120         }
121         /*判断客户端套接字是否有数据*/
122         if (FD_ISSET(clifd, readfds)) {
123             //接收客户端发送的信息
124             n = read(clifd, buf, MAXLINE);
125             if (n <= 0) {
126                 /*n==0表示读取完成,客户都关闭套接字*/
127                 FD_CLR(clifd, &s_srv_ctx->allfds);
128                 close(clifd);
129                 s_srv_ctx->clifds[i] = -1;
130                 continue;
131             }
132             handle_client_msg(clifd, buf);
133         }
134     }
135 }
136 static void handle_client_proc(int srvfd)
137 {
138     int  clifd = -1;
139     int  retval = 0;
140     fd_set *readfds = &s_srv_ctx->allfds;
141     struct timeval tv;
142     int i = 0;
143 
144     while (1) {
145         /*每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核修改啦*/
146         FD_ZERO(readfds);
147         /*添加监听套接字*/
148         FD_SET(srvfd, readfds);
149         s_srv_ctx->maxfd = srvfd;
150 
151         tv.tv_sec = 30;
152         tv.tv_usec = 0;
153         /*添加客户端套接字*/
154         for (i = 0; i < s_srv_ctx->cli_cnt; i++) {
155             clifd = s_srv_ctx->clifds[i];
156             /*去除无效的客户端句柄*/
157             if (clifd != -1) {
158                 FD_SET(clifd, readfds);
159             }
160             s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
161         }
162 
163         /*开始轮询接收处理服务端和客户端套接字*/
164         retval = select(s_srv_ctx->maxfd + 1, readfds, NULL, NULL, &tv);
165         if (retval == -1) {
166             fprintf(stderr, "select error:%s.\n", strerror(errno));
167             return;
168         }
169         if (retval == 0) {
170             fprintf(stdout, "select is timeout.\n");
171             continue;
172         }
173         if (FD_ISSET(srvfd, readfds)) {
174             /*监听客户端请求*/
175             accept_client_proc(srvfd);
176         } else {
177             /*接受处理客户端消息*/
178             recv_client_msg(readfds);
179         }
180     }
181 }
182 
183 static void server_uninit()
184 {
185     if (s_srv_ctx) {
186         free(s_srv_ctx);
187         s_srv_ctx = NULL;
188     }
189 }
190 
191 static int server_init()
192 {
193     s_srv_ctx = (server_context_st *)malloc(sizeof(server_context_st));
194     if (s_srv_ctx == NULL) {
195         return -1;
196     }
197 
198     memset(s_srv_ctx, 0, sizeof(server_context_st));
199 
200     int i = 0;
201     for (;i < SIZE; i++) {
202         s_srv_ctx->clifds[i] = -1;
203     }
204 
205     return 0;
206 }
207 
208 int main(int argc,char *argv[])
209 {
210     int  srvfd;
211     /*初始化服务端context*/
212     if (server_init() < 0) {
213         return -1;
214     }
215     /*创建服务,开始监听客户端请求*/
216     srvfd = create_server_proc(IPADDR, PORT);
217     if (srvfd < 0) {
218         fprintf(stderr, "socket create or bind fail.\n");
219         goto err;
220     }
221     /*开始接收并处理客户端请求*/
222     handle_client_proc(srvfd);
223     server_uninit();
224     return 0;
225 err:
226     server_uninit();
227     return -1;
228 }

客户端程序如下:

1 #include <netinet/in.h>2 #include <sys/socket.h>3 #include <stdio.h>4 #include <string.h>5 #include <stdlib.h>6 #include <sys/select.h>7 #include <time.h>8 #include <unistd.h>9 #include <sys/types.h>
10 #include <errno.h>
11 
12 #define MAXLINE 1024
13 #define IPADDRESS "127.0.0.1"
14 #define SERV_PORT 8787
15 
16 #define max(a,b) (a > b) ? a : b
17 
18 static void handle_recv_msg(int sockfd, char *buf) 
19 {
20 printf("client recv msg is:%s\n", buf);
21 sleep(5);
22 write(sockfd, buf, strlen(buf) +1);
23 }
24 
25 static void handle_connection(int sockfd)
26 {
27 char sendline[MAXLINE],recvline[MAXLINE];
28 int maxfdp,stdineof;
29 fd_set readfds;
30 int n;
31 struct timeval tv;
32 int retval = 0;
33 
34 while (1) {
35 
36 FD_ZERO(&readfds);
37 FD_SET(sockfd,&readfds);
38 maxfdp = sockfd;
39 
40 tv.tv_sec = 5;
41 tv.tv_usec = 0;
42 
43 retval = select(maxfdp+1,&readfds,NULL,NULL,&tv);
44 
45 if (retval == -1) {
46 return ;
47 }
48 
49 if (retval == 0) {
50 printf("client timeout.\n");
51 continue;
52 }
53 
54 if (FD_ISSET(sockfd, &readfds)) {
55 n = read(sockfd,recvline,MAXLINE);
56 if (n <= 0) {
57 fprintf(stderr,"client: server is closed.\n");
58 close(sockfd);
59 FD_CLR(sockfd,&readfds);
60 return;
61 }
62 
63 handle_recv_msg(sockfd, recvline);
64 }
65 }
66 }
67 
68 int main(int argc,char *argv[])
69 {
70 int sockfd;
71 struct sockaddr_in servaddr;
72 
73 sockfd = socket(AF_INET,SOCK_STREAM,0);
74 
75 bzero(&servaddr,sizeof(servaddr));
76 servaddr.sin_family = AF_INET;
77 servaddr.sin_port = htons(SERV_PORT);
78 inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
79 
80 int retval = 0;
81 retval = connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
82 if (retval < 0) {
83 fprintf(stderr, "connect fail,error:%s\n", strerror(errno));
84 return -1;
85 }
86 
87 printf("client send to server .\n");
88 write(sockfd, "hello server", 32);
89 
90 handle_connection(sockfd);
91 
92 return 0;
93 }

4、程序结果

启动服务程序,执行三个客户程序进行测试,结果如下图所示:

select的调用过程如下所示:

  • (1)使用copy_from_user从用户空间拷贝fd_set到内核空间
  • (2)注册回调函数__pollwait
  • (3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
  • (4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
  • (5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
  • (6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
  • (7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
  • (8)把fd_set从内核空间拷贝到用户空间。

总结:

select的几大缺点:

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024

学习直通车:

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂​ke.qq.com/course/4032547?flowToken=1040236​ke.qq.com/course/4032547?flowToken=1040236​ke.qq.com/course/4032547?flowToken=1040236

内核资料直通车:

Linux内核源码技术学习路线+视频教程内核源码​docs.qq.com/doc/DTXFKZlZ4YmFYQWZS正在上传…重新上传取消

二,poll实现

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

1、基本知识

  poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

2.2。poll函数

函数格式如下所示:

# include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

pollfd结构体定义如下:

struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;

每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:

  • POLLIN         有数据可读。
  • POLLRDNORM      有普通数据可读。
  • POLLRDBAND      有优先数据可读。
  • POLLPRI         有紧迫数据可读。
  • POLLOUT       写数据不会导致阻塞。
  • POLLWRNORM      写普通数据不会导致阻塞。
  • POLLWRBAND      写优先数据不会导致阻塞。
  • POLLMSGSIGPOLL     消息可用。
  • 此外,revents域中还可能返回下列事件:
    POLLER   指定的文件描述符发生错误。
  • POLLHUP   指定的文件描述符挂起事件。
  • POLLNVAL  指定的文件描述符非法。

这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。

使用poll()和select()不一样,你不需要显式地请求异常情况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。

timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:

  • EBADF   一个或多个结构体中指定的文件描述符无效。
  • EFAULTfds   指针指向的地址超出进程的地址空间。
  • EINTR     请求的事件之前产生一个信号,调用可以重新发起。
  • EINVALnfds  参数超出PLIMIT_NOFILE值。
  • ENOMEM   可用内存不足,无法完成请求。

2.3测出程序

编写一个echo server程序,功能是客户端向服务器发送信息,服务器接收输出并原样发送回给客户端,客户端接收到输出到终端。

服务器端程序如下:

1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 #include <errno.h>5 6 #include <netinet/in.h>7 #include <sys/socket.h>8 #include <poll.h>9 #include <unistd.h>10 #include <sys/types.h>11 12 #define IPADDRESS   "127.0.0.1"13 #define PORT        878714 #define MAXLINE     102415 #define LISTENQ     516 #define OPEN_MAX    100017 #define INFTIM      -118 19 //函数声明20 //创建套接字并进行绑定21 static int socket_bind(const char* ip,int port);22 //IO多路复用poll23 static void do_poll(int listenfd);24 //处理多个连接25 static void handle_connection(struct pollfd *connfds,int num);26 27 int main(int argc,char *argv[])28 {29     int  listenfd,connfd,sockfd;30     struct sockaddr_in cliaddr;31     socklen_t cliaddrlen;32     listenfd = socket_bind(IPADDRESS,PORT);33     listen(listenfd,LISTENQ);34     do_poll(listenfd);35     return 0;36 }37 38 static int socket_bind(const char* ip,int port)39 {40     int  listenfd;41     struct sockaddr_in servaddr;42     listenfd = socket(AF_INET,SOCK_STREAM,0);43     if (listenfd == -1)44     {45         perror("socket error:");46         exit(1);47     }48     bzero(&servaddr,sizeof(servaddr));49     servaddr.sin_family = AF_INET;50     inet_pton(AF_INET,ip,&servaddr.sin_addr);51     servaddr.sin_port = htons(port);52     if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)53     {54         perror("bind error: ");55         exit(1);56     }57     return listenfd;58 }59 60 static void do_poll(int listenfd)61 {62     int  connfd,sockfd;63     struct sockaddr_in cliaddr;64     socklen_t cliaddrlen;65     struct pollfd clientfds[OPEN_MAX];66     int maxi;67     int i;68     int nready;69     //添加监听描述符70     clientfds[0].fd = listenfd;71     clientfds[0].events = POLLIN;72     //初始化客户连接描述符73     for (i = 1;i < OPEN_MAX;i++)74         clientfds[i].fd = -1;75     maxi = 0;76     //循环处理77     for ( ; ; )78     {79         //获取可用描述符的个数80         nready = poll(clientfds,maxi+1,INFTIM);81         if (nready == -1)82         {83             perror("poll error:");84             exit(1);85         }86         //测试监听描述符是否准备好87         if (clientfds[0].revents & POLLIN)88         {89             cliaddrlen = sizeof(cliaddr);90             //接受新的连接91             if ((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen)) == -1)92             {93                 if (errno == EINTR)94                     continue;95                 else96                 {97                    perror("accept error:");98                    exit(1);99                 }
100             }
101             fprintf(stdout,"accept a new client: %s:%d\n", inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
102             //将新的连接描述符添加到数组中
103             for (i = 1;i < OPEN_MAX;i++)
104             {
105                 if (clientfds[i].fd < 0)
106                 {
107                     clientfds[i].fd = connfd;
108                     break;
109                 }
110             }
111             if (i == OPEN_MAX)
112             {
113                 fprintf(stderr,"too many clients.\n");
114                 exit(1);
115             }
116             //将新的描述符添加到读描述符集合中
117             clientfds[i].events = POLLIN;
118             //记录客户连接套接字的个数
119             maxi = (i > maxi ? i : maxi);
120             if (--nready <= 0)
121                 continue;
122         }
123         //处理客户连接
124         handle_connection(clientfds,maxi);
125     }
126 }
127 
128 static void handle_connection(struct pollfd *connfds,int num)
129 {
130     int i,n;
131     char buf[MAXLINE];
132     memset(buf,0,MAXLINE);
133     for (i = 1;i <= num;i++)
134     {
135         if (connfds[i].fd < 0)
136             continue;
137         //测试客户描述符是否准备好
138         if (connfds[i].revents & POLLIN)
139         {
140             //接收客户端发送的信息
141             n = read(connfds[i].fd,buf,MAXLINE);
142             if (n == 0)
143             {
144                 close(connfds[i].fd);
145                 connfds[i].fd = -1;
146                 continue;
147             }
148            // printf("read msg is: ");
149             write(STDOUT_FILENO,buf,n);
150             //向客户端发送buf
151             write(connfds[i].fd,buf,n);
152         }
153     }
154 }

客户端代码如下所示:

1 #include <netinet/in.h>2 #include <sys/socket.h>3 #include <stdio.h>4 #include <string.h>5 #include <stdlib.h>6 #include <poll.h>7 #include <time.h>8 #include <unistd.h>9 #include <sys/types.h>
10 
11 #define MAXLINE     1024
12 #define IPADDRESS   "127.0.0.1"
13 #define SERV_PORT   8787
14 
15 #define max(a,b) (a > b) ? a : b
16 
17 static void handle_connection(int sockfd);
18 
19 int main(int argc,char *argv[])
20 {
21     int                 sockfd;
22     struct sockaddr_in  servaddr;
23     sockfd = socket(AF_INET,SOCK_STREAM,0);
24     bzero(&servaddr,sizeof(servaddr));
25     servaddr.sin_family = AF_INET;
26     servaddr.sin_port = htons(SERV_PORT);
27     inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
28     connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
29     //处理连接描述符
30     handle_connection(sockfd);
31     return 0;
32 }
33 
34 static void handle_connection(int sockfd)
35 {
36     char    sendline[MAXLINE],recvline[MAXLINE];
37     int     maxfdp,stdineof;
38     struct pollfd pfds[2];
39     int n;
40     //添加连接描述符
41     pfds[0].fd = sockfd;
42     pfds[0].events = POLLIN;
43     //添加标准输入描述符
44     pfds[1].fd = STDIN_FILENO;
45     pfds[1].events = POLLIN;
46     for (; ;)
47     {
48         poll(pfds,2,-1);
49         if (pfds[0].revents & POLLIN)
50         {
51             n = read(sockfd,recvline,MAXLINE);
52             if (n == 0)
53             {
54                     fprintf(stderr,"client: server is closed.\n");
55                     close(sockfd);
56             }
57             write(STDOUT_FILENO,recvline,n);
58         }
59         //测试标准输入是否准备好
60         if (pfds[1].revents & POLLIN)
61         {
62             n = read(STDIN_FILENO,sendline,MAXLINE);
63             if (n  == 0)
64             {
65                 shutdown(sockfd,SHUT_WR);
66         continue;
67             }
68             write(sockfd,sendline,n);
69         }
70     }
71 }

三、epoll

3.1基本知识

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

3.2epoll接口

epoll操作过程需要三个接口,分别如下:

#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);

(1) int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

struct epoll_event {__uint32_t events;  /* Epoll events */epoll_data_t data;  /* User data variable */
};

events可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

3.3.工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

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

3.4.测试程序

编写一个服务器回射程序echo,练习epoll过程。

服务器代码如下所示:

1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 #include <errno.h>5 6 #include <netinet/in.h>7 #include <sys/socket.h>8 #include <arpa/inet.h>9 #include <sys/epoll.h>10 #include <unistd.h>11 #include <sys/types.h>12 13 #define IPADDRESS   "127.0.0.1"14 #define PORT        878715 #define MAXSIZE     102416 #define LISTENQ     517 #define FDSIZE      100018 #define EPOLLEVENTS 10019 20 //函数声明21 //创建套接字并进行绑定22 static int socket_bind(const char* ip,int port);23 //IO多路复用epoll24 static void do_epoll(int listenfd);25 //事件处理函数26 static void27 handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf);28 //处理接收到的连接29 static void handle_accpet(int epollfd,int listenfd);30 //读处理31 static void do_read(int epollfd,int fd,char *buf);32 //写处理33 static void do_write(int epollfd,int fd,char *buf);34 //添加事件35 static void add_event(int epollfd,int fd,int state);36 //修改事件37 static void modify_event(int epollfd,int fd,int state);38 //删除事件39 static void delete_event(int epollfd,int fd,int state);40 41 int main(int argc,char *argv[])42 {43     int  listenfd;44     listenfd = socket_bind(IPADDRESS,PORT);45     listen(listenfd,LISTENQ);46     do_epoll(listenfd);47     return 0;48 }49 50 static int socket_bind(const char* ip,int port)51 {52     int  listenfd;53     struct sockaddr_in servaddr;54     listenfd = socket(AF_INET,SOCK_STREAM,0);55     if (listenfd == -1)56     {57         perror("socket error:");58         exit(1);59     }60     bzero(&servaddr,sizeof(servaddr));61     servaddr.sin_family = AF_INET;62     inet_pton(AF_INET,ip,&servaddr.sin_addr);63     servaddr.sin_port = htons(port);64     if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)65     {66         perror("bind error: ");67         exit(1);68     }69     return listenfd;70 }71 72 static void do_epoll(int listenfd)73 {74     int epollfd;75     struct epoll_event events[EPOLLEVENTS];76     int ret;77     char buf[MAXSIZE];78     memset(buf,0,MAXSIZE);79     //创建一个描述符80     epollfd = epoll_create(FDSIZE);81     //添加监听描述符事件82     add_event(epollfd,listenfd,EPOLLIN);83     for ( ; ; )84     {85         //获取已经准备好的描述符事件86         ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);87         handle_events(epollfd,events,ret,listenfd,buf);88     }89     close(epollfd);90 }91 92 static void93 handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)94 {95     int i;96     int fd;97     //进行选好遍历98     for (i = 0;i < num;i++)99     {
100         fd = events[i].data.fd;
101         //根据描述符的类型和事件类型进行处理
102         if ((fd == listenfd) &&(events[i].events & EPOLLIN))
103             handle_accpet(epollfd,listenfd);
104         else if (events[i].events & EPOLLIN)
105             do_read(epollfd,fd,buf);
106         else if (events[i].events & EPOLLOUT)
107             do_write(epollfd,fd,buf);
108     }
109 }
110 static void handle_accpet(int epollfd,int listenfd)
111 {
112     int clifd;
113     struct sockaddr_in cliaddr;
114     socklen_t  cliaddrlen;
115     clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
116     if (clifd == -1)
117         perror("accpet error:");
118     else
119     {
120         printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
121         //添加一个客户描述符和事件
122         add_event(epollfd,clifd,EPOLLIN);
123     }
124 }
125 
126 static void do_read(int epollfd,int fd,char *buf)
127 {
128     int nread;
129     nread = read(fd,buf,MAXSIZE);
130     if (nread == -1)
131     {
132         perror("read error:");
133         close(fd);
134         delete_event(epollfd,fd,EPOLLIN);
135     }
136     else if (nread == 0)
137     {
138         fprintf(stderr,"client close.\n");
139         close(fd);
140         delete_event(epollfd,fd,EPOLLIN);
141     }
142     else
143     {
144         printf("read message is : %s",buf);
145         //修改描述符对应的事件,由读改为写
146         modify_event(epollfd,fd,EPOLLOUT);
147     }
148 }
149 
150 static void do_write(int epollfd,int fd,char *buf)
151 {
152     int nwrite;
153     nwrite = write(fd,buf,strlen(buf));
154     if (nwrite == -1)
155     {
156         perror("write error:");
157         close(fd);
158         delete_event(epollfd,fd,EPOLLOUT);
159     }
160     else
161         modify_event(epollfd,fd,EPOLLIN);
162     memset(buf,0,MAXSIZE);
163 }
164 
165 static void add_event(int epollfd,int fd,int state)
166 {
167     struct epoll_event ev;
168     ev.events = state;
169     ev.data.fd = fd;
170     epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
171 }
172 
173 static void delete_event(int epollfd,int fd,int state)
174 {
175     struct epoll_event ev;
176     ev.events = state;
177     ev.data.fd = fd;
178     epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
179 }
180 
181 static void modify_event(int epollfd,int fd,int state)
182 {
183     struct epoll_event ev;
184     ev.events = state;
185     ev.data.fd = fd;
186     epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
187 }

客户端也用epoll实现,控制STDIN_FILENO、STDOUT_FILENO、和sockfd三个描述符,程序如下所示:

1 #include <netinet/in.h>2 #include <sys/socket.h>3 #include <stdio.h>4 #include <string.h>5 #include <stdlib.h>6 #include <sys/epoll.h>7 #include <time.h>8 #include <unistd.h>9 #include <sys/types.h>10 #include <arpa/inet.h>11 12 #define MAXSIZE     102413 #define IPADDRESS   "127.0.0.1"14 #define SERV_PORT   878715 #define FDSIZE        102416 #define EPOLLEVENTS 2017 18 static void handle_connection(int sockfd);19 static void20 handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf);21 static void do_read(int epollfd,int fd,int sockfd,char *buf);22 static void do_read(int epollfd,int fd,int sockfd,char *buf);23 static void do_write(int epollfd,int fd,int sockfd,char *buf);24 static void add_event(int epollfd,int fd,int state);25 static void delete_event(int epollfd,int fd,int state);26 static void modify_event(int epollfd,int fd,int state);27 28 int main(int argc,char *argv[])29 {30     int                 sockfd;31     struct sockaddr_in  servaddr;32     sockfd = socket(AF_INET,SOCK_STREAM,0);33     bzero(&servaddr,sizeof(servaddr));34     servaddr.sin_family = AF_INET;35     servaddr.sin_port = htons(SERV_PORT);36     inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);37     connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));38     //处理连接39     handle_connection(sockfd);40     close(sockfd);41     return 0;42 }43 44 45 static void handle_connection(int sockfd)46 {47     int epollfd;48     struct epoll_event events[EPOLLEVENTS];49     char buf[MAXSIZE];50     int ret;51     epollfd = epoll_create(FDSIZE);52     add_event(epollfd,STDIN_FILENO,EPOLLIN);53     for ( ; ; )54     {55         ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);56         handle_events(epollfd,events,ret,sockfd,buf);57     }58     close(epollfd);59 }60 61 static void62 handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf)63 {64     int fd;65     int i;66     for (i = 0;i < num;i++)67     {68         fd = events[i].data.fd;69         if (events[i].events & EPOLLIN)70             do_read(epollfd,fd,sockfd,buf);71         else if (events[i].events & EPOLLOUT)72             do_write(epollfd,fd,sockfd,buf);73     }74 }75 76 static void do_read(int epollfd,int fd,int sockfd,char *buf)77 {78     int nread;79     nread = read(fd,buf,MAXSIZE);80         if (nread == -1)81     {82         perror("read error:");83         close(fd);84     }85     else if (nread == 0)86     {87         fprintf(stderr,"server close.\n");88         close(fd);89     }90     else91     {92         if (fd == STDIN_FILENO)93             add_event(epollfd,sockfd,EPOLLOUT);94         else95         {96             delete_event(epollfd,sockfd,EPOLLIN);97             add_event(epollfd,STDOUT_FILENO,EPOLLOUT);98         }99     }
100 }
101 
102 static void do_write(int epollfd,int fd,int sockfd,char *buf)
103 {
104     int nwrite;
105     nwrite = write(fd,buf,strlen(buf));
106     if (nwrite == -1)
107     {
108         perror("write error:");
109         close(fd);
110     }
111     else
112     {
113         if (fd == STDOUT_FILENO)
114             delete_event(epollfd,fd,EPOLLOUT);
115         else
116             modify_event(epollfd,fd,EPOLLIN);
117     }
118     memset(buf,0,MAXSIZE);
119 }
120 
121 static void add_event(int epollfd,int fd,int state)
122 {
123     struct epoll_event ev;
124     ev.events = state;
125     ev.data.fd = fd;
126     epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
127 }
128 
129 static void delete_event(int epollfd,int fd,int state)
130 {
131     struct epoll_event ev;
132     ev.events = state;
133     ev.data.fd = fd;
134     epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
135 }
136 
137 static void modify_event(int epollfd,int fd,int state)
138 {
139     struct epoll_event ev;
140     ev.events = state;
141     ev.data.fd = fd;
142     epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
143 }

epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

  1. 对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
  2. 对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。
  3. 对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。

总结:

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

相关文章:

一文读懂select、poll、epoll的用法

select&#xff0c;poll&#xff0c;epoll都是IO多路复用的机制。I/O多路复用就通过一种机制&#xff0c;可以监视多个描述符&#xff0c;一旦某个描述符就绪&#xff08;一般是读就绪或者写就绪&#xff09;&#xff0c;能够通知程序进行相应的读写操作。但select&#xff0c;…...

《C陷阱与缺陷》----词法“陷阱”

导言&#xff1a; 由于一个程序错误可以从不同层面采用不同方式进行考察&#xff0c;而根据程序错误与考察程序的方式之间的相关性&#xff0c;可以将程序错误进行划分为各种陷阱与缺陷&#xff1a; ①.词法“陷阱” ②.语法“陷阱” ③.语义“陷阱” ④.连接问题 ⑤.库函数问…...

千锋教育+计算机四级网络-计算机网络学习-04

UDP概述 UDP协议 面向无连接的用户数据报协议&#xff0c;在传输数据前不需要先建立连接&#xff1b;目地主机的运输层收到UDP报文后&#xff0c;不需要给出任何确认 UDP特点 相比TCP速度稍快些简单的请求/应答应用程序可以使用UDP对于海量数据传输不应该使用UDP广播和多播应用…...

蓝桥杯算法训练合集十四 1.P08052.P07053.同余方程4.P08015.ascii应用

目录 1.P0805 2.P0705 3.同余方程 4.P0801 5.ascii应用 1.P0805 问题描述 当两个比较大的整数相乘时&#xff0c;可能会出现数据溢出的情形。为避免溢出&#xff0c;可以采用字符串的方法来实现两个大数之间的乘法。具体来说&#xff0c;首先以字符串的形式输入两个整数&…...

判断字符串中的字符的类型isdecimal();isalpha();isdigit();isalnum()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 判断字符串中的字符的类型 isdecimal()&#xff1b;isalpha()&#xff1b;isdigit()&#xff1b;isalnum() [太阳]选择题 对于代码中isdecimal()和isalnum()输出的结果是? s "ABc123&…...

VSCode远程调试Linux代码,python解释器配置

安装插件并配置 安装后找到插件图标&#xff0c;点击 点击SSH上的 号 在弹出框中输入命令&#xff1a;ssh usernameip -p port username: 远程服务器的用户名 ip&#xff1a; 远程ip port&#xff1a;端口号&#xff0c;没有可以不用 输入完毕后点击enter 选择ssh配置文件保存…...

03:入门篇 - CTK Plugin Framework 基本原理

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 CTK Plugin Framework 技术是面向 C++ 的动态模型系统。该系统允许插件之间的松散耦合,并且提供了设计良好的方式来进行功能和数据的交互。此外,它没有预先对插件施加限制,这样就可以很容易地将插件的相关…...

面试攻略,Java 基础面试 100 问(九)

数组有没有 length()方法?String 有没有 length()方法&#xff1f; 数组没有 length()方法&#xff0c;有 length 的属性。String 有 length()方法。JavaScript 中&#xff0c;获得字符串的长度是通过 length 属性得到的&#xff0c;这一点容易和 Java混淆。 在 Java 中&…...

JavaScript 代码不嵌套主义

文章目录前言一、何为嵌套代码二、避免嵌套1.提炼抽取2.反转排列总结前言 看过不少过度嵌套的代码, 我真正意识到问题的严重性是刚入职那会, 我在一个老项目里看到了40个连续的else if, 套了6层的if, for和forEach, 因为我们并没有做什么限制代码嵌套的提前约定. 呃, 那之后认…...

使用默认参数的4大要点

概述 默认参数是C中新增的特性。在C中&#xff0c;可以为函数的参数指定默认值。调用函数时&#xff0c;如果没有指定实参&#xff0c;则自动使用默认参数。默认参数的基本语法这里就不作介绍了&#xff0c;下面重点介绍使用默认参数的一些知识要点。 基本规则 1、当函数中某个…...

Linux文件系统中的硬链接及常见面试题

如果能对inode的概念有所了解&#xff0c;对理解本文会有所帮助。如果对inode的概念不太清楚也没有关系&#xff0c;我们会捎带介绍一下。在文件系统的实现层面&#xff0c;我们可以认为包含两个组件&#xff1a;一个是包含数据块的池子&#xff0c;池子中的数据块是等大小的&a…...

opencv-StereoBM算法

原理解释目前立体匹配算法是计算机视觉中的一个难点和热点&#xff0c;算法很多&#xff0c;但是一般的步骤是&#xff1a;A、匹配代价计算匹配代价计算是整个立体匹配算法的基础&#xff0c;实际是对不同视差下进行灰度相似性测量。常见的方法有灰度差的平方SD&#xff08;squ…...

图像分类竞赛进阶技能:OpenAI-CLIP使用范例

OpenAI-CLIP 官方介绍 尽管深度学习已经彻底改变了计算机视觉&#xff0c;但目前的方法存在几个主要问题:典型的视觉数据集是劳动密集型的&#xff0c;创建成本高&#xff0c;同时只教授一组狭窄的视觉概念;标准视觉模型擅长于一项任务且仅擅长于一项任务&#xff0c;并且需要大…...

Metasploit框架基础(一)

文章目录前言一、基础认知二、批量POC/EXP的构想三、poc检测框架的简单实现四、xray五、Meatsploit框架参考前言 Metasploit 一款渗透测试框架漏洞利用的集合与构建和定制满足你的需求的基础漏洞利用和验证的工具 这几个说法都是百度或者官方文档中出现的手法&#xff0c;说…...

pytorch零基础实现语义分割项目(二)——标签转换与数据加载

数据转换与加载项目列表前言标签转换RGB标签到类别标签映射RGB标签转换成类别标签数据数据加载随机裁剪数据加载项目列表 语义分割项目&#xff08;一&#xff09;——数据概况及预处理 语义分割项目&#xff08;二&#xff09;——标签转换与数据加载 语义分割项目&#x…...

python(8.5)--列表习题

目录 一、求输出结果题 二、计算列表元素个数 三、查找是否存在某元素 四、删除某元素 五、如何在列表中插入元素 六、如何从列表中删除重复的元素 七、 如何将列表中的元素按照从小到大的顺序排序 八、从列表中删除重复的元素 九、大到小的顺序排序 一、求输出结…...

rt-thread pwm 多通道

一通道pwm参考 https://blog.csdn.net/yangshengwei230612/article/details/128738351?spm1001.2014.3001.5501 以下主要是多通道与一通道的区别 芯片 stm32f407rgt6 1、配置PWM设备驱动相关宏定义 添加PWM宏定义 #define BSP_USING_PWM8 #define BSP_USING_PWM8_CH1 #d…...

C语言练习 | 初学者经典练习汇总

目录 1、下面代码输出多少&#xff0c;为什么&#xff1f; 2、你要好好学习么&#xff1f; 3、一直写代码&#xff0c; 4、两个数求最大值 5、输入1-5输出工作日&#xff0c;输入6-7输出休息日&#xff0c;其他输入错误 6、写一个输入密码的代码 7、怎么样当输入数字时候…...

华为OD机试 - 自动曝光(Python) | 机试题算法思路 【2023】

最近更新的博客 华为OD机试 - 卡片组成的最大数字(Python) | 机试题算法思路 华为OD机试 - 网上商城优惠活动(一)(Python) | 机试题算法思路 华为OD机试 - 统计匹配的二元组个数(Python) | 机试题算法思路 华为OD机试 - 找到它(Python) | 机试题算法思路 华为OD机试…...

「6」线性代数(期末复习)

&#x1f680;&#x1f680;&#x1f680;大家觉不错的话&#xff0c;就恳求大家点点关注&#xff0c;点点小爱心&#xff0c;指点指点&#x1f680;&#x1f680;&#x1f680; 目录 第五章 相似矩阵及二次型 &2&#xff09;方阵的特征值与特征向量 &3&#xff…...

1.1 硬件与micropython固件烧录及自编译固件

1.ESP32硬件和固件 淘宝搜ESP32模块,20-50元都有,自带usb口,即插即用. 固件下载地址:MicroPython - Python for microcontrollers 2.烧录方法 为简化入门难度,建议此处先使用带GUI的开发工具THonny,记得不是给你理发的tony老师. 烧录的入口是: 后期通过脚本一次型生成和烧…...

【MySQL进阶】视图 存储过程 触发器

&#x1f60a;&#x1f60a;作者简介&#x1f60a;&#x1f60a; &#xff1a; 大家好&#xff0c;我是南瓜籽&#xff0c;一个在校大二学生&#xff0c;我将会持续分享Java相关知识。 &#x1f389;&#x1f389;个人主页&#x1f389;&#x1f389; &#xff1a; 南瓜籽的主页…...

[Linux篇] Linux常见命令和权限

文章目录使用XShell登录Linux1.Linux常用基本命令&#xff1a;1.1 ls&#xff08;列出当前的目录下都有哪些文件和目录&#xff09;1.2 cd (change directory 切换目录)1.3 pwd&#xff08;查看当前目录的绝对路径&#xff09;1.4 touch&#xff08;创建文件&#xff09;1.5 ca…...

29岁从事功能测试被辞,面试2个月都找不到工作吗?

最近一个28岁老同学联系我&#xff0c;因为被公司辞退&#xff0c;找我倾诉&#xff0c;于是写下此文。 他是14年二本毕业&#xff0c;在我的印象里人特别懒&#xff0c;不爱学习&#xff0c;专业不好&#xff0c;毕业前因为都没找到合适工作&#xff0c;直接去创业了&#xf…...

【C#个人错题笔记1】

观前提醒 记录一些我不会或者少见的内容&#xff0c;不一定适合所有人 字符串拼接 int a3,b8; Console.WriteLine(ab);//11 Console.WriteLine("ab");//ab Console.WriteLine(a""b);//38 Console.WriteLine("ab"ab);//ab38 Console.WriteLine…...

基于lambda的mongodb查询插件

需求背景需要一个像mybatis plus 一样的基于lambda, 且面向对象的查询mongo数据的插件。在网上找了很久&#xff0c;没有发现有类似功能的插件。于是自己手写了一个&#xff0c;借助mongoTemplate屏蔽了底层查询语句的实现细节。在此基础上&#xff0c;实现了查询的统一封装。技…...

基于微信小程序的微信社团小程序

文末联系获取源码 开发语言&#xff1a;Java 框架&#xff1a;ssm JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏览器…...

GEE学习笔记 七十三:【GEE之Python版教程七】静态展示影像和动态展示影像

我们使用GEE在线编辑可以直接通过在线的网页可以加载展示我们计算的结果&#xff0c;而python版的GEE要展示我们的计算结果可能就比较麻烦。如果有同学看过GEE的python版API中可以找到一个类ee.mapclient&#xff0c;这个类的介绍是它是GEE官方通过Tk写的一个加载展示地图的类。…...

PGLBox全面解决图训练速度、成本、稳定性、复杂算法四大问题!

图神经网络&#xff08;Graph Neural Network&#xff0c;GNN&#xff09;是近年来出现的一种利用深度学习直接对图结构数据进行学习的方法&#xff0c;通过在图中的节点和边上制定聚合的策略&#xff0c;GNN能够学习到图结构数据中节点以及边内在规律和更加深层次的语义特征。…...

超详细的 pytest 教程(一)使用入门篇

前言 pytest到目前为止还没有翻译的比较好全面的使用文档&#xff0c;很多英文不太好的小伙伴&#xff0c;在学习时看英文文档还是很吃力。本来去年就计划写pytest详细的使用文档的&#xff0c;由于时间关系一直搁置&#xff0c;直到今天才开始写。本文是第一篇&#xff0c;主…...

网站创意设计方案/b站视频推广怎么买

我正在努力以自己的方式打印JTables。这是我在打印时想要获得的&#xff1a;Page 1 Page 2 Page 3| 1 2 3 | | 4 5 6 | | 7 8 9 ||1 | |1 | |1 ||2 | |2 | |2 ||3 | |3 | |3 |Page 4 Page 5 Page 6| 1 2 3 | | 4 5 6 | | 7 8 9 ||4 | |4 | |4 ||5 | |5 | |5 ||6 | |6 | |6 |如您…...

网站开发怎么挣钱/网络营销收获与体会

1、两张表关联用的三种连接: left join 、right join 、inner join区别 &#xff08;备注&#xff1a;两个表链接肯定是根据两个表&#xff08;如A B&#xff09;中的两个字段值&#xff08;如A.bId和B.id&#xff09;&#xff0c;相等就行连接&#xff09; left join就是只要…...

唐县做网站/公司网站搭建流程

转眼间&#xff0c;2019就到了末尾&#xff0c;回首这一年&#xff0c;一半颓废&#xff0c;一半努力。上半年过的滋滋润润的&#xff0c;而下半年却为了找工作找到了高考的感觉&#xff0c;基本上&#xff0c;果然出来混都是要还的。 回想起来大学四年&#xff0c;感觉也就大…...

网站建设模块方案/百度游戏中心app

ag-grid setPinnedTopRowData设置表格置顶行汇总行有时我们需要在表格顶部设置一行置顶行进行汇总&#xff0c;这时我们可以通过ag-grid setPinnedTopRowData设置表格置顶行汇总行。效果图setPinnedTopRowData代码例子//定义表格列var columnDefs [{ headerName: 名称, field:…...

提供手机自适应网站公司/如何创建自己的网站

欢迎关注头条号&#xff1a;Java小野猫web项目部署到tomcat上之后&#xff0c;有时需要打断点单步调试&#xff0c;如果用的是Intellij idea&#xff0c;可以通过如下方法实现&#xff1a;开启debug端口&#xff0c;启动tomcat以tomcat7.0.75为例&#xff0c;打开bin目录下的ca…...

用6数字域名做网站的是/深圳网站提升排名

用 64bit 的 Python 调用 32bit 的 Dll 会出错 转载于:https://www.cnblogs.com/blog-3123958139/p/5574501.html...