NetworkIPC之Open Server(三)(有遗留问题)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/LoveStackover/article/details/79692599

1. 概述

  open-server这个APUE花了很大篇幅介绍的一种Server-Clinets模式, 以Clientsfork-exec一个Server,并通过给Server传递需要打开的文件信息,而Server将打开的fd传递给Client,这样做的优点有以下几点:

  • The server can easily be contacted by any client, similar to the client calling a library function. We are not hard-coding a particular service into the application, but designing a general facility that others can reuse.
  • If we need to change the server, only a single program is affected. Conversely, updating a library function can require that all programs that call the function be updated (i.e., relinked with the link editor). Shared libraries can simplify this updating.
  • The server can be a set-user-ID program, providing it with additional permissions that the client does not have. Note that a library function (or shared library function) can’t provide this capability.

2. Open-Server I

//Clinet程序
#include "open.h"
#include <fcntl.h>
#define BUFFSIZE 8192
int main(int argc, char *argv[])
{
    int n, fd;
    char buf[BUFFSIZE];
    char line[MAXLINE];
    /* read filename to cat from stdin */
    while (fgets(line, MAXLINE, stdin) != NULL) {
        if (line[strlen(line) - 1] == ’\n’)
            line[strlen(line) - 1] = 0; /* replace newline with null */
        /* open the file */
        if ((fd = csopen(line, O_RDONLY)) < 0)
            continue; /* csopen() prints error from server */
        /* and cat to stdout */
        while ((n = read(fd, buf, BUFFSIZE)) > 0)
            if (write(STDOUT_FILENO, buf, n) != n)
                err_sys("write error");
        if (n < 0)
            err_sys("read error");
        close(fd);
    }
    exit(0);
}

  fgets会保留换行符,所以要将换行符转为为\0csopen是以上思想的核心函数:

int csopen(char *name, int oflag)
{
    pid_t pid;
    int len;
    char buf[10];
    struct iovec iov[3];
    static int fd[2] = { -1, -1 };
    if (fd[0] < 0) { /* fork/exec our open server first time */
        if (fd_pipe(fd) < 0) {
            err_ret("fd_pipe error");
            return(-1);
        }
        if ((pid = fork()) < 0) {
            err_ret("fork error");
            return(-1);
        } else if (pid == 0) { /* child */
            close(fd[0]);
            if (fd[1] != STDIN_FILENO && dup2(fd[1], STDIN_FILENO) != STDIN_FILENO)
                err_sys("dup2 error to stdin");
            if (fd[1] != STDOUT_FILENO && dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)
                err_sys("dup2 error to stdout");
            if (execl("./opend", "opend", (char *)0) < 0)
                err_sys("execl error");
        }
        close(fd[1]); /* parent */
    }
    sprintf(buf, " %d", oflag); /* oflag to ascii */
    iov[0].iov_base = CL_OPEN " "; /* string concatenation */
    iov[0].iov_len = strlen(CL_OPEN) + 1;
    iov[1].iov_base = name;
    iov[1].iov_len = strlen(name);
    iov[2].iov_base = buf;
    //sprintf并不输出null
    iov[2].iov_len = strlen(buf) + 1; /* +1 for null at end of buf */
    len = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len;
    if (writev(fd[0], &iov[0], 3) != len) {
        err_ret("writev error");
        return(-1);
    }
    /* read descriptor, returned errors handled by write() */
    return(recv_fd(fd[0], write));
}

  opend子程序的标准输出输入重导向到fd[1],Clients将打开文件的信息传递给子进程,并且等待opend子进程将打开的文件描述符回传给Clients。 opend的程序为:

#include "opend.h"
char errmsg[MAXLINE];
int oflag;
char *pathname;
int main(void)
{
    int nread;
    char buf[MAXLINE];
    for ( ; ; ) { /* read arg buffer from client, process request */
        if ((nread = read(STDIN_FILENO, buf, MAXLINE)) < 0)
            err_sys("read error on stream pipe");
        else if (nread == 0)
            break; /* client has closed the stream pipe */
        handle_request(buf, nread, STDOUT_FILENO);
    }
    exit(0);
}

  read要打开的文件信息读取到buf中,并传递给handle_request:


void handle_request(char *buf, int nread, int fd)
{
    int newfd;
    if (buf[nread-1] != 0) {
        //注意%*.*s
        snprintf(errmsg, MAXLINE-1, "request not null terminated: %*.*s\n", nread, nread, buf);
        send_err(fd, -1, errmsg);
        return;
    }
    if (buf_args(buf, cli_args) < 0) { /* parse args & set options */
        send_err(fd, -1, errmsg);
        return;
    }
    if ((newfd = open(pathname, oflag)) < 0) {
        snprintf(errmsg, MAXLINE-1, "can’t open %s: %s\n", pathname, strerror(errno));
        send_err(fd, -1, errmsg);
        return;
    }
    if (send_fd(fd, newfd) < 0) /* send the descriptor */
        err_sys("send_fd error");
    close(newfd); /* we’re done with descriptor */
}

  注意%*.*ssend_fd之后直接关闭newfd,这里需要看看close的准确定义:

close() closes a file descriptor, so that it no longer refers to any file and may be reused. Any record locks (see fcntl(2)) held on the file it was associated with, and owned by the process, are removed. if the descriptor was the last reference to a file which has been removed using unlink(2) the file is deleted.

  传递过去的newfd,直接在Client程序中打印,其值并没有改变,和fork时候,子进程继承父进程的open fd具有很相似的地方。

#include "apue.h"
#define MAXARGC 50 /* max number of arguments in buf */
#define WHITE " \t\n" /* white space for tokenizing arguments */
/*
* buf[] contains white-space-separated arguments. We convert it to an
* argv-style array of pointers, and call the user’s function (optfunc)
* to process the array. We return -1 if there’s a problem parsing buf,
* else we return whatever optfunc() returns. Note that user’s buf[]
* array is modified (nulls placed after each token).
*/
int buf_args(char *buf, int (*optfunc)(int, char **))
{
    char *ptr, *argv[MAXARGC];
    int argc;
    if (strtok(buf, WHITE) == NULL) /* an argv[0] is required */
        return(-1);
    argv[argc = 0] = buf;
    while ((ptr = strtok(NULL, WHITE)) != NULL) {
        if (++argc >= MAXARGC-1) /* -1 for room for NULL at end */
        return(-1);
        argv[argc] = ptr;
    }
    argv[++argc] = NULL;
    /*
    * Since argv[] pointers point into the user’s buf[],
    * user’s function can just copy the pointers, even
    * though argv[] array will disappear on return.
    */
    return((*optfunc)(argc, argv));
}

  strtok处理字符串的方式会找到分隔符WHITE,并其替换为\0,具体可以看MAN手册。

#include "opend.h"
/*
* This function is called by buf_args(), which is called by
* handle_request(). buf_args() has broken up the client’s
* buffer into an argv[]-style array, which we now process.
*/
int cli_args(int argc, char **argv)
{
    if (argc != 3 || strcmp(argv[0], CL_OPEN) != 0) {
        strcpy(errmsg, "usage: <pathname> <oflag>\n");
        return(-1);
    }
    pathname = argv[1]; /* save ptr to pathname to open */
    oflag = atoi(argv[2]);
    return(0);
}    

  需要注意的是,argvbuf_args函数结束时,会自动被释放。所以会在cli_args对其进行保存,这里是赋值给全局变量。

三:Open-Server II

  上面的每个Client都具有一个Open-Server,这样做非常浪费系统资源。有一种方式通过指定一个Daemon Server进程来处理所有的Client请求。但是这种不相关的Client和Server的通信必须通过进程间IPC,这里选择使用UNIX domain socket这种形式的IPC。

//#define CS_OPEN "/tmp/opend.socket"
#include "open.h"
#include <sys/uio.h> /* struct iovec */
/*
* Open the file by sending the "name" and "oflag" to the
* connection server and reading a file descriptor back.
*/
int csopen(char *name, int oflag)
{
    int len;
    char buf[12];
    struct iovec iov[3];
    static int csfd = -1;
    if (csfd < 0) { /* open connection to conn server */
        //cli_conn返回一个建立连接的csfd,其是否阻塞取决于CS_OPEN
        if ((csfd = cli_conn(CS_OPEN)) < 0) {
            err_ret("cli_conn error");
            return(-1);
        }
    }
    sprintf(buf, " %d", oflag);
    //其他与上面一致
    ...
}
#include "opend.h"
#define NALLOC 10 /* # client structs to alloc/realloc for */
static void client_alloc(void) /* alloc more entries in the client[] array */
{
    int i;
    if (client == NULL)
        //保证malloc只调用一次
        client = malloc(NALLOC * sizeof(Client));
    else
        //之后再次调用client_alloc的时候都会增加10个Client structure的空间
        client = realloc(client, (client_size+NALLOC)*sizeof(Client));
    if (client == NULL)
        err_sys("can’t alloc for client array");
    //将新的地址空间初始化
    for (i = client_size; i < client_size + NALLOC; i++)
        client[i].fd = -1; /* fd of -1 means entry available */
    client_size += NALLOC;
}
/*
* Called by loop() when connection request from a new client arrives.
*/
int client_add(int fd, uid_t uid)
{
    int i;
    if (client == NULL) /* first time we’re called */
        client_alloc();
    again:
        //搜寻当前未被使用的成员
        for (i = 0; i < client_size; i++) {
            if (client[i].fd == -1) { /* find an available entry */
                client[i].fd = fd;
                client[i].uid = uid;
                return(i); /* return index in client[] array */
            }
        }
    //如果全部占用则再次分配
    /* client array full, time to realloc for more */
    client_alloc();
    goto again; /* and search again (will work this time) */
}
/*
* Called by loop() when we’re done with a client.
*/
void client_del(int fd)
{
    int i;
    for (i = 0; i < client_size; i++) {
        if (client[i].fd == fd) {
        client[i].fd = -1;
        return;
        }
    }
    log_quit("can’t find client entry for fd %d", fd);
}

  如果想通过命令option的方式决定一个server是否以daemon运行,那么必须对输入命令做一定的处理,一般options遵循下面的准则:

They include such suggestions as ‘‘Restrict each command-line option to a single alphanumeric character’’ and ‘‘All options should be preceded by a − character.’’

#include <unistd.h>
int getopt(int argc, char * const argv[], const char *options); extern int optind, opterr, optopt;
extern char *optarg;
Returns: the next option character, or −1 when all options have been processed

  argcargv参数和传入main函数的一致,options参数包含该进程支持的所有options,如果后面紧跟:,则该option具有参数,一个例子如下:

For example, if the usage statement for a command was
   command [-i] [-u username] [-z] filename
we would pass “iu:z” as the options string to getopt.

  如果遇到无效option或者一个option缺少参数,getopt返回?,如果遇到--,则该函数返回-1,getopt需要几个额外的全局变量支持:

NAME DESCRIPTOR
optarg If an option takes an argument, getopt sets optarg to point to the option’s argument string when an option is processed.
opterr If an option error is encountered, getopt will print an error message by default. To disable this behavior, applications can set opterr to 0.
optind The index in the argv array of the next string to be processed. It starts at 1 and is incremented for each argument processed by getopt.
optopt If an error is encountered during options processing, getopt will set optopt to point to the option string that caused the error.
#include "opend.h"
#include <syslog.h>
int debug, oflag, client_size, log_to_stderr;
char errmsg[MAXLINE];
char *pathname;
Client *client = NULL;
int main(int argc, char *argv[])
{
    int c;
    log_open("open.serv", LOG_PID, LOG_USER);
    opterr = 0; /* don’t want getopt() writing to stderr */
    while ((c = getopt(argc, argv, "d")) != EOF) {
        switch (c) {
        case ’d’: /* debug */
            debug = log_to_stderr = 1;
            break;
        case ’?’:
            err_quit("unrecognized option: -%c", optopt);
        }
    }
    if (debug == 0)
        daemonize("opend");
    loop(); /* never returns */
}

  这个opterr错误的情况还是有点不知所云,以后再寻找答案。再看看loop函数:

#include "opend.h"
#include <sys/select.h>
void loop(void)
{
    int i, n, maxfd, maxi, listenfd, clifd, nread;
    char buf[MAXLINE];
    uid_t uid;
    fd_set rset, allset;
    FD_ZERO(&allset);
    /* obtain fd to listen for client requests on */
    if ((listenfd = serv_listen(CS_OPEN)) < 0)
        log_sys("serv_listen error");
    FD_SET(listenfd, &allset);
    maxfd = listenfd;
    maxi = -1;
    for ( ; ; ) {
        rset = allset; /* rset gets modified each time around */
        //select每次等待rset中的fd就绪
        if ((n = select(maxfd + 1, &rset, NULL, NULL, NULL)) < 0)
            log_sys("select error");
        //判断是否listenfd在该就绪的rset中
        if (FD_ISSET(listenfd, &rset)) {
            /* accept new client request */
            if ((clifd = serv_accept(listenfd, &uid)) < 0)
                log_sys("serv_accept error: %d", clifd);
            //建立连接之后,将uid和clifd存储到client数组中,并返回该对于的序号
            i = client_add(clifd, uid);
            //将clifd加入allset中,下面几处的处理都比较巧妙
            FD_SET(clifd, &allset);
            //为select更新maxfd的值
            if (clifd > maxfd)
                maxfd = clifd; /* max fd for select() */
            //更新maxi的值,并记录在log中
            if (i > maxi)
                maxi = i; /* max index in client[] array */
                log_msg("new connection: uid %d, fd %d", uid, clifd);
                continue;
        }
        //如果此时是某个client数据可读,则进展到这里
        for (i = 0; i <= maxi; i++) { /* go through client[] array */
            if ((clifd = client[i].fd) < 0)
                continue;
            //遍历client数组判断其成员是否读就绪
            if (FD_ISSET(clifd, &rset)) {
            /* read argument buffer from client */
                if ((nread = read(clifd, buf, MAXLINE)) < 0) {
                    log_sys("read error on fd %d", clifd);
                } else if (nread == 0) {
                    log_msg("closed: uid %d, fd %d", client[i].uid, clifd);
                    //这里client相应进程结束,从client数组中删除,从allset中删除
                    client_del(clifd); /* client has closed cxn */
                    FD_CLR(clifd, &allset);
                    //关闭对于的server 建立的socket fd
                    close(clifd);
                } else { /* process client’s request */
                    handle_request(buf, nread, clifd, client[i].uid);
                }
            }
        }
    }
} 

  maxi = i;前面是需要if判断的需要注意,因为有client_del的存在,每次建立的新连接并不一定意味着分配更大的数组。到现在我对于close(fd)unlink(fd)对socket的影响是否和普通文件一致,还是不敢确定。一些设计巧妙的地方如下:

We keep track of which descriptors are currently in use in the allset descriptor set. As new clients connect to the server, the appropriate bit is turned on in this descriptor set. The appropriate bit is turned off when the client terminates. We always know when a client terminates, whether the termination is voluntary or not, since all the client’s descriptors (including the connection to the server) are automatically closed by the kernel. This differs from the XSI IPC mechanisms.

  使用poll机制的版本:

#include "opend.h"
#include <poll.h>
#define NALLOC 10 /* # pollfd structs to alloc/realloc */
static struct pollfd * grow_pollfd(struct pollfd *pfd, int *maxfd)
{
    int i;
    int oldmax = *maxfd;
    int newmax = oldmax + NALLOC;
    if ((pfd = realloc(pfd, newmax * sizeof(struct pollfd))) == NULL)
        err_sys("realloc error");
    //初始化新分配的数组成员,因为只关心读,所以events设为POLLIN
    for (i = oldmax; i < newmax; i++) {
        pfd[i].fd = -1;
        pfd[i].events = POLLIN;
        pfd[i].revents = 0;
    }
    *maxfd = newmax;
    return(pfd);
}
void loop(void)
{
    int i, listenfd, clifd, nread;
    char buf[MAXLINE];
    uid_t uid;
    struct pollfd *pollfd;
    int numfd = 1;
    int maxfd = NALLOC;
    //首次调用分配初始pollfd数组,并初始化相应的成员
    if ((pollfd = malloc(NALLOC * sizeof(struct pollfd))) == NULL)
        err_sys("malloc error");
    for (i = 0; i < NALLOC; i++) {
        pollfd[i].fd = -1;
        pollfd[i].events = POLLIN;
        pollfd[i].revents = 0;
    }
    /* obtain fd to listen for client requests on */
    if ((listenfd = serv_listen(CS_OPEN)) < 0)
        log_sys("serv_listen error");
    //listenfd的client UID规定为0
    client_add(listenfd, 0); /* we use [0] for listenfd */
    pollfd[0].fd = listenfd;
    for ( ; ; ) {
        //poll阻塞等待pollfd中的fd就绪
        if (poll(pollfd, numfd, -1) < 0)
            log_sys("poll error");
        //判断是否listenfd就绪
        if (pollfd[0].revents & POLLIN) {
            /* accept new client request */
            if ((clifd = serv_accept(listenfd, &uid)) < 0)
                log_sys("serv_accept error: %d", clifd);
            client_add(clifd, uid);
            /* possibly increase the size of the pollfd array */
            if (numfd == maxfd)
                pollfd = grow_pollfd(pollfd, &maxfd);
            pollfd[numfd].fd = clifd;
            pollfd[numfd].events = POLLIN;
            pollfd[numfd].revents = 0;
            numfd++;
            log_msg("new connection: uid %d, fd %d", uid, clifd);
        }
        //如果是clients有数据可读时
        for (i = 1; i < numfd; i++) {
            if (pollfd[i].revents & POLLHUP) {
                goto hungup;
            } else if (pollfd[i].revents & POLLIN) {
                /* read argument buffer from client */
                if ((nread = read(pollfd[i].fd, buf, MAXLINE)) < 0) {
                    log_sys("read error on fd %d", pollfd[i].fd);
                } else if (nread == 0) {
                    hungup:
                        /* the client closed the connection */
                        log_msg("closed: uid %d, fd %d", client[i].uid, pollfd[i].fd);
                        client_del(pollfd[i].fd);
                        close(pollfd[i].fd);
                        if (i < (numfd-1)) {
                            /* pack the array */
                            pollfd[i].fd = pollfd[numfd-1].fd;
                            pollfd[i].events = pollfd[numfd-1].events;
                            pollfd[i].revents = pollfd[numfd-1].revents;
                            //一切都好理解,就是这里i--会让for循环重新检查该条目,因为此时该条目为未被处理的fd
                            i--; /* recheck this entry */
                        }
                    numfd--;
                } else { /* process client’s request */
                    handle_request(buf, nread, pollfd[i].fd, client[i].uid);
                }
            }
        }
    }
}

  poll版本中if (pollfd[i].revents & POLLHUP)如果没有这句的话,程序也可以正常运行,考虑下POLLHUP标志置位的条件是:

POLLHUP is not turned on in revents. If we’re reading from a modem and the telephone line is hung up, we’ll receive the POLLHUP notification.

  笔者认为针对UNIX DOMAIN SOCKET这里的情况,该语句可以直接省略。当然如果不正确请及时指正。当close(pollfd[i].fd);调用时,在该端口的未被读取的数据就被丢弃了。

#include "opend.h"
#include <fcntl.h>
void handle_request(char *buf, int nread, int clifd, uid_t uid)
{
    int newfd;
    if (buf[nread-1] != 0) {
        snprintf(errmsg, MAXLINE-1, "request from uid %d not null terminated: %*.*s\n", uid, nread, nread, buf);
        send_err(clifd, -1, errmsg);
        return;
    }
    log_msg("request: %s, from uid %d", buf, uid);
    /* parse the arguments, set options */
    if (buf_args(buf, cli_args) < 0) {
        send_err(clifd, -1, errmsg);
        log_msg(errmsg);
        return;
    }
    if ((newfd = open(pathname, oflag)) < 0) {
        snprintf(errmsg, MAXLINE-1, "can’t open %s: %s\n", pathname, strerror(errno));
        send_err(clifd, -1, errmsg);
        log_msg(errmsg);
        return;
    }
    /* send the descriptor */
    if (send_fd(clifd, newfd) < 0)
        log_sys("send_fd error");
    log_msg("sent fd %d over fd %d for %s", newfd, clifd, pathname);
    close(newfd); /* we’re done with descriptor */
}

  最后,笔者还是回顾下close相关的知识,当一个文件被关闭的时候,由内核检查此时打开该文件的进程数,如果该数为0,再检查文件所对应的inode link数,如果该值为0,则删除文件。打开一个文件,其文件对应的count link加1。所以一种程序自动删除临时文件的方式就是,新建文件之后立即调用unlink,则当程序结束的时候,该文件被自动删除,对于socket文件同样适用。

4. 实验部分

题目一

We chose to use UNIX domain datagram sockets in Figure 17.3, because they retain message boundaries. Describe the changes that would be necessary to use regular pipes instead. How can we avoid copying the messages two extra times?

题目二

Write the following program using the file descriptor passing functions from this chapter and the parent–child synchronization routines from Section 8.9. The program calls fork, and the child opens an existing file and passes the open descriptor to the parent. The child then positions the file using lseek and notifies the parent. The parent reads the file’s current offset and prints it for verification. If the file was passed from the child to the parent as we described, they should be sharing the same file table entry, so each time the
child changes the file’s current offset, that change should also affect the parent’s descriptor. Have the child position the file to a different offset and notify the parent again.

题目三

In Figures 17.20 and 17.21, we differentiated between declaring and defining the global variables. What is the difference?

题目四

In the serv_listen function (Figure 17.8), we unlink the name of the file representing the UNIX domain socket if the file already exists. To avoid unintentionally removing a file that isn’t a socket, we could call stat first to verify the file type. Explain the two problems with this approach.

题目五

Describe two possible ways to pass more than one file descriptor with a single call to sendmsg.

阅读更多 登录后自动展开
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页