除了普通文件fd, socket fd, Linux还提供了比较特殊的几种fd, 比如eventfd timerfd signalfd等等.

各种fd是系统调用 用perf看所有带fd的系统调用 perf list | grep syscalls | grep fd | grep enter

root@godev-server:/home/yingjieb# perf list | grep syscalls | grep fd | grep enter
  syscalls:sys_enter_eventfd                         [Tracepoint event]
  syscalls:sys_enter_eventfd2                        [Tracepoint event]
  syscalls:sys_enter_fdatasync                       [Tracepoint event]
  syscalls:sys_enter_gettimeofday                    [Tracepoint event]
  syscalls:sys_enter_memfd_create                    [Tracepoint event]
  syscalls:sys_enter_settimeofday                    [Tracepoint event]
  syscalls:sys_enter_signalfd                        [Tracepoint event]
  syscalls:sys_enter_signalfd4                       [Tracepoint event]
  syscalls:sys_enter_timerfd_create                  [Tracepoint event]
  syscalls:sys_enter_timerfd_gettime                 [Tracepoint event]
  syscalls:sys_enter_timerfd_settime                 [Tracepoint event]
  syscalls:sys_enter_userfaultfd                     [Tracepoint event]

下面就介绍一下这些fd的使用.

1. signalfd

#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

int
main(int argc, char *argv[])
{
    sigset_t mask;
    int sfd;
    struct signalfd_siginfo fdsi;
    ssize_t s;

    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGQUIT);

    /* 阻塞信号以使得它们不被默认的处理试方式处理 */

    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1)
        handle_error("sigprocmask");

    sfd = signalfd(-1, &mask, 0);
    if (sfd == -1)
        handle_error("signalfd");

    for (;;) {
        s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
        if (s != sizeof(struct signalfd_siginfo))
            handle_error("read");

        if (fdsi.ssi_signo == SIGINT) {
            printf("Got SIGINT\n");
        } else if (fdsi.ssi_signo == SIGQUIT) {
            printf("Got SIGQUIT\n");
            exit(EXIT_SUCCESS);
        } else {
            printf("Read unexpected signal\n");
        }
    }
}

这个例子只是很简单的说明了使用signalfd的方法,并没有真正发挥它的作用,有了这个API,就可以将信号处理作为IO看待.
每一个信号集合(或者某一个对应的信号)就会有对应的文件描述符,这样将信号处理的流程大大简化,将应用程序中的业务作为文件来操作,也体现了linux下的一切皆文件的说法,非常好,假如有很多种信号等待着处理,每一个信号描述符对待一种信号的处理,那么就可以将信号文件描述符设置为非阻塞,同时结合epoll使用,对信号的处理转化为IO复用,和这个有相似之处的API还有timerfd

2. timerfd

使用timerfd的一个例子是libevent. 在linux下面, 默认的libevent使用epoll, epoll有个超时时间.
libevent利用这个超时时间来做定时器的触发源, 即在event loop里面, 把定时器堆的时间做为超时时间.
更具体一些, 在libevent初始化时:
默认情况下, libevent不使用timerfd, epollop->timerfd = -1.
但有EVENT_BASE_FLAG_PRECISE_TIMER情况下,

if ((base->flags & EVENT_BASE_FLAG_PRECISE_TIMER) &&
     base->monotonic_timer.monotonic_clock == CLOCK_MONOTONIC) {
         int fd;
        fd = epollop->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK|TFD_CLOEXEC);
        if (epollop->timerfd >= 0) {
            struct epoll_event epev;
            memset(&epev, 0, sizeof(epev));
            epev.data.fd = epollop->timerfd;
            epev.events = EPOLLIN;
            //把这个fd加入epoll
            epoll_ctl(epollop->epfd, EPOLL_CTL_ADD, fd, &epev)

参考: https://blog.csdn.net/KangRoger/article/details/47844443

所以这里的timerfd_create()就是timerfd的使用方法.

2.1. timerfd API

       #include <sys/timerfd.h>

       int timerfd_create(int clockid, int flags);

        //这个时间精度应该比ms高.
       int timerfd_settime(int fd, int flags,
                           const struct itimerspec *new_value,
                           struct itimerspec *old_value);

       int timerfd_gettime(int fd, struct itimerspec *curr_value);

       read系统调用会返回超时次数, 如果一次超时都没到, read阻塞.
  1. timerfd_create用于创建一个定时器文件,函数返回值是一个文件句柄fd。
  2. timerfd_settime用于设置新的超时时间,并开始计时。flag为0表示相对时间,为1表示绝对时间。new_value为这次设置的新时间,old_value为上次设置的时间。返回0表示设置成功。
  3. timerfd_gettime用于获得定时器距离下次超时还剩下的时间。如果调用时定时器已经到期,并且该定时器处于循环模式(设置超时时间时struct itimerspec::it_interval不为0),那么调用此函数之后定时器重新开始计时。

2.2. 注epoll使用简介

man epoll可以看到, epoll的API有

epoll_create() : 用于创建epoll对象
epoll_ctl() : 用于管理fd set
epoll_wait() : 用于等待IO

2.2.1. 边沿触发和电平触发

epoll有类似硬件的触发概念

  • edge-triggered (ET): 边沿触发, 只有fd的状态有变化才触发. 例如epoll_wait返回一个fd可读, 它实际有2k的数据可以读, 但回调函数里只读了1k. 即使还有1k数据可读, 在ET触发模式下, epoll不会再触发fd可读. 用ET触发的推荐场景是:
    • 这个fd是非阻塞的
    • 并且read或者write返回EAGAIN
  • level-triggered (LT): 电平触发, 这是默认触发方式

2.2.2. epoll_wait

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
                int maxevents, int timeout);

这里的timeout单位是ms, 是用CLOCK_MONOTONIC度量的. epoll_wait返回有几种可能:

  • 底层driver有数据, event到达
  • 超时, -1表示永久等待. 0表示立即返回
  • 被signal

3. eventfd概念

eventfd()是个系统调用, 生成一个event fd对象, 内核为其维护一个计数器, 用来做用户进程间的wait/nofify机制, 也可以被kernel用来通知用户态进程. 可以用来代替pipe(), 相比于pipe来说,少用了一个文件描述符,而且不必管理缓冲区,单纯的事件通知的话,方便很多
fork()的时候, 子进程继承eventfd, 对应同一个eventfd对象, 并且, 这个fd在execve()后仍然保持, 但如果有close-on-exec选项则不保持.

创建eventfd以后, 可以子进程写, 父进程读;

  • 读的时候, 只要计数器非0, 就返回计数器值, 并reset到0; 计数器为0则block
  • 计数器不溢出就可以写, 写的值加到计数器上. 溢出的话会阻塞.
  • 也可以配合poll(), select()使用

3.1. 用法

#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);
    read()
    write()
    close()

//glibc还提供:
typedef uint64_t eventfd_t;
int eventfd_read(int fd, eventfd_t *value);
int eventfd_write(int fd, eventfd_t value);

4. 进程间共享文件描述符

OVS架构和代码中, qemu通过unix socket传递eventfd的文件描述符给OVS, 实际上是用了socket的SCM_RIGHTS方法. 实际上, 也可以用pipe之类的进程间通信为载体, 其底层是通过ioctl的I_SENDFDI_RECVFD完成的. 详见 http://poincare.matf.bg.ac.rs/~ivana/courses/ps/sistemi_knjige/pomocno/apue/APUE/0201433079/ch17lev1sec4.html 出自Advanced Programming in the UNIX® Environment: Second Edition 2005
看来这技术有十几年的时间了
发送进程:

#include "apue.h"
#include <stropts.h>

/*
 * Pass a file descriptor to another process.
 * If fd<0, then -fd is sent back instead as the error status.
 */
int
send_fd(int fd, int fd_to_send)
{
    char buf[2]; /* send_fd()/recv_fd() 2-byte protocol */

    buf[0] = 0; /* null byte flag to recv_fd() */
    if (fd_to_send < 0) {
        buf[1] = -fd_to_send; /* nonzero status means error */
        if (buf[1] == 0)
            buf[1] = 1; /* -256, etc. would screw up protocol */
    } else {
        buf[1] = 0; /* zero status means OK */
    }

    if (write(fd, buf, 2) != 2)
        return(-1);
    if (fd_to_send >= 0)
        if (ioctl(fd, I_SENDFD, fd_to_send) < 0)
            return(-1);
    return(0);
}

接收进程:

//接收时, 第三个参数是strrecvfd 结构体
   struct strrecvfd {
       int fd; /* new descriptor */
       uid_t uid; /* effective user ID of sender */
       gid_t gid; /* effective group ID of sender */
       char fill[8];
   };

#include "apue.h"
#include <stropts.h>

/*
 * Receive a file descriptor from another process (a server).
 * In addition, any data received from the server is passed
 * to (*userfunc)(STDERR_FILENO, buf, nbytes). We have a
 * 2-byte protocol for receiving the fd from send_fd().
 */
int
recv_fd(int fd, ssize_t (*userfunc)(int, const void *, size_t))
{
    int newfd, nread, flag, status;
    char *ptr;
    char buf[MAXLINE];
    struct strbuf dat;
    struct strrecvfd recvfd;

    status = -1;
    for ( ; ; ) {
        dat.buf = buf;
        dat.maxlen = MAXLINE;
        flag = 0;
        if (getmsg(fd, NULL, &dat, &flag) < 0)
            err_sys("getmsg error");
        nread = dat.len;
        if (nread == 0) {
            err_ret("connection closed by server");
            return(-1);
        }
        /*
         * See if this is the final data with null & status.
         * Null must be next to last byte of buffer, status
         * byte is last byte. Zero status means there must
         * be a file descriptor to receive.
         */
        for (ptr = buf; ptr < &buf[nread]; ) {
            if (*ptr++ == 0) {
                if (ptr != &buf[nread-1])
                    err_dump("message format error");
                 status = *ptr & 0xFF; /* prevent sign extension */
                 if (status == 0) {
                     if (ioctl(fd, I_RECVFD, &recvfd) < 0)
                         return(-1);
                     newfd = recvfd.fd; /* new descriptor */
                 } else {
                     newfd = -status;
                 }
                 nread -= 2;
            }
        }
        if (nread > 0)
            if ((*userfunc)(STDERR_FILENO, buf, nread) != nread)
                 return(-1);

        if (status >= 0) /* final data has arrived */
            return(newfd); /* descriptor, or -status */
    }
}

下面的例子说明, 两个进程想要传递fd, 要先有个通道, 比如pipe, 或者socket, 用来传递fd.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stropts.h>
#include <stdio.h>

#define TESTFILE "/dev/null"
main(int argc, char *argv[])
{
    int fd;
    int pipefd[2];
    struct stat statbuf;

    stat(TESTFILE, &statbuf);
    statout(TESTFILE, &statbuf);
    pipe(pipefd);
    if (fork() == 0) {
        close(pipefd[0]);
        sendfd(pipefd[1]);
    } else {
        close(pipefd[1])
        recvfd(pipefd[0]);
    }
}

sendfd(int p)
{
    int tfd;

    tfd = open(TESTFILE, O_RDWR);
    ioctl(p, I_SENDFD, tfd);
}

recvfd(int p)
{
    struct strrecvfd rfdbuf;
    struct stat statbuf;
    char    fdbuf[32];

    ioctl(p, I_RECVFD, &rfdbuf);
    fstat(rfdbuf.fd, &statbuf);
    sprintf(fdbuf, "recvfd=%d", rfdbuf.fd);
    statout(fdbuf, &statbuf); 
}

statout(char *f, struct stat *s)
{
    printf("stat: from=%s mode=0%o, ino=%ld, dev=%lx, rdev=%lx\n",
    f, s->st_mode, s->st_ino, s->st_dev, s->st_rdev);
    fflush(stdout);
}

4.1. 2023.02更新

使用unix socket传递fd, 底层调用的是:

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

struct iovec {                    /* Scatter/gather array items */
    void  *iov_base;              /* Starting address */
    size_t iov_len;               /* Number of bytes to transfer */
};

struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    size_t        msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};

使用msghdr结构体中的msg_control指针, 可以接收cmsg(control message).
cmsg的发送和接收是成对的, 我理解要在发送端有cmsg, 才能在接收端收到cmsg.

cmsg的格式如下:

#include <sys/socket.h>

struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
size_t CMSG_ALIGN(size_t length);
size_t CMSG_SPACE(size_t length);
size_t CMSG_LEN(size_t length);
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);

struct cmsghdr {
    size_t cmsg_len;    /* Data byte count, including header
                            (type is socklen_t in POSIX) */
    int    cmsg_level;  /* Originating protocol */
    int    cmsg_type;   /* Protocol-specific type */
    /* followed by unsigned char cmsg_data[]; */
};

man cmsg可以看到cmsg结构体的定义和使用示例.

results matching ""

    No results matching ""