Search code examples
linuxsocketssharefile-descriptorsystemd

Share socket between unrelated processes like systemd


There are multiple questions and answers how to do it, but both processes must cooperate

etc.

In systemd, there is feature socket activation, you just have opened and prepared file descriptotr in your process without any cooperation. You can just use file descriptor 3 (SD_LISTEN_FDS_START) and it is already activated socket by systemd.

How does systemd do this? I can't find any relevant source code.

Edit:

I know, how to write systemd socket activated service, but I'm interested in the process of passing file descriptor to my service form the systemd point of view.

E.g. if I would like to write my own socket activator, that behaves exactly as systemd.


Solution

  • systemd is not unrelated to the processes who share the sockets. systemd starts up and supervises the entire system, so it can pass the socket file descriptors during exec() easily. systemd listens on behalf of the services and whenever a connection would come in, an instance of the respective service would be spawned. Here is the implementation:

    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;
            }
            ...
    }
    

    Once a connection comes in, it will call 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);
    }
    

    finally, it calls execvpe(name, argv, envp); and wrap the fd up in envp. There is a trick in it, if fd_accepted is not equal to SD_LISTEN_FDS_START, it call dup2() to makes SD_LISTEN_FDS_START be the copy of 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;
        }
    

    So you can just use file descriptor 3 like this in your application, sd_listen_fds will parse the environment variable LISTEN_FDS passed from envp:

    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
    }