像systemd这样的不相关进程之间如何共享套接字?

3
有多个问题和答案,关于如何做到这一点,但两个进程必须合作。

等等。

systemd中,有一个名为socket activation的功能,您只需在自己的进程中打开和准备文件描述符,而无需任何合作。 您只需要使用文件描述符3SD_LISTEN_FDS_START),它已经被systemd激活为套接字。

systemd是如何做到这一点的呢? 我找不到任何相关的源代码。

编辑:

我知道如何编写systemd socket激活服务,但我对从systemd的角度将文件描述符传递到我的服务的过程感兴趣。

例如,如果我想编写自己的套接字激活器,并像systemd一样运行。

1个回答

2

systemd与共享套接字的进程并非没有关系。它启动并监控整个系统,因此在exec()期间可以轻松传递套接字文件描述符。systemd代表服务监听,每当有连接时,将生成相应服务的实例。这里是其实现:链接

int main(int argc, char **argv, char **envp) {
        int r, n;
        int epoll_fd = -1; 

        log_parse_environment();
        log_open();

        r = parse_argv(argc, argv);
        if (r <= 0)
                return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE;

        r = install_chld_handler();
        if (r < 0)
                return EXIT_FAILURE;

        n = open_sockets(&epoll_fd, arg_accept);
        if (n < 0)
                return EXIT_FAILURE;
        if (n == 0) {
                log_error("No sockets to listen on specified or passed in.");
                return EXIT_FAILURE;
        }

        for (;;) {
                struct epoll_event event;

                r = epoll_wait(epoll_fd, &event, 1, -1);
                if (r < 0) {
                        if (errno == EINTR)
                                continue;

                        log_error_errno(errno, "epoll_wait() failed: %m");
                        return EXIT_FAILURE;
                }

                log_info("Communication attempt on fd %i.", event.data.fd);
                if (arg_accept) {
                        r = do_accept(argv[optind], argv + optind, envp, event.data.fd);
                        if (r < 0)
                                return EXIT_FAILURE;
                } else
                        break;
        }
        ...
}

一旦有连接进来,它将调用do_accept()

static int do_accept(const char* name, char **argv, char **envp, int fd) {
        _cleanup_free_ char *local = NULL, *peer = NULL;
        _cleanup_close_ int fd_accepted = -1; 

        fd_accepted = accept4(fd, NULL, NULL, 0); 
        if (fd_accepted < 0)
                return log_error_errno(errno, "Failed to accept connection on fd:%d: %m", fd);

        getsockname_pretty(fd_accepted, &local);
        getpeername_pretty(fd_accepted, true, &peer);
        log_info("Connection from %s to %s", strna(peer), strna(local));

        return fork_and_exec_process(name, argv, envp, fd_accepted);
}

最后,调用execvpe(name, argv, envp);并将fd包装在envp中。其中有一个技巧,在fd_accepted不等于SD_LISTEN_FDS_START时,它会调用dup2()使SD_LISTEN_FDS_START成为fd_accepted的副本:

    if (start_fd != SD_LISTEN_FDS_START) {
            assert(n_fds == 1);

            r = dup2(start_fd, SD_LISTEN_FDS_START);
            if (r < 0)
                    return log_error_errno(errno, "Failed to dup connection: %m");

            safe_close(start_fd);
            start_fd = SD_LISTEN_FDS_START;
    }

因此,在您的应用程序中,您可以像这样使用文件描述符3,sd_listen_fds将解析从envp传递的环境变量LISTEN_FDS

int listen_sock;
int fd_count = sd_listen_fds(0);
if (fd_count == 1) { // assume one socket only
  listen_sock = SD_LISTEN_FDS_START; // SD_LISTEN_FDS_START is a macro defined to 3
} else {
  // error
}
struct sockaddr addr;
socklen_t addrlen;
while (int client_sock = accept(listen_sock, &addr, &addrlen)) {
  // do something
}

我明白这一点,但我感兴趣的是在exec()期间传递文件描述符的过程,而不需要被执行进程的合作。 - j123b567
谢谢你详细的回答。我需要稍微研究一下。对我来说,进程之间仍然是无关的,但有两个重要的诀窍。默认情况下,在execve()调用后文件描述符保持打开状态,这对我来说是新鲜事。第二个诀窍是使用dup2()函数进行最后的文件描述符迁移。 - j123b567
请注意,在open_sockets()中设置了close-on-exec标志,但是在dup2()之后,这两个文件描述符不共享文件描述符标志(即close-on-exec标志),因此fd_accepted的close-on-exec标志关闭。 - jfly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接