某天和同事闲聊,同事发现一个现象,PostgreSQL在空闲状态时(没有active连接),主进程的pstack显示一直在调用/lib64/libc.so.6的__select_nocancel ()接口,而子进程基本都在调用__epoll_wait_nocancel ()接口。同事咨询这是什么功能,有什么区别。之前对这块没有深入了解,只知道是多路复用的接口,因此翻阅了下源码,查询了一些资料,对这个问题做了下总结。

主进程PostMaster的stack信息:

[postgres@postgres_zabbix ~]$ pstack 6614
#0  0x00007f1706ee00d3 in __select_nocancel () from /lib64/libc.so.6
#1  0x00000000007e2819 in ServerLoop () at postmaster.c:1668
#2  0x00000000007e21fd in PostmasterMain(argc=1, argv=0x2045c00) at postmaster.c:1377
#3  0x000000000070f76d in main (argc=1, argv=0x2045c00) at main.c:228
[postgres@postgres_zabbix ~]$

子进程Checkpointer的stack信息:

[postgres@postgres_zabbix ~]$ pstack 6617
#0  0x00007f1706ee95e3 in __epoll_wait_nocancel () from /lib64/libc.so.6
#1  0x0000000000853571 in WaitEventSetWaitBlock (set=0x207a500, cur_timeout=300000, occurred_events=0x7ffcac6f1ce0, nevents=1) at latch.c:1080
#2  0x000000000085344c in WaitEventSetWait (set=0x207a500, timeout=300000, occurred_events=0x7ffcac6f1ce0, nevents=1, wait_event_info=83886084) at latch.c:1032
#3  0x0000000000852d38 in WaitLatchOrSocket (latch=0x7f17001d4894, wakeEvents=41, sock=-1, timeout=300000, wait_event_info=83886084) at latch.c:407
#4  0x0000000000852c03 in WaitLatch (latch=0x7f17001d4894, wakeEvents=41, timeout=300000, wait_event_info=83886084) at latch.c:347
#5  0x00000000007d6361 in CheckpointerMain() at checkpointer.c:554
#6  0x000000000054c136 in AuxiliaryProcessMain (argc=2, argv=0x7ffcac6f1f00) at bootstrap.c:461
#7  0x00000000007e6f43 in StartChildProcess (type=CheckpointerProcess) at postmaster.c:5392
#8  0x00000000007e46be in reaper (postgres_signal_arg=17) at postmaster.c:2973
#9  <signal handler called>
#10 0x00007f1706ee00d3 in __select_nocancel () from /lib64/libc.so.6
#11 0x00000000007e2819 in ServerLoop () at postmaster.c:1668
#12 0x00000000007e21fd in PostmasterMain (argc=1, argv=0x2045c00) at postmaster.c:1377
#13 0x000000000070f76d in main (argc=1, argv=0x2045c00) at main.c:228
[postgres@postgres_zabbix ~]

strace跟踪PostMaster系统调用:

[postgres@postgres_zabbix ~]$ strace -p 6614
strace: Process 6614 attached
select(7, [4 5 6], NULL, NULL, {33, 429024}) = 0 (Timeout)
rt_sigprocmask(SIG_SETMASK, ~[ILL TRAP ABRT BUS FPE SEGV CONT SYS RTMIN RT_1], NULL, 8) = 0
open("postmaster.pid", O_RDWR)          = 11
read(11, "6614\n/home/postgres/postgresql-1"..., 8191) = 111
close(11)                               = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
select(7, [4 5 6], NULL, NULL, {60, 0}) = 0 (Timeout)
rt_sigprocmask(SIG_SETMASK, ~[ILL TRAP ABRT BUS FPE SEGV CONT SYS RTMIN RT_1], NULL, 8) = 0
open("postmaster.pid", O_RDWR)          = 11
read(11, "6614\n/home/postgres/postgresql-1"..., 8191) = 111
close(11)                               = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
<detached ...>
[postgres@postgres_zabbix ~]$

跟踪CheckPointer的系统调用:

[postgres@postgres_zabbix ~]$ strace -p 6617
strace: Process 6617 attached
epoll_wait(4, [], 1, 13051)             = 0
close(4)                                = 0
sendto(10, "\f\0\0\0X\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 88, 0, NULL, 0) = 88
epoll_create1(EPOLL_CLOEXEC)            = 4
epoll_ctl(4, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLERR|EPOLLHUP, {u32=34055472, u64=34055472}}) = 0
epoll_ctl(4, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLERR|EPOLLHUP, {u32=34055496, u64=34055496}}) = 0
epoll_wait(4, [], 1, 300000)            = 0
close(4)                                = 0
sendto(10, "\f\0\0\0X\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 88, 0, NULL, 0) = 88
epoll_create1(EPOLL_CLOEXEC)            = 4
epoll_ctl(4, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLERR|EPOLLHUP, {u32=34055472, u64=34055472}}) = 0
epoll_ctl(4, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLERR|EPOLLHUP, {u32=34055496, u64=34055496}}) = 0
epoll_wait(4, ^Cstrace: Process 6617 detached<detached ...>
[postgres@postgres_zabbix ~]$

以上信息可以看到主进程PostMaster一直在轮询调用select,而CheckPointer一直在调用epoll_wait。

之前提到这两个是io多路复用接口,首先看下定义

1.什么是io多路复用

io模型大致有5种:
1)阻塞IO
2)非阻塞IO
3)IO复用(select、poll、epoll)
4)信号驱动
5)异步IO(Posix.1的aio系列函数)
前4种均为同步IO,其中I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功,这样可以提高CPU的有效利用率。写操作类似,操作系统的这个功能通过select/poll/epoll之类的系统调用来实现,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

2.IO复用的使用场景

IO复用解决的就是并发型的问题,比如多个用户并发访问一个WEB网站,对于服务端后台而言就会产生多个请求,处理多个请求对于中间件就会产生多个IO流对于系统的读写。那么对于IO流请求操作系统内核有并行处理和串行处理的概念,串行处理的方式是一个个处理,前面的发生阻塞,就没办法完成后面的请求。这个时候我们必须考虑并行的方式完成整个IO流的请求来实现最大的并发和吞吐,这时候就是用到IO复用技术。IO复用就是让一个Socket来作为复用完成整个IO流的请求,当然实现整个IO流的请求多线程的方式就是其中一种。
参考应用服务器软件的场景,DB软件也是相同的道理。

3.select和epoll的底层实现

1)select

函数原型:

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

功能:

监视并等待多个文件描述符的属性变化(可读、可写或错误异常)。select()函数监视的文件描述符分 3 类,分别是writefds、readfds、和 exceptfds。调用后 select() 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间),函数才返回。当 select()函数返回后,可以通过遍历 fdset,来找到就绪的描述符。

参数:

nfds: 要监视的文件描述符的范围,一般取监视的描述符数的最大值+1,如这里写 10, 这样的话,描述符 0,1, 2 …… 9 都会被监视,在 Linux 上最大值一般为1024。
readfd: 监视的可读描述符集合,只要有文件描述符即将进行读操作,这个文件描述符就存储到这。
writefds: 监视的可写描述符集合。
exceptfds: 监视的错误异常描述符集合
中间的三个参数 readfds、writefds 和 exceptfds 指定我们要让内核监测读、写和异常条件的描述字。如果不需要使用某一个的条件,就可以把它设为空指针( NULL )。
timeout: 超时时间,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其 timeval 结构用于指定这段时间的秒数和微秒数。
集合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);
超时时间timeout,其 timeval 结构用于指定这段时间的秒数和微秒数
struct timeval
{time_t tv_sec;       /* 秒 */suseconds_t tv_usec; /* 微秒 */
};这个参数有三种可能:
1)永远等待下去:仅在有一个描述字准备好 I/O 时才返回。为此,把该参数设置为空指针 NULL。2)等待固定时间:在指定的固定时间( timeval 结构中指定的秒数和微秒数)内,在有一个描述字准备好 I/O 时返回,如果时间到了,就算没有文件描述符发生变化,这个函数会返回 0。3)根本不等待(不阻塞):检查描述字后立即返回,这称为轮询。为此,struct timeval变量的时间值指定为 0 秒 0 微秒,文件描述符属性无变化返回 0,有变化返回准备好的描述符数量。

返回值:

成功:就绪描述符的数目,超时返回 0出错:-1

2)epoll

epoll由三个函数实现: epoll_create、epoll_ctl、epoll_wait

函数原型:

int epoll_create(int size);

功能:

该函数生成一个 epoll 专用的文件描述符(创建一个 epoll 的句柄)。

参数:

size: 用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/ 进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。

返回值:

成功:epoll 专用的文件描述符失败:-1

函数原型:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能:

epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

参数:

epfd: epoll 专用的文件描述符,epoll_create()的返回值op: 表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从 epfd 中删除一个 fd;fd: 需要监听的文件描述符event: 告诉内核要监听什么事件,struct epoll_event 结构如下:[cpp] view plain copy
// 保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64;
}epoll_data_t;
// 感兴趣的事件和被触发的事件
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 队列里

返回值:

成功:0失败:-1

函数原型:

int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );

功能:

等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。

参数:

epfd: epoll 专用的文件描述符,epoll_create()的返回值events: 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。maxevents: maxevents 告之内核这个 events 有多大 。timeout: 超时时间,单位为毫秒,为 -1 时,函数为阻塞

返回值:

成功:返回需要处理的事件数目,如返回 0 表示已超时。失败:-1

3)原理简述

调用select时,会发生以下事情:

  1. 从用户空间拷贝fd_set到内核空间;
  2. 注册回调函数__pollwait;
  3. 遍历所有fd,对全部指定设备做一次poll(这里的poll是一个文件操作,它有两个参数,一个是文件fd本身,一个是当设备尚未就绪时调用的回调函数__pollwait,这个函数把设备自己特有的等待队列传给内核,让内核把当前的进程挂载到其中);
  4. 当设备就绪时,设备就会唤醒在自己特有等待队列中的【所有】节点,于是当前进程就获取到了完成的信号。poll文件操作返回的是一组标准的掩码,其中的各个位指示当前的不同的就绪状态(全0为没有任何事件触发),根据mask可对fd_set赋值;
    如果所有设备返回的掩码都没有显示任何的事件触发,就去掉回调函数的函数指针,进入有限时的睡眠状态,再恢复和不断做poll,再作有限时的睡眠,直到其中一个设备有事件触发为止。
    只要有事件触发,系统调用返回,将fd_set从内核空间拷贝到用户空间,回到用户态,用户就可以对相关的fd作进一步的读或者写操作了。

epoll:

调用epoll_create时,做了以下事情:

内核帮我们在epoll文件系统里建了个file结点;
在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket;
建立一个list链表,用于存储准备就绪的事件。

调用epoll_ctl时,做了以下事情:
把socket放到epoll文件系统里file对象对应的红黑树上;
给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。

调用epoll_wait时,做了以下事情:
观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。

总结如下:

一颗红黑树,一张准备就绪句柄链表,少量的内核cache,解决了大并发下的socket处理问题。

执行epoll_create时,创建了红黑树和就绪链表;
执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;
执行epoll_wait时立刻返回准备就绪链表里的数据即可。

两种模式的区别:

LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时重复返回这个句柄,而ET模式仅在第一次返回。

两种模式的实现:

当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait检查这些socket,如果是LT模式,并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表。所以,LT模式的句柄,只要它上面还有事件,epoll_wait每次都会返回。

4)对比及适用场景

对比

前边有提到5种IO模型,这里补充下其中几个模型区别。主要比对IO复用模型

假设你现在去女生宿舍楼找自己的女神, 但是你只知道女神的手机号,并不知道女神的具体房间,如何找到女神呢?

  1. 阻塞IO, 给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待你不会做其他事情, 属于备胎做法.
  2. 非阻塞IO, 给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间你除了发短信等待不会做其他事情, 属于专一做法.
  3. IO多路复用,是找一个宿管阿姨来帮你监视下楼的女生, 这个期间你可以些其他的事情. 例如可以顺便看看其他妹子,玩玩王者荣耀, 上个厕所等等,属于高效做法。

你:(应用进程/线程),宿管阿姨:(select/epoll),所有女生(监测的所有FD),女神:(准备就绪的FD)

select阿姨每一个女生下楼, select阿姨都不知道这个是不是你的女神, 她需要一个一个询问, 并且select阿姨能力还有限, 最多一次帮你监视1024个妹子

epoll阿姨不限制盯着女生的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll阿姨会为每个进宿舍楼的女生脸上贴上一个大字条,上面写上女生自己的名字, 只要女生下楼了, epoll阿姨就知道这个是不是你女神了, 然后阿姨再通知你

select缺点
  1. 最大并发数限制:使用32个整数的32位,即32*32=1024来标识fd,虽然可修改,但是有以下第二点的瓶颈;
  2. 效率低:每次都会线性扫描整个fd_set,集合越大速度越慢;
  3. 内核/用户空间内存拷贝问题。
epoll的提升
  1. 本身没有最大并发连接的限制,仅受系统中进程能打开的最大文件数目限制;
  2. 效率提升:只有活跃的socket才会主动的去调用callback函数;
  3. 省去不必要的内存拷贝:epoll通过内核与用户空间mmap同一块内存实现。
适用场景

select:

并发量较低,socket连接都比较活跃的情况下,即网络使用场景不需要高并发的情况下(比如客户端或者服务端内部通讯) 。

epoll:
高并发,且任一时间只有少数socket连接是活跃的,例如服务器的前端接入高并发场景。

4.PostgreSQL中的使用

PostMaster中select的使用

PostMaster的核心功能是:
1.做一些初始化,依次启动postgresql的所需的所有辅助子进程(logger、checkpoint、bgwriter、walwrite、autovacuum、stats collector等)
2.进入ServerLoop,是一个无条件的for循环,在这个循环里,主要做的是
1)select监测socket,当socket的文件描述符可用时,例如客户端发来连接请求,fork出一个子进程postgres作为服务端去处理应用客户端的sql请求
2)监控所有子进程的状态

代码如下:

static int
ServerLoop(void)
{fd_set     readmask;int            nSockets;time_t     last_lockfile_recheck_time,last_touch_time;last_lockfile_recheck_time = last_touch_time = time(NULL);##initMasks函数传递socket相关的FD集合,传递到select函数的第二个参数FD_SET *,nSockets 为FD_SET集合最后一个元素+1nSockets = initMasks(&readmask);for (;;){fd_set     rmask;int           selres;time_t       now;##将FD_SET集合,拷贝到&rmaskmemcpy((char *) &rmask, (char *) &readmask, sizeof(fd_set));if (pmState == PM_WAIT_DEAD_END){PG_SETMASK(&UnBlockSig);pg_usleep(100000L); /* 100 msec seems reasonable */selres = 0;PG_SETMASK(&BlockSig);}else{/* must set timeout each time; some OSes change it! */struct timeval timeout;/* Needs to run with blocked signals! */##timeout有多种取值,在DetermineSleepTime函数中做不同分支处理DetermineSleepTime(&timeout);         PG_SETMASK(&UnBlockSig);##这里调用select轮询监测selres = select(nSockets, &rmask, NULL, NULL, &timeout);PG_SETMASK(&BlockSig);}/* Now check the select() result */##select返回0说明出现错误if (selres < 0){if (errno != EINTR && errno != EWOULDBLOCK){ereport(LOG,(errcode_for_socket_access(),errmsg("select() failed in postmaster: %m")));return STATUS_ERROR;}}/** New connection pending on any of our sockets? If so, fork a child* process to deal with it.*/##select结果大于0, 即socket FD准备就绪,if (selres > 0){int            i;for (i = 0; i < MAXLISTEN; i++){if (ListenSocket[i] == PGINVALID_SOCKET)break;##确定FD_SET可读,可以创建连接if (FD_ISSET(ListenSocket[i], &rmask)){Port      *port;##根据socket FD来创建连接port = ConnCreate(ListenSocket[i]);if (port){##连接建立后,BackendStartup函数里会fork出一个postgres子进程,进行相关的初始化,然后由该进程处理客户端连接的sql请求BackendStartup(port);/** We no longer need the open socket or port structure* in this process*/StreamClose(port->sock);ConnFree(port);}}}}......

从代码来看,select一直在监测socket FD是否就绪,select的返回值大于0则说明已经就绪;若小于0则报错退出;若等于0(等于0的情况可以理解为达到select timeout周期),函数没做分支,那就继续轮询serverLoop了。

那为什么用select?不用epoll?刚才已经分析过场景了,PostMaster进程这里,并发量相对应用服务器前端是比较小的,并且每次监测的FD数量不多。并且应用连接经常是活跃的,所以使用select效率相对较高。

文章最早的strace跟踪中可以看到,FD_SET集合也就三个成员4,5,6,且此时select返回0,说明socket FD未就绪,没有连接请求。达到timeout后,又继续轮询了。

postgres@postgres_zabbix ~]$ strace -p 6614
strace: Process 6614 attached
select(7, [4 5 6], NULL, NULL, {33, 429024}) = 0 (Timeout)
rt_sigprocmask(SIG_SETMASK, ~[ILL TRAP ABRT BUS FPE SEGV CONT SYS RTMIN RT_1], NULL, 8) = 0
open("postmaster.pid", O_RDWR)          = 11
read(11, "6614\n/home/postgres/postgresql-1"..., 8191) = 111
close(11)                               = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0

那么FD为4,5,6是什么东东?
lsof查看为socket套接字,即IPV4,IPV6和/tmp/.s.PGSQL.6545文件
对应以下第4列为4u,5u,6u。u为FD类型

[postgres@postgres_zabbix ~]$ lsof|grep 6614
postgres  6614            postgres  cwd       DIR              253,0      4096   8476560 /home/postgres/postgresql-12.2/pg12debug/data
postgres  6614            postgres  rtd       DIR              253,0      4096        64 /
postgres  6614            postgres  txt       REG              253,0  23127936  36307686 /home/postgres/postgresql-12.2/pg12debug/bin/postgres
postgres  6614            postgres  DEL       REG                0,4               80881 /dev/zero
postgres  6614            postgres  mem       REG              253,0     61624    365466 /usr/lib64/libnss_files-2.17.so
postgres  6614            postgres  mem       REG              253,0   1010528  36393762 /home/postgres/postgresql-12.2/pg12debug/lib/pg_pathman.so
postgres  6614            postgres  mem       REG              253,0 106075056    365438 /usr/lib/locale/locale-archive
postgres  6614            postgres  mem       REG              253,0   2151672    365445 /usr/lib64/libc-2.17.so
postgres  6614            postgres  mem       REG              253,0   1137024    365453 /usr/lib64/libm-2.17.so
postgres  6614            postgres  mem       REG              253,0     19288    365451 /usr/lib64/libdl-2.17.so
postgres  6614            postgres  mem       REG              253,0     43776    372649 /usr/lib64/librt-2.17.so
postgres  6614            postgres  mem       REG              253,0    141968    372643 /usr/lib64/libpthread-2.17.so
postgres  6614            postgres  mem       REG              253,0    163400    365437 /usr/lib64/ld-2.17.so
postgres  6614            postgres  mem       REG               0,19      7408     79783 /dev/shm/PostgreSQL.1250449390
postgres  6614            postgres  DEL       REG                0,4              131072 /SYSV0063de69
postgres  6614            postgres    0r      CHR                1,3       0t0      1043 /dev/null
postgres  6614            postgres    1w     FIFO                0,9       0t0     79785 pipe
postgres  6614            postgres    2w     FIFO                0,9       0t0     79785 pipe
postgres  6614            postgres    3u     unix 0xffff8fb797c42000       0t0     79774 socket
postgres  6614            postgres    4u     IPv6              79780       0t0       TCP localhost:6545 (LISTEN)
postgres  6614            postgres    5u     IPv4              79781       0t0       TCP localhost:6545 (LISTEN)
postgres  6614            postgres    6u     unix 0xffff8fb797c44c00       0t0     79782 /tmp/.s.PGSQL.6545
postgres  6614            postgres    7r     FIFO                0,9       0t0     79784 pipe
postgres  6614            postgres    8w     FIFO                0,9       0t0     79784 pipe
postgres  6614            postgres    9r     FIFO                0,9       0t0     79785 pipe
postgres  6614            postgres   10u     IPv6              79789       0t0       UDP localhost:43284->localhost:43284
[postgres@postgres_zabbix ~]$

之前分析过了FD_SET的集合是在initMasks函数中指定的,gdb跟踪如下:

Breakpoint 3, initMasks (rmask=0x7fffffffe180) at postmaster.c:1862
1862            int                     maxsock = -1;
(gdb) n
1865            FD_ZERO(rmask);
(gdb)
1867            for (i = 0; i < MAXLISTEN; i++)
(gdb) n
1869                    int                     fd = ListenSocket[i];
(gdb)
1871                    if (fd == PGINVALID_SOCKET)
(gdb) p i
##这里确定了FD_SET的集合为[4 5 6]
$3 = 0
(gdb) p ListenSocket[i]
$4 = 4
(gdb) p ListenSocket[1]
$6 = 5
(gdb) p ListenSocket[2]
$7 = 6
##下标大于2以后ListenSocket[i]返回-1,就是不存在,退出循环,返回FD_SET 集合
(gdb) p ListenSocket[3]
$8 = -1
1871                    if (fd == PGINVALID_SOCKET)
(gdb)
1873                    FD_SET(fd, rmask);
(gdb)
1875                    if (fd > maxsock)
(gdb)## 这里maxsock 为6
1876                            maxsock = fd;
(gdb)
1867            for (i = 0; i < MAXLISTEN; i++)
(gdb) p i
$8 = 3
1869                    int                     fd = ListenSocket[i];
(gdb)##跳出循环
1871                    if (fd == PGINVALID_SOCKET)
(gdb)
1872                            break;
(gdb)##返回7
1879            return maxsock + 1;

与select实际调用传参一致,所以在数据库没连接请求时,select大多时间返回0,可以理解为一直在轮询等待
select(7, [4 5 6], NULL, NULL, {33, 429024}) = 0 (Timeout)

验证一下有连接请求的处理:
session 1 继续strace跟踪系统调用
session 2 用psql发起一个连接请求

session 2:
建立连接

[postgres@postgres_zabbix ~]$ psql -h 127.0.0.1
psql (12.2)
Type "help" for help.postgres=#

session 1:
此时select结果已经为1了,已经在处理连接请求了

select(7, [4 5 6], NULL, NULL, {60, 0}) = 1 (in [5], left {57, 174358})
rt_sigprocmask(SIG_SETMASK, ~[ILL TRAP ABRT BUS FPE SEGV CONT SYS RTMIN RT_1], NULL, 8) = 0
accept(5, {sa_family=AF_INET, sin_port=htons(40532), sin_addr=inet_addr("127.0.0.1")}, [16]) = 11
getsockname(11, {sa_family=AF_INET, sin_port=htons(6545), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(11, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(11, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
open("/dev/urandom", O_RDONLY)          = 12
read(12, "\323\6\246\312", 4)           = 4
close(12)                               = 0
PostgreSQL子进程中epoll的使用

为什么其他子进程中大多是以epoll来处理?
简单分析了下主要有两个原因:

  1. select 一般单次默认只能监测1024个FD,并且FD线性增加后,处理效率越来越低效,时间复杂度为O(n)。而epoll的时间复杂度为O(1),在这种情况下是更高效的。
  2. 是因为统一, 方便封装/使用和调测等,PostgreSQL的部分等待事件(event),其实就是根据调用接口场景,自定义事件类型,底层epoll实现。可以理解为对epoll做了不同封装。

以CheckPointer进程为例,来看看epoll的处理过程

抓取CheckPointer的内核调用:

[postgres@postgres_zabbix ~]$ strace -p 15376
strace: Process 15376 attached
epoll_wait(5, [], 1, 84768)             = 0
close(5)                                = 0
sendto(10, "\f\0\0\0X\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 88, 0, NULL, 0) = 88
epoll_create1(EPOLL_CLOEXEC)            = 5
epoll_ctl(5, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLERR|EPOLLHUP, {u32=34055472, u64=34055472}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLERR|EPOLLHUP, {u32=34055496, u64=34055496}}) = 0
epoll_wait(5, [], 1, 84768)             = 0
close(5)                                = 0
sendto(10, "\f\0\0\0X\0\0\0\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 88, 0, NULL, 0) = 88
epoll_create1(EPOLL_CLOEXEC)            = 5
epoll_ctl(5, EPOLL_CTL_ADD, 11, {EPOLLIN|EPOLLERR|EPOLLHUP, {u32=34055472, u64=34055472}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLERR|EPOLLHUP, {u32=34055496, u64=34055496}}) = 0

lsof查看FD 5,11,7分别为[eventpoll]和pipe,对应以下第4列为5u,7r,11r

[postgres@postgres_zabbix ~]$ lsof|grep 15376
postgres  15376            postgres  cwd       DIR              253,0      4096   8476560 /home/postgres/postgresql-12.2/pg12debug/data
postgres  15376            postgres  rtd       DIR              253,0      4096        64 /
postgres  15376            postgres  txt       REG              253,0  23127936  36307686 /home/postgres/postgresql-12.2/pg12debug/bin/postgres
postgres  15376            postgres  DEL       REG                0,4              163258 /dev/zero
postgres  15376            postgres  mem       REG              253,0     61624    365466 /usr/lib64/libnss_files-2.17.so
postgres  15376            postgres  mem       REG              253,0   1010528  36393762 /home/postgres/postgresql-12.2/pg12debug/lib/pg_pathman.so
postgres  15376            postgres  mem       REG              253,0 106075056    365438 /usr/lib/locale/locale-archive
postgres  15376            postgres  mem       REG              253,0   2151672    365445 /usr/lib64/libc-2.17.so
postgres  15376            postgres  mem       REG              253,0   1137024    365453 /usr/lib64/libm-2.17.so
postgres  15376            postgres  mem       REG              253,0     19288    365451 /usr/lib64/libdl-2.17.so
postgres  15376            postgres  mem       REG              253,0     43776    372649 /usr/lib64/librt-2.17.so
postgres  15376            postgres  mem       REG              253,0    141968    372643 /usr/lib64/libpthread-2.17.so
postgres  15376            postgres  mem       REG              253,0    163400    365437 /usr/lib64/ld-2.17.so
postgres  15376            postgres  mem       REG              253,0     81139    365436 /usr/share/locale/zh_CN/LC_MESSAGES/libc.mo
postgres  15376            postgres  mem       REG              253,0     26254  67431869 /usr/lib64/gconv/gconv-modules.cache
postgres  15376            postgres  mem       REG               0,19      7408    163260 /dev/shm/PostgreSQL.1517397882
postgres  15376            postgres  DEL       REG                0,4              163840 /SYSV0063de69
postgres  15376            postgres    0r      CHR                1,3       0t0      1043 /dev/null
postgres  15376            postgres    1w     FIFO                0,9       0t0     79785 pipe
postgres  15376            postgres    2w     FIFO                0,9       0t0     79785 pipe
postgres  15376            postgres    3u     unix 0xffff8fb797c42000       0t0     79774 socket
postgres  15376            postgres    4u      REG              253,0  16777216  36322961 /home/postgres/postgresql-12.2/pg12debug/data/pg_wal/000000010000000000000001
postgres  15376            postgres    5u  a_inode               0,10         0      6904 [eventpoll]
postgres  15376            postgres    7r     FIFO                0,9       0t0     79784 pipe
postgres  15376            postgres   10u     IPv6              79789       0t0       UDP localhost:43284->localhost:43284
postgres  15376            postgres   11r     FIFO                0,9       0t0    163261 pipe
postgres  15376            postgres   12w     FIFO                0,9       0t0    163261 pipe

这里只贴出了两次轮询结果,在CheckPointer空闲时,epoll_wait返回值为0(这里是由于设置了timeout即传递了checkpoint_timeout,如果没设置timeout会一直在epoll_wait里sleep,不会返回)

这里不详细分析代码了,简述下过程

  1. CheckperointMain的for循环一直在轮询调用waitLatch,之后调用链为:
    waitLatch => WaitLatchOrSocket => CreateWaitEventSet =>epoll_create1(EPOLL_CLOEXEC) 创建epoll文件句柄,创建了红黑树和就绪链表;

  2. 然后WaitLatchOrSocket => AddWaitEventToSet => WaitEventAdjustEpoll =>epoll_ctl(set->epoll_fd, action, event->fd, &epoll_ev)把监测的FD放到epoll文件系统里file对象对应的红黑树上;

  3. WaitLatchOrSocket => WaitEventSetWait => WaitEventSetWaitBlock => epoll_wait(set->epoll_fd, set->epoll_ret_events,
    nevents, cur_timeout); 一直观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。

那什么时候epoll_wait返回大于0零,也就是监测的文件描述符主备就绪了呢?CheckPointer的主要功能就是做检查点,除了做检查点其他时候都是在epoll_wait中轮询sleep。那么我们手动强制触发checkpoint试试,会有什么结果

开一个session,建立连接,执行checkpoint

[postgres@postgres_zabbix ~]$ psql -h 127.0.0.1
psql (12.2)
Type "help" for help.
postgres=# checkpoint;
CHECKPOINT
postgres=#

此时epoll_wait返回了1,并且read FD 11成功


epoll_wait(5, [{EPOLLIN, {u32=34055472, u64=34055472}}], 1, 300000) = 1
read(11, "\0", 16)                      = 1
epoll_wait(5, ^Cstrace: Process 15376 detached<detached ...>
[postgres@postgres_zabbix ~]

这里只是简述了epoll在CheckPointer子进程中的一种使用场景,其余的子进程大都类似,只是不同进程调用epoll的函数接口不同而已。

PostgreSQL的部分等待事件wait_event实现原理

刚才提到了PostgreSQL的部分等待事件,其实就是不同接口,根据场景自定义等待事件类型,底层epoll实现。这里顺便分析下原理

当sql执行慢时,我们经常会查看pg_stat_activity
视图,查看wait_event是什么,判断是不是锁冲突等等。

那这里的event是哪里来的?怎么来的?

查看视图如下:

-[ RECORD 1 ]----+--------------------------------
datid            |
datname          |
pid              | 15376
usesysid         |
usename          |
application_name |
client_addr      |
client_hostname  |
client_port      |
backend_start    | 2020-05-18 17:30:59.165047+08
xact_start       |
query_start      |
state_change     |
wait_event_type  | Activity
wait_event       | CheckpointerMain
state            |
backend_xid      |
backend_xmin     |
query            |
backend_type     | checkpointer
-[ RECORD 2 ]----+--------------------------------
datid            | 13593
datname          | postgres
pid              | 15401
usesysid         | 10
usename          | postgres
application_name | psql
client_addr      | 127.0.0.1
client_hostname  |
client_port      | 40534
backend_start    | 2020-05-18 17:31:47.48813+08
xact_start       |
query_start      |
state_change     | 2020-05-18 17:31:47.57147+08
wait_event_type  | Client
wait_event       | ClientRead
state            | idle
backend_xid      |
backend_xmin     |
query            |
backend_type     | client backend

这里两条记录,分别对应两个进程Checkpoint和postgres(psql连接),等待事件为CheckpointerMain和ClientRead。

我自己的数据库是编译的debug版本,进程堆栈可以打印出实参传递信息,我们可以看看这些等待事件是在哪传递的

Checkpointer:

[postgres@postgres_zabbix ~]$ pstack 15376
#0  0x00007f1706ee95e3 in __epoll_wait_nocancel () from /lib64/libc.so.6
#1  0x0000000000853571 in WaitEventSetWaitBlock (set=0x207a500, cur_timeout=300000, occurred_events=0x7ffcac6f1ce0, nevents=1) at latch.c:1080
#2  0x000000000085344c in WaitEventSetWait (set=0x207a500, timeout=300000, occurred_events=0x7ffcac6f1ce0, nevents=1, wait_event_info=83886084) at latch.c:1032
#3  0x0000000000852d38 in WaitLatchOrSocket (latch=0x7f17001d4544, wakeEvents=41, sock=-1, timeout=300000, wait_event_info=83886084) at latch.c:407
#4  0x0000000000852c03 in WaitLatch (latch=0x7f17001d4544, wakeEvents=41, timeout=300000, wait_event_info=83886084) at latch.c:347
#5  0x00000000007d6361 in CheckpointerMain () at checkpointer.c:554
#6  0x000000000054c136 in AuxiliaryProcessMain (argc=2, argv=0x7ffcac6f1f00) at bootstrap.c:461
#7  0x00000000007e6f43 in StartChildProcess (type=CheckpointerProcess) at postmaster.c:5392
#8  0x00000000007e46be in reaper (postgres_signal_arg=17) at postmaster.c:2973
#9  <signal handler called>
#10 0x00007f1706ee00d3 in __select_nocancel () from /lib64/libc.so.6
#11 0x00000000007e2819 in ServerLoop () at postmaster.c:1668
#12 0x00000000007e21fd in PostmasterMain (argc=1, argv=0x2045c00) at postmaster.c:1377
#13 0x000000000070f76d in main (argc=1, argv=0x2045c00) at main.c:228

可以看到#0行调用的是epoll_wait,最早传递等待事件在#4行WaitLatch 函数中wait_event_info=83886084,这个83886084对应的就是事件CheckpointerMain,后边描述计算过程。

Postgres:

[postgres@postgres_zabbix ~]$ pstack 15401
#0  0x00007f1706ee95e3 in __epoll_wait_nocancel () from /lib64/libc.so.6
#1  0x0000000000853571 in WaitEventSetWaitBlock (set=0x207a448, cur_timeout=-1, occurred_events=0x7ffcac6f2270, nevents=1) at latch.c:1080
#2  0x000000000085344c in WaitEventSetWait (set=0x207a448, timeout=-1, occurred_events=0x7ffcac6f2270, nevents=1, wait_event_info=100663296) at latch.c:1032
#3  0x00000000006fdc17 in secure_read (port=0x20702a0, ptr=0xed5500 <PqRecvBuffer>, len=8192) at be-secure.c:185
#4  0x000000000070ad2c in pq_recvbuf () at pqcomm.c:964
#5  0x000000000070adc6 in pq_getbyte () at pqcomm.c:1007
#6  0x000000000087a4ad in SocketBackend (inBuf=0x7ffcac6f2480) at postgres.c:341
#7  0x000000000087a976 in ReadCommand (inBuf=0x7ffcac6f2480) at postgres.c:514
#8  0x000000000087f22f in PostgresMain (argc=1, argv=0x207a6e0, dbname=0x207a578 "postgres", username=0x207a558 "postgres") at postgres.c:4189
#9  0x00000000007e6a9e in BackendRun (port=0x20702a0) at postmaster.c:4437
#10 0x00000000007e629d in BackendStartup (port=0x20702a0) at postmaster.c:4128
#11 0x00000000007e293d in ServerLoop () at postmaster.c:1704
#12 0x00000000007e21fd in PostmasterMain (argc=1, argv=0x2045c00) at postmaster.c:1377
#13 0x000000000070f76d in main (argc=1, argv=0x2045c00) at main.c:228

和checkpointer相同的是在第#0行调用epoll_wait;而最早传递等待事件是在第 #2 行WaitEventSetWait函数中wait_event_info=100663296,这个100663296对应的就是事件ClientRead。

来看下怎么转换的,83886084和100663296都是10进制数

PostgreSQL对于等待事件的定义在pgstat.h中

如下:

/* ----------* Wait Classes 定义了9个宏,总共有9类事件,对应pg_stat_activity视图的wait_event_type字段* 每个宏都是一个枚举类型,而且给出了首个成员对应值是一个无符号16进制数* 我只拷贝了Activity和Client两个枚举成员,其余的省略* ----------*/#define PG_WAIT_LWLOCK             0x01000000U
#define PG_WAIT_LOCK                0x03000000U
#define PG_WAIT_BUFFER_PIN          0x04000000U
#define PG_WAIT_ACTIVITY            0x05000000U  //这个宏对应查到CheckPoint的事件类型 Activity
#define PG_WAIT_CLIENT              0x06000000U //这个宏对应查到Postgres的事件类型Client
#define PG_WAIT_EXTENSION           0x07000000U
#define PG_WAIT_IPC                 0x08000000U
#define PG_WAIT_TIMEOUT             0x09000000U
#define PG_WAIT_IO                  0x0A000000U/* ----------* Wait Events - Activity** Use this category when a process is waiting because it has no work to do,* unless the "Client" or "Timeout" category describes the situation better.* Typically, this should only be used for background processes.* ----------*/
typedef enum
{WAIT_EVENT_ARCHIVER_MAIN = PG_WAIT_ACTIVITY,WAIT_EVENT_AUTOVACUUM_MAIN,WAIT_EVENT_BGWRITER_HIBERNATE,WAIT_EVENT_BGWRITER_MAIN,WAIT_EVENT_CHECKPOINTER_MAIN, //这里就对应到了Checkpoint的等待事件 CheckpointerMainWAIT_EVENT_LOGICAL_APPLY_MAIN,WAIT_EVENT_LOGICAL_LAUNCHER_MAIN,WAIT_EVENT_PGSTAT_MAIN,WAIT_EVENT_RECOVERY_WAL_ALL,WAIT_EVENT_RECOVERY_WAL_STREAM,WAIT_EVENT_SYSLOGGER_MAIN,WAIT_EVENT_WAL_RECEIVER_MAIN,WAIT_EVENT_WAL_SENDER_MAIN,WAIT_EVENT_WAL_WRITER_MAIN
} WaitEventActivity;/* ----------* Wait Events - Client** Use this category when a process is waiting to send data to or receive data* from the frontend process to which it is connected.  This is never used for* a background process, which has no client connection.* ----------*/
typedef enum
{WAIT_EVENT_CLIENT_READ = PG_WAIT_CLIENT,  //这里就对应到了Postgres的等待事件ClientReadWAIT_EVENT_CLIENT_WRITE,WAIT_EVENT_LIBPQWALRECEIVER_CONNECT,WAIT_EVENT_LIBPQWALRECEIVER_RECEIVE,WAIT_EVENT_SSL_OPEN_SERVER,WAIT_EVENT_WAL_RECEIVER_WAIT_START,WAIT_EVENT_WAL_SENDER_WAIT_WAL,WAIT_EVENT_WAL_SENDER_WRITE_DATA,WAIT_EVENT_GSS_OPEN_SERVER,
} WaitEventClient;

计算过程如下:

  1. CheckpointerMain事件是宏PG_WAIT_ACTIVITY的第4个成员,那么十进制表示为:
    5 * 16的6次方 + 4 = 83886084 // 这里5 * 16的6次方是将0x05000000U转换为十进制数

  2. ClientRead事件是宏PG_WAIT_CLIENT的第0个成员,十进制表示为:
    6 * 16的6次方 = 100663296 //同样也是将0x06000000U转换为十进制数。

还有一点是pg_stat_activity中显示为ClientRead,实际的枚举值为WAIT_EVENT_CLIENT_READ ,是在函数中做了简化,提高可读性。

总结:PostgreSQL的大部分等待事件,其实就是不同接口,根据场景自定义等待事件类型,底层是epoll实现。

参考:https://blog.csdn.net/chengchaonan/article/details/89315763

https://www.toutiao.com/i6826975203554230792/?timestamp=1589706230&app=news_article&group_id=6826975203554230792&req_id=2020051717034901013003701242C49A5E

https://www.cnblogs.com/wt645631686/p/8528912.html

PostgreSQL中的io多路复用--select和epoll实现相关推荐

  1. IO多路复用select/poll/epoll详解以及在Python中的应用

    IO multiplexing(IO多路复用) IO多路复用,有些地方称之为event driven IO(事件驱动IO). 它的好处在于单个进程可以处理多个网络IO请求.select/epoll这两 ...

  2. IO多路复用select,poll epoll以及区别

    看这个一次读懂 Select.Poll.Epoll IO复用技术 文章来简单理解下,如果不是很明白的话,可以参考下面转的知乎上面白话文列子 作者:Leslie 链接:https://www.zhihu ...

  3. python3 异步 非阻塞 IO多路复用 select poll epoll 使用

    有许多封装好的异步非阻塞IO多路复用框架,底层在linux基于最新的epoll实现,为了更好的使用,了解其底层原理还是有必要的. 下面记录下分别基于Select/Poll/Epoll的echo ser ...

  4. Python异步非阻塞IO多路复用Select/Poll/Epoll使用

    来源:http://www.haiyun.me/archives/1056.html 有许多封装好的异步非阻塞IO多路复用框架,底层在linux基于最新的epoll实现,为了更好的使用,了解其底层原理 ...

  5. python poll_python IO 多路复用 select poll epoll

    select select 原理 select 是通过系统调用来监视着一个由多个文件描述符(file descriptor)组成的数组,当select()返回后,数组中就绪的文件描述符会被内核修改标记 ...

  6. java nio原理 epoll_多路复用 Select Poll Epoll 的实现原理(BIO与NIO)

    BIO blocking阻塞的意思,当我们在后端开发使用的时候,accetp 事件会阻塞主线程. 当accept事件执行的时候,客户的会和服务建立一个socket 连接.一般后端就会开启一个线程执行后 ...

  7. linux IO多路复用 select epoll

    概念 IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程 通俗理解(摘自网上一大神) 这些名词比较绕口,理解涵义就好.一个epoll场景:一个酒吧服务员(一个线程),前 ...

  8. IO多路复用 select、poll、epoll

    什么是IO多路复用 在同一个线程里面, 通过拨开关的方式,来同时传输多个(socket)I/O流. 在英文中叫I/O multiplexing.这里面的 multiplexing 指的其实是在单个线程 ...

  9. 详解磁盘IO、网络IO、零拷贝IO、BIO、NIO、AIO、IO多路复用(select、poll、epoll)

    文章很长,但是很用心! 文章目录 1. 什么是I/O 2. 磁盘IO 3. 网络IO 4. IO中断与DMA 5. 零拷贝IO 6. BIO 7. NIO 8. IO多路复用 8.1 select 8 ...

最新文章

  1. vue---进行post和get请求
  2. python小项目案例-拯救Python新手的几个项目实战
  3. 一夜暴富之前的漫漫长路
  4. dhcp只能分配与路由器相同网段么_dhcp工作原理
  5. 59 MM配置-后勤发票校验-维护税代码缺省值
  6. 【转载】Sqlserver使用Convert函数进行数据类型转换
  7. 阶段3 1.Mybatis_11.Mybatis的缓存_3 mybatis一对一实现延迟加载
  8. 《21天学通Java(第6版)》—— 导读
  9. 查看redis安装路径
  10. 计算机网络实验二:网络基础编程实验
  11. 边框给背景图css怎么写,使用css设置边框背景图片
  12. c语言输入一个整数打印出它是奇数还是偶数,1. 编写程序,输入一个整数,打印出它是奇数还是偶数....
  13. 金明的预算方案(01背包)
  14. 2020年9月指数定期审核与调整 | TokenInsight
  15. 全球及中国车载定位模块行业发展格局与运营动向分析报告2022版
  16. 华为认证的含金量高吗?
  17. [C]C语言基本语句(5/7)→ 用scanf语句输入int, float, double, char型数据
  18. 人工智能时代:软件中的人工智能将如何改变程序员的角色
  19. bitcoin源码分析
  20. 美国大学计算机科学专业排名2020,美国大学计算机专业排名2020情况如何?

热门文章

  1. 用户生命周期(User Lifetime)
  2. 什么是中断?什么是中断向量?中断向量表的地址范围?
  3. M: Triangular Relationship (数论)
  4. 【机器学习之向量求导】分子布局 分母布局
  5. 车载以太网 - SomeIP - 总纲
  6. python图片转为64位编码形式
  7. python调用通达信数据_[python]沪深龙虎榜数据导入通达信的自选板块并标注于k线图上...
  8. vscode_历史版本下载_便携版/安装版
  9. 【学习笔记】刘晓艳英语语法笔记(2/6)——并列句
  10. 软考程序员 c java 二选一_往年软考程序员试题分析及备考建议