Analysis of loopback service model in the first six chapters of UNP

This paper tries to analyze the retroreflective service model in UNP up to Chapter 6, focusing on why and how the code does it. The following codes correspond to 5-12 tcpserv04 in the book c,6-13 strcliselect02.c.

Server code:

#include <unp.h>

void sig_chld(int signo) {
    pid_t pid;
    int stat;
    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
        printf("child %d terminated\n", pid);
    }
    return;
}

void str_echo(int sockfd)
{
    ssize_t n;
    char buf[MAXLINE];
again:
    while ( (n = read(sockfd, buf, MAXLINE)) > 0)
        Writen(sockfd, buf, n);
    if (n < 0 && errno == EINTR)
        goto again;
    else if (n < 0)
        err_sys("str_echo: read error");
}

int main(int argc, char **argv)
{
    int listenfd, connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);

    signal(SIGCHLD, sig_chld);
    for (;;)
    {
        clilen = sizeof(cliaddr);
        if ((connfd = Accept(listenfd, (SA *)&cliaddr, &clilen)) < 0)
        {
            if (errno == EINTR)
                continue;
            else
                err_sys("accept error");
        }
        // The subprocess exit will lead to the arrival of the signal sigcld, which will cause sig_ Asynchronous execution of chld, when from sig_ When chld() returns, the accept system call is interrupted
        // bsd4.4 will automatically restart the accept system call, otherwise a
        if ((childpid = Fork()) == 0)
        {
            Close(listenfd);
            str_echo(connfd);
            exit(0);
        }
        Close(connfd);
    }
}

Client code:

#include <unp.h>

void str_cli(FILE *fp, int sockfd)
{
    int maxfdp1, stdineof = 0;
    fd_set rset;
    char buf[MAXLINE];
    int n;

    FD_ZERO(&rset);
    for (;;)
    {
        if (!stdineof)
            FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        maxfdp1 = max(fileno(fp), sockfd) + 1;
        
        Select(maxfdp1, &rset, NULL, NULL, NULL); 
        if (FD_ISSET(sockfd, &rset))
        {
            if ( (n = Read(sockfd, buf, MAXLINE)) == 0)
            {
                if (stdineof == 1)
                    return;
                else
                    err_quit("str_cli: server terminated prematurely");
            }
            Write(fileno(stdout), buf, n);
        }
        if (FD_ISSET(fileno(fp), &rset))
        {
            if ((n = read(fileno(fp), buf, MAXLINE)) == 0) {
                stdineof = 1;
                Shutdown(sockfd, SHUT_WR);
                FD_CLR(fileno(fp), &rset);
                continue;
            }
            Writen(sockfd, buf, n);
        }
    }
}

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;

    if (argc != 2)
        err_quit("usage: tcpcli <IPaddress>");
    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    Connect(sockfd, &servaddr, sizeof(servaddr));
    str_cli(stdin, sockfd);
    exit(0);
}

Operation logic

The server

  1. Create a listening socket to listen to the serv at any address of the host_ Port (9877), set the handler for SIGCHLD.
  2. Call blocking accept and wait for the arrival of the completed connection (the three handshakes of TCP connection are completed in the kernel).
  3. Once the server gets a TCP connection, it calls fork() to send the child process, while closing the monitor socket in the child process, and then calling str_. Echo() function.
  4. In str_ In the echo () function, loop blocking read and echo back to the client until FIN is received to end the child process.

client

  1. Create an active socket to the serv of the specified IP address_ The port initiates a connect connection.
  2. Run str_cli() function and set FD at the same time_ The initial value of set is the input file descriptor fileno(fp) and socket sockfd. Call blocking select to listen for both.
  3. sockfd corresponds to TCP connection, which becomes readable when the server returns data; fileno(fp) corresponds to standard input and becomes readable when entered by the user. When either of them becomes readable, the corresponding FD will be set by select_ Set and return.
  4. if determines which fd triggered the select return, and then reads the TCP connection data or reads the user input and sends the TCP data.

main points

The server

  1. The server must capture the termination signal of the child process (line 41) and call the non blocking waitpid in a loop (line 6). If the termination signal of the child process is not captured, the data of the child process will remain in memory and occupy system resources (unless the parent process exits). In addition, because multiple sub processes may end at the same time, the server process will receive multiple consecutive sigchlds, and UNIX signals are not queued by default, so it is possible to trigger sig only once_ Chld () call, we need to sig this time_ The chld() call reclaims all the child processes that have ended (the child processes that have not ended do not need to be considered, because their termination will cause a new SIGCHLD signal and the next sig_chld() call). Waitpid (- 1, & stat, wnohang) will wait for any terminated child process. If there is no terminated child process to be recycled, it will immediately return 0, which realizes a sig_ The chld () call collects all child processes to be recycled.

  2. **The server needs to deal with the interruption of the full system calls accept (line 47) and read (line 19)** Because when a process blocking a slow system call captures a signal, when the signal processing function returns, the system call is interrupted and returns an EINTR error (although the kernel of some systems can automatically restart the interrupted system call). So we need to restart this system call when we recognize that errno is EINTR.

  3. **After forking (), the server needs to close the connected socket of the parent process (line 60) and the listening socket of the child process (line 56)** Because after fork(), the file descriptor of the file parent process will be copied to the child process, and we need to know that each file or socket has a reference count. Only when the count reaches 0, the relevant resources will be cleaned up. If we don't close the socket that this process doesn't use in time, the cleaning work can't be carried out in time after another process closes the socket (it won't be cleaned until this process automatically closes all sockets when it exits).

client

  1. **The client must poll sockfd and fileno(fp) using select (line 18)** If the user input is blocked by TCP - > read, the data will be sent back to the user's input (that is, if the user input is blocked by TCP - > read), and then the data will be sent back to the user's input. Then, when the server terminates abnormally, the server sends FIN and is accepted by the client, then the client is not allowed to read or write sockfd next, but the problem is that the client process is often blocked waiting for user input. Only after the user inputs, the client writes and sends data (the server will return rst, but the time reason is that the client process will execute read() before receiving RST), When you try to call the read() function again, you will get the EOF error, which is a delayed error notification with poor user experience. Instead, after polling with select, the customer will be notified of FIN immediately before the end of user input and exit on line 26.

  2. **The client must use the stdineof flag to mark the end of user input (lines 13, 23, 33) and exit the process only after reading data from sockfd (line 21)** First of all, the end of user input does not immediately exit the program, because in mass input (especially from file input), the client has to wait for the server to echo after sending data, and then output it to the display. If you exit the program before echo, the echo data will be lost. Ironically, this does not happen in a non select linear model, where the client process must wait until write - > read before exiting.

  3. **Instead of the FD client, use the FD client instead of the FD client** The main reason is that close starts waving four times only when the reference count of sockfd is reduced to 0, and shutdown immediately sends FIN to the server. Another reason is that shutdown can only close the write side or the read side. After calling close, the read and write ends will be closed, that is, we can't read and write sockfd. What we need is to close the write half, so we use shutdown.

Added by markyoung1984 on Thu, 10 Feb 2022 00:27:22 +0200