Postgresql随手记(6)latch实现中self-pipe trick解决什么问题

2022-07-14 13:51:31 浏览数 (3)

self-pipe trick

1 问题在哪里

当你在使用io multiplexor函数时,例如select,有没有考虑过如果收到信号会发生什么?

代码语言:javascript复制
RETURN VALUE
       On success, select() and pselect() return the number of file descriptors contained in the three returned descriptor sets  (that
       is, the total number of bits that are set in readfds, writefds, exceptfds) which may be zero if the timeout expires before any‐
       thing interesting happens.  On error, -1 is returned, and errno is set appropriately; the sets and timeout become undefined, so
       do not rely on their contents after an error.

ERRORS
       EBADF  An  invalid file descriptor was given in one of the sets.  (Perhaps a file descriptor that was already closed, or one on
              which an error has occurred.)

       EINTR  A signal was caught; see signal(7).

       EINVAL nfds is negative or the value contained within timeout is invalid.

       ENOMEM unable to allocate memory for internal tables.

select的答案是:会返回-1,并置EINTR=4(Interrupted system call),结果就是select被信号唤醒了(不是被IO事件唤醒的)

http://cr.yp.to/docs/selfpipe.html

Richard Stevens’s 1992 book ``Advanced programming in the UNIX environment’’ says that you can’t safely mix select() or poll() with SIGCHLD (or other signals). The SIGCHLD might go off while select() is starting, too early to interrupt it, too late to change its timeout. Solution: the self-pipe trick. Maintain a pipe and select for readability on the pipe input. Inside the SIGCHLD handler, write a byte (non-blocking, just in case) to the pipe output. Done. Of course, the Right Thing would be to have fork() return a file descriptor, not a process ID.

收到信号后,系统会跳转到信号处理函数,如果当前正在做一些IO相关的系统调用,例如上面的select,会直接失败返回EINTR。

结果:

1、虽然PG使用sigaction可以定义syscall的行为:SA_RESTART,但这点其实是无法严格保证的。所以在底层代码经常看到这样写的select:

代码语言:javascript复制
while ((ready = select(nfds, &readfds, NULL, NULL, pto)) == -1 && errno == EINTR)

2、信号处理喊出要求比较严格:递归调用场景要求函数是可重入的,或者在信号处理时屏蔽新的信号

3、io多路复用函数和信号处理函数的竞争场景会有并发问题。(select运行中被信号处理函数中断,导致select未预期结果)

那么有什么方法能把信号处理 归并到 IO事件处理里面,用统一的逻辑唤醒进程?

2 self-pipe trick

1、定义一个匿名管道(进出全部非阻塞)只给当前进程自己使用,所以叫self-pipe。

2、用select监听业务IO事件 并 监听self-pipe[0]读端。

3、当信号到来时,信号处理函数在self-pipe[1]中写入1个字节就退出。

4、select被self-pipe[0]唤醒,检查FD_ISSET(self-pipe[0]…)是否就绪,走信号处理流程。

看下面这个例子,可以直接运行:

代码语言:javascript复制
#include <sys/time.h>
#include <sys/select.h>
#include <fcntl.h>
#include <signal.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h> 

#define max(m,n) ((m) > (n) ? (m) : (n))


/* File descriptors for pipe */
static int pfd[2];

static void
handler(int sig)
{
    int savedErrno;                    
    printf("[enter] sig handlern");

    savedErrno = errno;
    if (write(pfd[1], "x", 1) == -1 && errno != EAGAIN) {
        printf("failed to write pipen");
        fflush(stdout);
        exit(1);
    }
    printf("[exit] sig handlern");
    errno = savedErrno;
}

int main(int argc, char *argv[])
{
    /* Initialize 'timeout' */
    struct timeval *pto = NULL;

    /* [SELECT] Build the 'readfds' */
    int fd = 0;
    int nfds = 0;
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(0, &readfds);

    if (fd >= nfds)
      nfds = fd   1;

    /* [PIPE] Create pipe before establishing signal handler to prevent race */
    if (pipe(pfd) == -1) {
        fflush(stdout);
        printf("failed to create pipen");
        fflush(stdout);
        exit(1);
    }

    FD_SET(pfd[0], &readfds);
    nfds = max(nfds, pfd[0]   1);
    printf("nfds: %dn", nfds);

    /* Make read and write ends of pipe nonblocking */
    int flags;
    flags = fcntl(pfd[0], F_GETFL);
    flags |= O_NONBLOCK;
    fcntl(pfd[0], F_SETFL, flags);  

    flags = fcntl(pfd[1], F_GETFL);  
    flags |= O_NONBLOCK;               
    fcntl(pfd[1], F_SETFL, flags);


    struct sigaction sa;    
    sigemptyset(&sa.sa_mask);
     /* Restart interrupted reads()s */
    sa.sa_flags = SA_RESTART;          
    sa.sa_handler = handler;
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        printf("failed to sigactionn");
        fflush(stdout);
        exit(1);
    }

    int ready;
    while ((ready = select(nfds, &readfds, NULL, NULL, pto)) == -1 && errno == EINTR) {
        printf("select continue ready: %d, errno[%d]: %sn", ready, errno, strerror(errno));
        fflush(stdout);
        continue;
    }
    printf("select break ready: %d, errno[%d]: %sn", ready, errno, strerror(errno));

    if (ready == -1) {
        printf("failed to get ready: %dn", ready);
        fflush(stdout);
        exit(1);
    }

    /* Handler was called */
    char ch;
    if (FD_ISSET(pfd[0], &readfds)) {   
        printf("A signal was caughtn");
        fflush(stdout);

        /* Consume bytes from pipe */
        for (;;) {                      
            if (read(pfd[0], &ch, 1) == -1) {
                if (errno == EAGAIN) {
                    break;
                } else {
                    printf("failed to readn");
                    fflush(stdout);
                    exit(1);
                }
            }
            /* Perform any actions that should be taken in response to signal */
        }
    }
        

    /* Examine file descriptor sets returned by select() to see
       which other file descriptors are ready */
    /* And check if read end of pipe is ready */
    exit(0);
}

执行后kill -INT pid

返回

代码语言:javascript复制
nfds: 4
[enter] sig handler 												: 收到信号,在pipe中write一个字节
[exit] sig handler   												: 退出信号处理函数
select continue ready: -1, errno[4]: Interrupted system call 		: select被信号后唤醒,返回-1 和 eno=4;
select break ready: 1, errno[4]: Interrupted system call  			: select被pipe[0]唤醒,进入信号处理逻辑
A signal was caught

1 人点赞