UNIX环境高级编程 学习笔记 第十七章 高级进程间通信

UNIX域套接字可在同一计算机上运行的两个进程间传送打开文件描述符。服务器进程可以将文件描述符和一个名字关联,客户进程可用这个名字与服务器进程通信。

UNIX域套接字比因特网域套接字效率更高,UNIX域套接字仅仅复制数据,不执行任何协议处理。

UNIX域套接字提供字节流接口和数据报接口,UNIX域数据报服务是可靠的,不会丢失报文或传递失序。可用socketpair函数创建一对无命名的、相互连接的UNIX域套接字:
在这里插入图片描述
虽然函数有domain参数可指定域,但一般操作系统只对UNIX域提供支持。

一对相互连接的UNIX域套接字起到全双工管道的作用,将其称为fd管道,以便与普通的半双工管道区分开来:
在这里插入图片描述
创建UNIX域流套接字的函数:

#include <sys/socket.h>

// Returns a full-duplex pipe (a UNIX domain socket) with
// the two file descriptors returned in fd[0] and fd[1].
int fd_pipe(int fd[2]) {
    return socketpair(AF_UNIX, SOCK_STREAM, 0, fd);
}

某些基于BSD的系统使用UNIX域套接字来实现管道,但当调用pipe时,第一描述符的写端和第二描述符的读端都是关闭的,为得到全双工管道,必须调用socketpair。

我们曾提到XSI消息队列的一个问题,它们不能和poll或select函数一起使用,因为它们不能关联到文件描述符。但套接字是与文件描述符关联的,我们可以用套接字通知我们有消息到来。具体做法为,我们对每个消息队列使用一个线程,每个线程都阻塞在msgrcv调用处,当消息到达时,线程接收消息并将消息写入一个UNIX域套接字的一端,应用使用poll函数指示套接字的另一端是否可读,如可读,使用套接字在另一端接收消息:

#include <poll.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/msg.h>
#include <sys/socket.h>

#define NQ 3    // number of queues
#define MAXMSZ 512    // maximum message size
#define KEY 0x123    // key for first message queue

struct threadinfo {
    int qid;
    int fd;
};

struct mymesg {
    long mtype;
    char mtext[MAXMSZ];
};

void *helper(void *arg) {
    int n;
    struct mymesg m;
    struct threadinfo *tip = arg;

    for (; ; ) {
        memset(&m, 0, sizeof(m));
		if ((n =  msgrcv(tip->qid, &m, MAXMSZ, 0, MSG_NOERROR)) < 0) {
		    printf("msgrcv error\n");
		    exit(1);
		}
		if (write(tip->fd, m.mtext, n) < 0) {
		    printf("write error\n");
		    exit(1);
		}
    }
}

int main() {
    int i, n, err;
    int fd[2];
    int qid[NQ];
    struct pollfd pfd[NQ];
    struct threadinfo ti[NQ];
    pthread_t tid[NQ];
    char buf[MAXMSZ];

    for (i = 0; i < NQ; ++i) {
        if ((qid[i] = msgget((KEY + i), IPC_CREAT | 0666)) < 0) {
		    printf("msgget error\n");
		    exit(1);
		}
	
		printf("queue ID %d is %d\n", i, qid[i]);
	
		if (socketpair(AF_UNIX, SOCK_DGRAM, 0, fd) < 0) {
		    printf("socketpair error\n");
		    exit(1);
		}
	
		pfd[i].fd = fd[0];
		pfd[i].events = POLLIN;
		
		ti[i].qid = qid[i];
		ti[i].fd = fd[1];
	
		if ((err = pthread_create(&tid[i], NULL, helper, &ti[i])) != 0) {
		    printf("pthread_create error\n");
		    exit(1);
		}
    }

    for (; ; ) {
        // 第三个参数表示等待的ms数,-1表示永远等待,直到有事件发生
        if (poll(pfd, NQ, -1) < 0) {
		    printf("poll error\n");
		    exit(1);
		}
	
		for (i = 0; i < NQ; ++i) {
		    if (pfd[i].revents & POLLIN) {
		        if ((n = read(pfd[i].fd, buf, sizeof(buf))) < 0) {
				    printf("read error\n");
					exit(1);
				}
				buf[n] = 0;
				printf("queue id %d, message %s\n", qid[i], buf);
			}
	    }
    }

    exit(0);
}

以上程序中使用的是数据报(SOCK_DGRAM)而非流套接字,这样可以保持消息边界,保证从套接字里一次只读取一条消息。以上过程的额外开销在于为每个队列分配一个线程以及每个消息额外复制两次(一次写入套接字,一次从套接字中读出来),开销可接受。

向以上程序发送消息的程序:

#include <sys/msg.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#define MAXMSZ 512

struct mymesg {
    long mtype;
    char mtext[MAXMSZ];
};

int main(int argc, char *argv[]) {
    key_t key;
    long qid;
    size_t nbytes;
    struct mymesg m;

    if (argc != 3) {
        fprintf(stderr, "usage: sendmsg KEY message\n");
		exit(1);
    }

    // 将argv[1]从字符串转换为长整型
    // 第二个参数是argv[1]中数值部分的下一个字符的指针的地址,用于修改该指针本身的值
    // 第三个参数是argv[1]中数字是几进制的,0表示根据字符串前缀来判断进制(如0x就是16进制)
    key = strtol(argv[1], NULL, 0);
    if ((qid = msgget(key, 0)) < 0) {
        printf("can't open queue key %s\n", argv[1]);
		exit(1);
    }
    memset(&m, 0, sizeof(m));
    strncpy(m.mtext, argv[2], MAXMSZ - 1);
    nbytes = strlen(m.mtext);
    m.mtype = 1;
    if (msgsnd(qid, &m, nbytes, 0) < 0) {
        printf("can't send message\n");
		exit(1);
    }
    exit(0);
}

首先运行接收消息的程序:
在这里插入图片描述
之后向该程序发送消息:
在这里插入图片描述
接收消息的程序即可收到消息:
在这里插入图片描述
如果以上接收消息的程序不使用UNIX域数据报套接字,而是使用常规的管道实现,由于常规管道提供的是字节流接口,为了确定消息边界,我们需要给每个消息增加一个头部来指示长度。但这仍涉及两次额外的复制操作,一次是将收到的消息写入管道,另一次是从管道读出。更有效的方法是仅将管道用于告知主线程有一个新消息可用,我们用单个字节用作通知。为了采用这种方法,我们可将mymesg结构移动到threadinfo结构中,并使用一个互斥量和条件变量来防止辅助线程在主线程完成前重新使用mymesg结构:

#include <poll.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/msg.h>
#include <sys/socket.h>

#define NQ 3    /* number of queues */
#define MAXMSZ 512    /* maximum message size */
#define KEY 0x123    /* key for first message queue */

struct mymesg {
    long mtype;
    char mtext[MAXMSZ + 1];
};

struct threadinfo {
    int qid;
    int fd;
    int len;
    pthread_mutex_t mutex;
    pthread_cond_t ready;
    struct mymesg m;
};

void *helper(void *arg) {
    int n;
    struct threadinfo *tip = arg;

    for (; ; ) {
        memset(&tip->m, 0, sizeof(struct mymesg));
		if ((n = msgrcv(tip->qid, &tip->m, MAXMSZ, 0, MSG_NOERROR)) < 0) {
		    printf("msgrcv error\n");
		    exit(1);
		}
		tip->len = n;
		// 此处需要先对互斥量加锁,然后再通知主线程
		// 否则主线程可能在我们加锁前,读取完消息队列中的消息并为了调用pthread_cond_signal而加锁
		// 这样主线程会先调用pthread_cond_signal,之后本线程才调用pthread_cond_wait,导致本线程一直阻塞
		pthread_mutex_lock(&tip->mutex);
		if (write(tip->fd, "a", sizeof(char)) < 0) {
		    printf("write error\n");
		    exit(1);
		}
		pthread_cond_wait(&tip->ready, &tip->mutex);
		pthread_mutex_unlock(&tip->mutex);
    }
}

int main() {
    char c;
    int i, n, err;
    int fd[2];
    int qid[NQ];
    struct pollfd pfd[NQ];
    struct threadinfo ti[NQ];
    pthread_t tid[NQ];

    for (i = 0; i < NQ; ++i) {
        if ((qid[i] = msgget((KEY + i), IPC_CREAT | 0666)) < 0) {
		    printf("msgget error\n");
		    exit(1);
		}
		printf("queue ID %d is %d\n", i, qid[i]);
        if (socketpair(AF_UNIX, SOCK_DGRAM, 0, fd) < 0) {
		    printf("socketpair error\n");
		    exit(1);
		}
		pfd[i].fd = fd[0];
        pfd[i].events = POLLIN;
		ti[i].qid = qid[i];
		ti[i].fd = fd[1];
		if (pthread_cond_init(&ti[i].ready, NULL) != 0) {
		    printf("pthread_cond_init error\n");
		    exit(1);
		}
		if (pthread_mutex_init(&ti[i].mutex, NULL) != 0) {
		    printf("pthread_mutex_init error\n");
		    exit(1);
		}
		if ((err = pthread_create(&tid[i], NULL, helper, &ti[i])) != 0) {
		    printf("pthread_create error\n");
		    exit(1);
		}
    }

    for (; ; ) {
        if (poll(pfd, NQ, -1) < 0) {
		    printf("poll error\n");
		    exit(1);
		}
		for (i = 0; i < NQ; ++i) {
		    if (pfd[i].revents & POLLIN) {
		        if ((n = read(pfd[i].fd, &c, sizeof(char))) < 0) {
				    printf("read error\n");
				    exit(1);
				}
				ti[i].m.mtext[ti[i].len] = 0;
				printf("queue id %d, message %s\n", qid[i], ti[i].m.mtext);
				pthread_mutex_lock(&ti[i].mutex);
				pthread_cond_signal(&ti[i].ready);
				pthread_mutex_unlock(&ti[i].mutex);
			}
    	}
	}

    exit(0);
}

socketpair函数创建的相互连接的UNIX域套接字没有名字,无关进程不能使用它们。可将一个地址绑定到一个因特网域套接字上,也可将一个地址绑定到UNIX域套接字上,UNIX域套接字的地址用sockaddr_un结构表示,在Linux 3.2.0和Solaris 10中,sockaddr_un结构定义在头文件sys/un.h中:
在这里插入图片描述
在FreeBSD 8.0和Mac OS X 10.6.8中,sockaddr_un结构定义如下:
在这里插入图片描述
sockaddr_un结构的sun_path成员包含一个路径名,当我们将一个地址绑定到一个UNIX域套接字时,系统会用该路径名创建一个S_IFSOCK类型文件,该文件仅用于向客户进程告知套接字的名字,无法被打开,也不能由应用程序用于通信。如果试图绑定到同一地址,该文件已经存在时,bind函数会失败。关闭套接字时不会删除该文件,在应用退出前,需要对该文件执行解除链接操作。

将地址绑定到UNIX域套接字:

#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <sys/un.h>

int main() {
    int fd, size;
    struct sockaddr_un un;

    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, "foo.socket");
    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        printf("socket failed\n");
		exit(1);
    }
    
    size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);    // offsetof函数获取sum_path成员在结构sockaddr_un中相对于结构体开头的偏移
    if (bind(fd, (struct sockaddr *)&un, size) < 0) {
        printf("bind failed\n");
		exit(1);
    }

    printf("UNIX domain socket bound\n");
    exit(0);
}

运行它:
在这里插入图片描述
第二次运行时由于地址已被绑定,因此绑定失败,此时只有删掉socket文件才能再绑定此地址。

以上程序确定绑定地址长度时,先计算了成员sun_path在sockaddr_un结构中的偏移量,然后将结果与路径名长度(不包含null字符)相加,这是由于sockaddr_un结构中sun_path成员之前的成员与实现相关。

offsetof函数常定义为宏:

#define offsetof(TYPE, MEMBER) ((int)&((TYPE *)0)->MEMBER))

该宏假定结构从地址0开始,返回该成员地址的整型值。

服务器进程可用bind、listen、accept函数与客户进程建立一个UNIX域连接。
在这里插入图片描述
以下是自编写的三个用于建立UNIX域套接字连接的函数:
在这里插入图片描述
服务器进程调用serv_listen函数声明它要在一个众所周知的名字(文件系统中的某个路径)上监听客户进程的连接请求。客户进程可用该名字连接到服务器进程。serv_listen函数的返回值是用于接收客户进程连接请求的服务器UNIX域套接字。

服务器进程可用serv_accept函数等待客户进程连接请求的到达,当一个连接请求到达时,系统自动创建一个新的UNIX域套接字,并将它与客户端套接字相连接,函数返回值就是这个新UNIX域套接字。客户进程的有效用户ID存放在参数uidptr指向的存储区中。

客户进程调用cli_conn函数连接到服务器进程,参数name必须与服务器进程调用serv_listen函数时用的名字相同,函数返回值是连接到服务器进程的文件描述符。

serv_listen函数:

#include <sys/socket.h>
#include <unistd.h>
#include <stddef.h>
#include <sys/un.h>
#include <errno.h>
#include <string.h>

#define QLEN 10

// Create a server endpoint of a connection.
// Returns fd if all OK, <0 on error.
int serv_listen(const char *name) {
    int fd, len, err, rval;
    struct sockaddr_un un;

    if (strlen(name) >= sizeof(un.sun_path)) {
        errno = ENAMETOOLONG;
		return -1;
    }

    // create a UNIX domain stream socket
    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        return -2;
    }

    unlink(name);    // incase it already exists

    // fill in socket address structure
    // 此处不需要设置某些平台提供的sun_len字段,操作系统会用传给bind函数的第三个地址长度参数设置该字段
    memset(&un, 0, sizeof(un));
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, name);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(name);

    // bind the name to the descriptor
    if (bind(fd, (struct sockaddr *)&un, len) < 0) {
        rval = -3;
		goto errout;
    }

    if (listen(fd, QLEN) < 0) {    // tell kernel we're a server
        rval = -4;
		goto errout;
    }

    return fd;

errout:
    err = errno;
    close(fd);
    errno = err;
    return rval;
}

以上函数中,如果文件已经存在,我们先以代表UNIX域套接字的文件名为参数调用unlink,为了防止误删除不是套接字的文件,可先调用stat验证文件类型,如果是UNIX域套接字再unlink它,这种做法的问题如下:
1.调用stat和unlink之间可能文件会发生改变。

2.如果名字是一个指向UNIX域套接字文件的符号链接,那么stat函数会报告名字是一个套接字,但调用unlink时,删除的是符号链接,然后我们再创建一个同名的UNIX域套接字文件,其他进程就可能使用该UNIX域套接字。可用lstat函数替换stat函数,lstat函数可检测出该文件是符号链接,如果该文件类型不是UNIX域套接字,可以换一个名字,防止其他进程误用该UNIX域套接字,但这解决不了第一个问题。

serv_accept函数:

#include <sys/socket.h>
#include <stdlib.h>
#include <stddef.h>
#include <sys/stat.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <time.h>
#include <errno.h>

#define STALE 30    // client's name can't be older than this (sec)

// Wait for a client connection to arrive, and accept it.
// We also obtain the client's user ID from the pathname
// that it must bind before calling us.
// Returns new fd if all OK, <0 on error.
int serv_accept(int listenfd, uid_t *uidptr) {
    int clifd, err, rval;
    socklen_t len;
    time_t staletime;
    struct sockaddr_un un;
    struct stat statbuf;
    char *name;

    // allocate enough space for longest name plus terminating null
    if ((name = malloc(sizeof(un.sun_path) + 1)) == NULL) {
        return -1;
    }
    len = sizeof(un);
    if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
        free(name);
		return -2;    // often errno = EINTR, if signal caught
    }

    // obtain the client's uid from its calling address
    len -= offsetof(struct sockaddr_un, sun_path);    // len of pathname
    memcpy(name, un.sun_path, len);
    name[len] = 0;    // 确保套接字文件路径以null结尾,如果路径名占用了sockaddr_un.sun_path的所有空间,就没有空间存放null了
    if (stat(name, &statbuf) < 0) {
        rval = -3;
		goto errout;
    }

#ifdef S_ISSOCK    // not defined for SVR4
    if (S_ISSOCK(statbuf.st_mode) == 0) {
        rval = -4;    // not a socket
		goto errout;
    }
#endif

    if ((statbuf.st_mode & (S_IRWXG | S_IRWXO)) || statbuf.st_mode & S_IRWXU != S_IRWXU) {
        rval = -5;    // is not rwx------
		goto errout;
    }

    staletime = time(NULL) - STALE;
    if (statbuf.st_atime < staletime || statbuf.st_ctime < staletime || statbuf.st_mtime < staletime) {
        rval = -6;    // i-node is too old
		goto errout;
    }

    if (uidptr != NULL) {
        *uidptr = statbuf.st_uid;    // return uid of caller
    }
    unlink(name);    /* we're done with pathname now */
    free(name);
    return clifd;

errout:
    err = errno;
    close(clifd);
    free(name);
    errno = err;
    return rval;
}

cli_conn函数:

#include <sys/socket.h>
#include <stdio.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <errno.h>

#define CLI_PATH "/var/tmp/"
#define CLI_PERM S_IRWXU    // rwx for user only

// Create a endpoint and connect to a server.
// Returns fd if all OK, <0 on error.
int cli_conn(const char *name) {
    int fd, len, err, rval;
    struct sockaddr_un un, sun;
    int do_unlink = 0;

    if (strlen(name) > sizeof(un.sun_path)) {
        errno = ENAMETOOLONG;
		return -1;
    }

    // create a UNIX domain stream socket
    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        return -1;
    }

    // fill socket address structure with our address
    memset(&un, 0, sizeof(un));
    un.sun_family = AF_UNIX;
    sprintf(un.sun_path, "%s%05ld", CLI_PATH, (long)getpid());
    // 由于sockaddr_un.sun_path字段使用了一个固定大小的字符数组来存储路径名
    // 但实际存储其中的路径名长度是可变的,因此不能用sizeof(sockaddr_un)来计算长度
    len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);

    unlink(un.sun_path);    // incase it already exists
    if (bind(fd, (struct sockaddr *)&un, len) < 0) {
        rval = -2;
		goto errout;
    }
    if (chmod(un.sun_path, CLI_PERM) < 0) {
        rval = -3;
		do_unlink = 1;
		goto errout;
    }

    // fill socket address structure with server's address
    memset(&sun, 0, sizeof(sun));
    sun.sun_family = AF_UNIX;
    strcpy(sun.sun_path, name);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
    if (connect(fd, (struct sockaddr *)&sun, len) < 0) {
       rval = -4;
       do_unlink = 1;
       goto errout;
    }
    return fd;

errout:
    err = errno;
    close(fd);
    if (do_unlink) {
        unlink(un.sun_path);
    }
    errno = err;
    return rval;
}

上例中,没有让系统选择默认地址,而是绑定在/var/tmp/pid上,目的是为了让服务器进程区分各个客户进程,如果不为UNIX域套接字显式绑定名字,内核会隐式地绑定一个地址,且不会在文件系统中创建表示这个套接字的文件。以上函数中,客户端调用bind将名字赋给客户进程套接字,这会在文件系统中创建一个套接字文件,之后关闭除用户读、写、执行以外的其他权限,在serv_accept函数中,服务器进程检验这些权限以及套接字文件的用户ID以验证客户进程身份。之后,填充服务进程的sockaddr_un结构,这次用的是服务进程众所周知的路径名,然后调用connect初始化与服务进程的连接。

一个进程可向另一个进程传送一个打开文件描述符:
在这里插入图片描述
传送文件描述符使一个进程打开文件描述符时所要做的一切操作(包括将网络名翻译为网络地址、拨号调制解调器、协商文件锁等)对传送文件描述符的目标进程透明。

接收文件描述符的进程将文件指针放在第一个可用描述符项中,之后两个进程共享同一个文件表项,与调用fork后的父子进程共享打开文件表的情况完全相同。

当发送进程将描述符传给接收进程后,通常会关闭该描述符。发送进程关闭该描述符不会关闭该文件或设备,原因是该描述符仍被视为由接收进程打开,即使接收进程还未收到该描述符。

自定义的3个发送和接收文件描述符的函数:
在这里插入图片描述
send_fd和send_err函数可由一个进程(通常是服务器进程)调用,从而将一个描述符传送给另一个进程,等待接收描述符的进程调用recv_fd。

send_fd函数的参数fd是UNIX域套接字,参数fd_to_send是要发送的描述符。

send_err函数的参数fd是UNIX域套接字,该函数会发送参数errmsg表示的字符串+status参数表示的字节。status参数值应在-1~-255。

客户进程调用recv_fd函数接收描述符,如果发送者调用了send_fd,则函数返回非负描述符,否则,返回值是send_err函数的status参数。

制定发送文件描述符的协议:
1.为发送一个文件描述符,send_fd函数先发送2字节0,然后发送实际描述符。
2.为发送一条出错消息,send_err函数先发送errmsg(不带结尾null字节),然后是1字节0,最后是status的绝对值。
3.recv_fd函数先从头读取套接字中字节,直到遇到null字符,将null字符以及之前的所有字符(出错消息)都传送给userfunc参数表示的函数,客户端通常将write函数作为userfunc参数,用来输出出错消息。之后recv_fd函数读取状态字节,如果状态字节为0,表示一个描述符传送过来,否则表示没有描述符可接收。

send_err函数:

#include <string.h>

// Used when we had planned to send an fd using send_fd()
// but encountered an error instead. We send the error back
// using the send_fd()/recv_fd() protocol.
int send_err(int fd, int errcode, const char *msg) {
    int n;

    if ((n = strlen(msg)) > 0) {
        if (writen(fd, msg, n) != n) {    // send the error message,自定义函数,循环发送完n字节
		    return -1;
		}
    }

    if (errcode >= 0) {
        errcode = -1;    // must be negative
    }

    if (send_fd(fd, errcode) < 0) {
        return -1;
    }

    return 0;
}

为了用UNIX域套接字交换文件描述符,需要调用sendmsg和recvmsg函数,这两个函数都有一个指向msghdr结构的指针参数,该结构包含了要发送或要接收的消息的信息:
在这里插入图片描述
前两个字段通常用于在网络连接上发送数据报(如无连接的UDP数据报),此时前两个字段可指示数据报的目的地址,如果用不到(如TCP或连接的UDP),可分别设为NULL和0。接下来两个字段用于散布读和聚集写。msg_flags字段包含了接收到的消息的各种特征(如接收了带外数据、控制数据被截断等),此字段在sendmsg函数中被忽略。

msg_control和msg_controllen字段处理控制信息的传送和接收。msg_control字段指向cmsghdr(control message header)结构;msg_controllen字段是控制信息的字节数。
在这里插入图片描述
为发送文件描述符,需要将cmsg_len字段设为cmsghdr结构的长度加一个整型的长度(文件描述符的长度),cmsg_level字段设为SOL_SOCKET,cmsg_type字段设为SCM_RIGHTS(表示在传送访问权;SCM是Socket-level Control Message,即套接字级控制消息)。访问权只能通过UNIX域套接字传送。文件描述符紧随在cmsg_type字段之后存储,可用CMSG_DATA宏获得指向该整型(即文件描述符)的指针。

三个用于访问控制数据的宏和一个用于计算cmsghdr.cmsg_len的值的宏:
在这里插入图片描述
SUS定义了前三个宏,但没有定义CMSG_LEN。

CMSG_LEN宏返回cmsghdr结构存储参数nbytes表示的控制数据字节数后的总字节数,它先将参数nbytes加上cmshdr结构的长度,再按处理器体系结构的对齐要求进行调整,最后向上取整。

seng_fd函数通过UNIX域套接字传送文件描述符,sendmsg函数使用UNIX域套接字传送协议数据(null字节+状态字节)和描述符:

#include <sys/socket.h>
#include <stdlib.h>
#include <stddef.h>

// size of control buffer to send/recv one file descriptor
#define CONTROLLEN CMSG_LEN(sizeof(int))

static struct cmsghdr *cmptr = NULL;    // malloc'ed first time

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

    iov[0].iov_base = buf;
    iov[0].iov_len = 2;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    if (fd_to_send < 0) {
	    msg.msg_control = NULL;
		msg.msg_controllen = 0;
		buf[1] = -fd_to_send;    // nonzero status means error
		if (buf[1] == 0) {    // -256, etc(这些数字会直接将后8bit给buf[1],即0). would screw up protocol
		    buf[1] = 1;    
		}
    } else {
        if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL) {
		    return -1;
		}
		cmptr->cmsg_level = SOL_SOCKET;
		cmptr->cmsg_type = SCM_RIGHTS;
		cmptr->cmsg_len = CONTROLLEN;
		msg.msg_control = cmptr;
		msg.msg_controllen = CONTROLLEN;
		*(int *)CMSG_DATA(cmptr) = fd_to_send;    // the fd to pass
		buf[1] = 0;    // zero status means OK
    }

    // buf[0]是错误消息结尾的空字符,如果不是通过send_err函数调用的本函数(没有错误消息),相当于错误消息是空串
    buf[0] = 0;    // null byte flag to recv_fd()
    if (sendmsg(fd, &msg, 0) != 2) {
        return -1;
    }

    return 0;
}

recv_fd函数:

#include <sys/socket.h>    // struct msghdr
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// size of control buffer to send/recv one file descriptor
#define CONTROLLEN CMSG_LEN(sizeof(int))

#define MAXLINE 1024

static struct cmsghdr *cmptr = NULL;    // malloc'ed first time

// Receive a file descriptor from a server process. Also, any data
// received 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, nr, status;
    char *ptr;
    char buf[MAXLINE];
    struct iovec iov[1];
    struct msghdr msg;

    status = -1;
    for (; ; ) {
        iov[0].iov_base = buf;
		iov[0].iov_len = sizeof(buf);
        msg.msg_iov = iov;
		msg.msg_iovlen = 1;
		msg.msg_name = NULL;
		msg.msg_namelen = 0;
		if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL) {
		    return -1;
		}
		msg.msg_control = cmptr;
		msg.msg_controllen = CONTROLLEN;
		if ((nr = recvmsg(fd, &msg, 0)) < 0) {
		    err_ret("recvmsg error");
		    return -1;
		} else if (nr == 0) {
		    err_ret("connection closed by server");
		    return -1;
		}
	
		// See if this is the final data with null & status. Null
		// is next to last byte of buffer; status byte is last byte.
		// Zero status means there is a file descriptor to receive.
		for (ptr = buf; ptr < &buf[nr]; ) {
		    if (*ptr++ == 0) {
		        if (ptr != &buf[nr - 1]) {    // 状态码应该在null字节后,且是收到的最后一个字节
				    err_dump("message format error");
				}
				status = *ptr & 0xFF;    // prevent sign extension,保证status到此大于等于0
				if (status == 0) {
				    if (msg.msg_controllen < CONTROLLEN) {
				        err_dump("status = 0 but no fd");
				    }
				    newfd = *(int *)CMSG_DATA(cmptr);
				} else {
				    newfd = -status;
				}
				nr -= 2;
		    }
		}
	
		if (nr > 0 && (*userfunc)(STDERR_FILENO, buf, nr) != nr) {    // userfunc通常是write函数
		    return -1;
		}
		if (status >= 0) {    // final data has arrived
		    return newfd;    // descriptor, or -status(-status是负数)
		}
    }
}

以上程序总是准备接收一个描述符(在每次调用recvmsg前,设置msg_control和msg_controllen)。

以上函数中,status = *ptr & 0xFF;这句代码的目的是为了防止符号扩展,符号扩展指的是当一个有符号整数类型从低精度到高精度转换时,如果最高位是1,会扩展高位的1,这样有符号数的值才会不变,但我们传的status是无符号数,因此不能进行符号扩展。

serv_accept函数会确定调用者身份,如果内核能把调用者的证书在调用accept后返回给调用处会更好。某些Unix域套接字的实现提供类似的功能,但它们的接口不同。

FreeBSD 8.0和Linux 3.2.0都支持通过UNIX域套接字发送证书,但它们的实现不同。Mac OS X 10.6.8是部分从FreeBSD派生出来的,但禁止传送证书。Solaris 10不支持通过UNIX域套接字传送证书,然而它支持获取通过STREAMS管道传送文件描述符的进程的证书。

FreeBSD中,将证书作为cmsgcred结构传送:
在这里插入图片描述
在这里插入图片描述
在传送证书时,需要为cmsgcred结构保留存储空间,内核将填充该结构空间,以防止应用程序伪装成具有另一种身份。

Linux中,证书作为ucred结构传送:
在这里插入图片描述
与FreeBSD不同,Linux需要在传输前初始化这个结构,内核会保证该结构中的值与调用者对应或调用者有权限使用该结构中的值。

包含发送进程证书的send_fd函数:

#include <sys/socket.h>
#include <stddef.h>
#include <malloc.h>

#if defined(SCM_CREDS)    /* BSD interface */
#define CREDSTRUCT cmsgcred
#define SCM_CREDTYPE SCM_CREDS    /* 在FreeBSD中,SCM_CREDS表示要传送证书 */
#elif defined(SCM_CREDENTIALS)    /* Linux interface */
#define CREDSTRUCT ucred
#define SCM_CREDSTRUCT SCM_CREDENTIALS    /* 在Linux中,使用SCM_CREDENTIALS表示要传送证书 */
#else
#error passing credentials is unsupported!
#endif

/* size of control buffer to send/recv one file descriptor */
#define RIGHTSLEN CMSG_LEN(sizeof(int))
#define CREDSLEN CMSG_LEN(sizeof(struct CREDSTRUCT))
#define CONTROLLEN (RIGHTSLEN + CREDSLEN)

static struct cmsghdr *cmptr = NULL;    /* malloc'ed first time */

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

    iov[0].iov_base = buf;
    iov[0].iov_len = 2;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_flags = 0;
    if (fd_to_send < 0) {
        msg.msg_control = NULL;
		msg.msg_controllen = 0;
		buf[1] = -fd_to_send;    /* nonzero status means error */
		if (buf[1] == 0) {
		    buf[1] = 1;    /* -256, etc. would screw up protocol */
		}
    } else {
        if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL) {
		    return -1;
		}
		msg.msg_control = cmptr;
		msg.msg_controllen = CONTROLLEN;
		cmp = cmptr;
		cmp->cmsg_level = SOL_SOCKET;
		cmp->cmsg_type = SCM_RIGHTS;
		cmp->cmsg_len = RIGHTSLEN;
		*(int *)CMSG_DATA(cmp) = fd_to_send;    /* the fd to pass */
	    cmp = CMSG_NXTHDR(&msg, cmp);
		cmp->cmsg_level = SOL_SOCKET;
		cmp->cmsg_type = SCM_CREDTYPE;
		cmp->cmsg_len = CREDSLEN;
		credp = (struct CREDSTRUCT *)CMSG_DATA(cmp);
#if defined(SCM_CREDENTIALS)
	    credp->uid = geteuid();
		credp->gid = getegid();
		credp->pid = getpid();
#endif
	    buf[1] = 0;    /* zero status means OK */
	}
	buf[0] = 0;
	if (sendmsg(fd, &msg, 0) != 2) {
	    return -1;
	}
	return 0;
}

以下函数recv_ufd是函数recv_fd的修改版,它通过一个引用参数返回发送者的用户ID:

#include <sys/socket.h>    /* struct msghdr */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>
#include <stddef.h>
#include <sys/un.h>

#define MAXLINE 1024

#if defined(SCM_CREDS)    /* BSD interface */
#define CREDSTRUCT cmsgcred
#define CR_UID cmcred_uid
#define SCM_CREDTYPE SCM_CREDS
#elif defined(SCM_CREDENTIALS)    /* Linux interface */
#define CREDSTRUCT ucred
#define CR_UID uid
#define CREDOPT SO_PASSCRED
#define SCM_CREDTYPE SCM_CREDENTIALS
#else
#error passing credentials is unsupported!
#endif

/* size of control buffer to send/recv ont file descriptor */
#define RIGHTLEN CMSG_LEN(sizeof(int))
#define CREDSLEN CMSG_LEN(sizeof(struct CREDSTRUCT))
#define CONTROLLEN (RIGHTLEN + CREDSLEN)

static struct cmsghdr *cmptr = NULL;    /* malloc'ed first time */

/*
 * Receive a file descriptor from a server process. Also, any data
 * received is passed to (*userfunc)(STDERR_FILENO, buf, nbytes).
 * We have a 2-byte protocol for receiving the fd from send_fd().
 */
int recv_ufd(int fd, uid_t *uidptr, ssize_t (*userfunc)(int, const void *, size_t)) {
    struct cmsghdr *cmp;
    struct CREDSTRUCT *credp;
    char *ptr;
    char buf[MAXLINE];
    struct iovec iov[1];
    struct msghdr msg;
    int nr;
    int newfd = -1;
    int status = -1;
#if defined(CREDOPT)
    const int on = 1;

    if (setsockopt(fd, SOL_SOCKET, CREDOPT, &on, sizeof(int)) < 0) {
        printf("setsockopt error\n");
		return -1;
    }
#endif
    for (; ; ) {
        iov[0].iov_base = buf;
		iov[0].iov_len = sizeof(buf);
		msg.msg_iov = iov;
		msg.msg_iovlen = 1;
		msg.msg_name = NULL;
		msg.msg_namelen = 0;
		if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL) {
		    return -1;
		}
		msg.msg_control = cmptr;
		msg.msg_controllen = CONTROLLEN;
		if ((nr = recvmsg(fd, &msg, 0)) < 0) {
		    printf("recvmsg error\n");
		    return -1;
		} else if (nr == 0) {
		    printf("connection closed by server\n");
		    return -1;
		}
	
		/*
		 * See if this is the final data with null & status. Null
		 * is next to last byte of buffer; status byte is last byte.
		 * Zero status means there is a file descriptor to receive.
		 */
		for (ptr = buf; ptr < &buf[nr]; ) {
		    if (*ptr++ == 0) {
		        if (ptr != &buf[nr - 1]) {
				    printf("message format error\n");
				    exit(1);
				}
				status = *ptr & 0xFF;    /* prevent sign extension */
				if (status == 0) {
				    if (msg.msg_controllen != CONTROLLEN) {
				        printf("status = 0 buf no fd\n");
						exit(1);
				    }
		
				    /* process the control data */
				    for (cmp = CMSG_FIRSTHDR(&msg); cmp != NULL; cmp = CMSG_NXTHDR(&msg, cmp)) {
				        if (cmp->cmsg_level != SOL_SOCKET) {
						    continue;
						}
						switch (cmp->cmsg_type) {
						case SCM_RIGHTS:
						    newfd = *(int *)CMSG_DATA(cmp);
						    break;
						case SCM_CREDTYPE:
						    credp = (struct CREDSTRUCT *)CMSG_DATA(cmp);
						    *uidptr = credp->CR_UID;
						}
				    }
				} else {
				    newfd = -status;
				}
				nr -= 2;
		    }
		}
		if (nr > 0 && (*userfunc)(STDERR_FILENO, buf, nr) != nr) {
		    return -1;
		}
		if (status >= 0) {    /* final data has arrived */
		    return newfd;    /* descriptor, or -status */
		}
    }
}

使用文件描述符传输开发一个open服务器,该服务器打开一个或多个文件,并将打开文件描述符送回客户。该服务器进程可对任何类型的文件(如设备或套接字)起作用。客户进程和服务器进程用IPC交换最小量的信息:客户进程传送文件名和打开模式,服务器进程传送描述符。文件内容不通过IPC交换。

将服务器设计成一个单独的可执行程序(由客户进程执行或由守护服务器进程执行)有很多优点:
1.任何客户进程都能方便地与服务器进程联系,类似于客户进程调用了一个库函数。我们没有将特定服务硬编码在应用程序中,而是设计了一种可供重用的设施。
2.如果要更改服务器进程,只需更新服务器程序,相反,更新一个库函数可能需要更新调用此库函数的所有程序(即用链接编辑器重新链接)。共享库函数可以简化这种更新。
3.服务器进程可以是一个设置用户ID程序,使其具有客户进程没有的附加权限。库函数(或共享库函数)不能提供这种能力。

客户进程创建一个fd管道,然后调用fork和exec来调用服务器进程,客户进程使用fd管道发送请求,服务器进程使用fd管道回送响应。

定义客户进程和服务器进程间的应用程序协议:
1.客户进程通过fd管道向服务器进程发送open <pathname> <openmode>\0形式的请求,<openmode>是数值,以ASCII十进制数表示,是open函数的第二个参数。
2.服务器进程调用send_fd或send_err回送打开描述符或出错消息。

客户进程调用csopen来联系open服务器进程,该函数定义在我们的头文件open.h中:

#include <errno.h>

#define CL_OPEN "open"    /* client's request for server */

int csopen(char *, int);

客户进程的main函数是一个循环,每次循环中,它先从标准输入读一个路径名,然后将该文件复制到标准输出。它调用csopen来联系open服务器进程,从其返回一个描述符:

#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

#define BUFFSIZE 8192
#define MAXLINE 1024

extern int csopen(char *, int);

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) {    /* 读入错误或遇到EOF时,返回NULL */
        // line的结构为xxxx\n\0
        if (line[strlen(line) - 1] == '\n') {
		    line[strlen(line) - 1] = 0;    /* replace new line 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) {
		        printf("write error\n");
		    }
		}
		if (n < 0) {
		    printf("read error\n");
		}
		close(fd);
    }

    exit(0);
}

函数csopen在创建了fd管道后,调用了fork和exec运行服务器进程:

#include <sys/uio.h>    /* struct iovec */
#include <string.h>
#include <open.h>    /* 最好是#include "open.h" */
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>

extern int recv_fd(int, ssize_t (*)(int, const void *, size_t));

// Returns a full-duplex pipe (a UNIX domain socket) with
// the two file descriptors returned in fd[0] and fd[1].
int fd_pipe(int fd[2]) {
    return socketpair(AF_UNIX, SOCK_STREAM, 0, fd);
}

/*
 * 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) {
    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) {
		    printf("fd_pipe error\n");
		    return -1;
		}
		if ((pid = fork()) < 0) {
		    printf("fork error\n");
		    return -1;
		} else if (pid == 0) {    /* child */
		    close(fd[0]);
		    if (fd[1] != STDIN_FILENO && dup2(fd[1], STDIN_FILENO) != STDIN_FILENO) {
		        printf("dup2 error to stdin\n");
				return -1;
		    }
		    if (fd[1] != STDOUT_FILENO && dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO) {
		        printf("dup2 error to stdout\n");
				return -1;
		    }
		    if (execl("./opend", "opend", (char *)0) < 0) {
		        printf("execl error\n");
				return -1;
		    }
		}
		close(fd[1]);    /* parent */
	}
	sprintf(buf, " %d", oflag);    /* oflag to ascii */
	iov[0].iov_base = CL_OPEN " ";    /* string concatenation,字符串连接时CL_OPEN表示的字符串的末尾null字节会被丢弃,结果是"open \0" */
	iov[0].iov_len = strlen(CL_OPEN) + 1;
	iov[1].iov_base = name;
	iov[1].iov_len = strlen(name);
	iov[2].iov_base = buf;
	iov[2].iov_len = strlen(buf) + 1;    /* +1 for null at end of buf,服务器会校验最后一位是否为\0 */
	len = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len;
	if (writev(fd[0], &iov[0], 3) != len) {
	    printf("writev error\n");
		return -1;
    }

    /* read descriptor, returned errors handled by write() */
    return recv_fd(fd[0], write);
}

csopen函数中,子进程关闭fd管道的一端,父进程关闭另一端。作为服务器进程,子进程也将fd管道的一端复制到其标准输入和标准输出(另一种可选方案是将子进程打开的fd管道一端(即fd[1])的ASCII表示形式作为一个参数传送给服务器进程),exec出来的服务器进程会从其标准输入和标准输出读来自客户进程的请求。

父进程将包含路径名和打开模式的请求发送给服务器进程,最后父进程调用recv_fd返回描述符或出错消息,如果服务器进程返回出错消息,那么父进程调用write,向标准错误输出该消息。

以上C代码中,头文件open.h是以尖括号而非引号括起来的,对于尖括号括起来的头文件,预处理器认为它是一个系统自带的头文件,而我们自己编写的头文件可能不在系统自带头文件的默认搜索路径中,系统自带头文件的默认搜索路径为/usr/include、/usr/local/include,除此之外,还会在环境变量C_INCLUDE_PATH表示的目录中进行搜索,可将open.h头文件所在目录添加到环境变量中:

export C_INCLUDE_PATH=path:$C_INCLUDE_PATH

环境变量的修改可在命令行输入,此时只会临时生效,关闭shell再打开或用户重新登录就会失效;也可将命令添加到/etc/profile文件中,这会对所有用户生效,此文件在用户登录时被执行一次;也可将命令添加到用户home目录下的.bashrc(此文件在用户登录或每次打开shell时执行)或.bash_profile(此文件在用户登录时被执行一次)中,这样仅针对某个用户生效。这三个文件的改动都可以用source filename的方式立即生效,source命令的作用为在当前shell环境下读取并执行filename中的命令,source命令常用.替代,如. filename

对于C++,头文件搜索路径相关的环境变量为CPLUS_INCLUDE_PATH。

对于用引号括起来的头文件,预处理器认为它是用户编写的头文件,会先在当前目录中查找,如果没有找到再到系统自带的头文件的默认搜索路径中搜索。

open服务器进程的程序是opend,首先要有一个opend.h头文件:

#include <errno.h>

#define CL_OPEN "open"    /* client's request for server */

extern char errmsg[];    /* error message string to return to client */
extern int oflag;    /* open() flag: O_xxx ... */
extern char *pathname;    /* of file to open() for client */

int cli_args(int, char **);
void handle_request(char *, int, int);

upend.h头文件中使用extern关键字声明了3个全局变量,此时并没有为它们分配存储单元。在以下main.c中,我们才分配了存储单元。

opend的main函数经fd管道(它的标准输入)读来自客户进程的请求,然后调用函数handle_request:

#include "opend.h"
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

#define MAXLINE 1024

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) {
		    printf("read error on stream pipe\n");
		} else if (nread == 0) {
		    break;    /* client has closed the stream pipe */
		}
		handle_request(buf, nread, STDOUT_FILENO);
    }
    exit(0);
}

opend的main函数中,handle_request函数承担了全部工作,它将调用buf_args将客户进程请求分解成标准argv型的参数表,然后调用cli_args处理客户进程的参数,如果一切正常,则调用open打开相应文件,接着调用send_fd,经由fd管道(opend进程的标准输出)将描述符回送给客户进程;如果出错,调用send_err回送一则出错消息。

以上函数中,main函数的参数是void,在C++中,它与main()完全相同,但在C中,main(void)明确指定main函数只能不用参数来调用。

// 在C中可正常运行,但在C++中会报错
void fun() {
    printf("called\n");
} 

int main(void) {
    fun(1, "df");
    return 0;
}
// 在C和C++中都会报错
void fun(void) { }

int main(void) {
    fun(1, "df");
    return 0;
}
// 在C和C++中都会报错
void fun(int i) {
    printf("called, param is %d", i);
}

int main(void) {
    fun(1, "df");
    return 0;
}

由上例可见,C语言中,如果函数声明时没有形参,则调用时可以传实参,但传入的实参会被忽略,但如果指定了形参,则调用时实参和形参必须匹配;C++中,不管函数声明时有没有形参,调用时实参和形参都要匹配。

handle_request函数:

#include "opend.h"
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>

#define MAXLINE 1024

extern int send_err(int, int, const char *);
extern int buf_args(char *, int (*)(int, char **));
extern int send_fd(int, int);

void handle_request(char *buf, int nread, int fd) {
    int newfd;

    if (buf[nread - 1] != 0) {
        // 第一个星号表示%s字符串的宽度
        // 第二个星号表示%s字符串的精度,即限制%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);    // errmsg会在调用buf_args时被设置
		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 */
        printf("send_fd error\n");
    }
    close(newfd);    /* we're done with descriptor */
}

客户进程的请求是一个以null终止的字符串,它包含由空格分隔的参数,使用以下buf_args函数将字符串分解成标准argv型参数表,并调用用户函数(上例中是cli_args函数)处理参数。buf_args函数用ISO C函数strtok将字符串分割成独立的参数:

#include <string.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 (opfunc)
 * 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);
}

以上代码中,参数数量是有限制的,可进行动态存储分配解除参数数量限制:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXARGC 3    /* max number of arguments in buf */
#define WHITE " \t\n"    /* white space for tokenizing arguments */
#define MAXLINE 1024

// 宏定义中,#是字符串化运算符,用于将宏参数转换为字符串常量
// 以下宏定义用于open函数时sys_chk(open("test.txt", O_RDONLY))
// 可能会输出这样的错误消息"Error in `open` syscall (file.c:10)"
#define sys_chk(call) if ((call) == -1) { \
    printf("Error in `" #call "` syscall (%s:%d)", __FILE__, __LINE__); \
    }

/*
 * 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;
    int argc;

    int argv_len = MAXARGC;
    char **argv = calloc(argv_len, sizeof(char *));    // calloc函数分配内存,并将内存置0

    if (strtok(buf, WHITE) == NULL) {    /* an argv[0] is required */
        return -1;
    }
    argv[argc = 0] = buf;
    while ((ptr = strtok(NULL, WHITE)) != NULL) {
        if (++argc >= argv_len - 1) {
		    argv_len *= 2;
		    argv = realloc(argv, argv_len * sizeof(char *));
		}
		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.
     */
    int ret = (*optfunc)(argc, argv);
    free(argv);
    return ret;
}

int print(int argc, char *argv[]) {
    for (int i = 0; i < argc; ++i) {
        printf("#%d: %s\n", i, argv[i]);
    }
    return 0;
}

int main() {
    char *buf =  malloc(MAXLINE);

    strcpy(buf, "hello world a b c");
    sys_chk(buf_args(buf, print));

    return EXIT_SUCCESS;
}

运行以上代码:
在这里插入图片描述
buf_args函数中的strtok函数会根据WHITE宏中的字符将buf中的内容分隔,每次调用strtok都会返回指向一个分隔部分的指针,buf中的每个存在于WHITE中的字符都会被替换为空字节(每次调用strtok只会替换一个字节),从而使指针指向的每个分隔部分在使用时当作一个完整字符串使用。如果连续两个以上字符都要被替换时,只会替换第一个字符,并在下一次调用时返回最后一个要替换的字符之后的字符的指针:

#include <string.h>
#include <stdio.h>

int main()
{
    char str[80] = "This -- aaa - bbb---";
    const char s[2] = "-";
    char *token;

    /* 获取第一个子字符串 */
    token = strtok(str, s);
    /* 继续获取其他的子字符串 */
    while (token != NULL)
    {
        printf("%s\n", token);
        token = strtok(NULL, s);
    }
    printf("\n");

    for (int i = 0; i < 21; i++) {
        if (str[i] == '\0') {
    	    printf("\\0");
	 	    continue;
		}
        printf("%c", str[i]);
    }
    printf("\n");

    return (0);
}

运行以上函数:
在这里插入图片描述
函数cli_args验证客户进程发送的参数个数是否正确,并将路径名和打开模式存储在全局变量中:

#include "opend.h"
#include <string.h>
#include <stdlib.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;
}

以下是守护进程方式的open服务器进程,一个服务器进程处理所有客户进程的请求。由于避免了fork和exec函数,我们期望这个设计更有效率。在服务器进程和客户进程之间仍使用UNIX域套接字连接。

守护进程方式实现的open服务器的客户进程main函数与子进程方式实现的open服务器的客户进程main函数完全相同。对于open.h头文件,需要加入以下行:

#define CS_OPEN "/tmp/opend.socket"    /* server's well-known name */

守护进程方式实现的open服务器的客户的csopen函数:

#include "open.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/uio.h>    /* struct iovec */

extern int recv_fd(int, ssize_t (*)(int, const void *, size_t));
extern int cli_conn(const char *);

/*
 * 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 */
        if ((csfd = cli_conn(CS_OPEN)) < 0) {
		    printf("cli_conn error\n");
		    return -1;
		}
    }

    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;
    iov[2].iov_len = strlen(buf) + 1;    /* null always sent */
    len = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len;
    if (writev(csfd, &iov[0], 3) != len) {
        printf("writev error\n");
		return -1;
    }

    /* read back descriptor; returned errors handled by write() */
    return recv_fd(csfd, write);
}

守护进程方式实现的open服务器的头文件opend.h:

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

#define CS_OPEN "/tmp/opend.socket"    /* well-known name */
#define CL_OPEN "open"    /* client's request for server */

extern int debug;    /* nonzero if interactive (not daemon) */
extern char errmsg[];    /* error message string to return to client */
extern int oflag;    /* open flag: O_xxx ... */
extern char *pathname;    /* of file to open for client */

typedef struct {    /* one Client struct per connected client */
    int fd;    /* fd, or -1 if available */
    uid_t uid;
} Client;

extern Client *client;    /* ptr to malloc'ed array */
extern int client_size;    /* # entries in client [] array(#表示number of) */

int cli_rgs(int, char **);
int client_add(int, uid_t);
void client_del(int);
void loop(void);
void handle_request(char *, int, int, uid_t);

因为此服务器进程处理所有客户进程,它必须保存每个客户进程连接的状态,这是通过opend.h头文件中声明的client数组实现的,以下是3个处理client数组的函数:

#include "opend.h"
#include <stddef.h>
#include <stdlib.h>
#include <stdio.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) {
        client = malloc(NALLOC * sizeof(Client));
    } else {
        client = realloc(client, (client_size + NALLOC) * sizeof(Client));
    }

    if (client == NULL) {
        err_sys("Can't alloc for client array\n");
		return;
    }

    /* initialize the new entries */
    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\n", fd);
}

client数组的长度是运行时动态分配的,不需要在编译时将估计的数组长度值放入头文件中从而限制client数组的长度。

通常服务器进程会作为守护进程运行,但我们想提供一个让其前台运行的选项,此时就能将分析信息发送到标准错误输出,这可以使服务器更容易评测和调试,特别是当用户没有权限读取日志文件时。可以使用一个命令行选项来控制服务器在前台运行或作为守护进程在后台运行。

一个系统的所有命令遵循相同的约定很重要,这可以提高系统的易用性。有些命令需要它的选项和其参数以空格隔开,而另一些希望它的参数直接跟在它的选项之后,如果命令没有遵循一致的规则,用户就得记住所有命令的语法。

SUS有命令行语法一致性的规范,其中包含一些建议,如限制每个命令行选项为一个单一的字母或数字字符、所有选项应该以-作为开头字符。

getopt函数可帮助开发者以一致的方式处理命令行选项:
在这里插入图片描述
参数argc和argv是传入main函数的参数,getopt函数负责解析传入main函数的参数。options参数是一个包含该命令支持的选项字符的字符串,如果一个选项字符后面接了一个冒号,则表示该选项需要参数,否则,该选项不需要额外参数,如一条命令的用法如下:

command [-i] [-u username] [-z] filename

则我们可以给getopt函数传送一个"iu:z"作为options字符串参数。

函数getopt一般用在循环体内,循环直到getopt函数返回-1时退出。每次循环中,getopt函数会返回下一个选项,应用需要筛选这些选项,判断是否有冲突,getopt函数仅负责解释选项本身并保证一个标准的格式。

遇到无效选项时,getopt函数返回一个问号而非这个选项字符,如果选项缺少参数,也会返回一个问号,但如果选项字符串的第一个字符是冒号,getopt函数会直接返回冒号。特殊的--格式会导致getopt函数停止处理选项并返回-1,这允许用户传递以-开头,但不是选项的参数,如有一个名字为-bar的文件夹,下面的命令行无法删除这个文件夹(rm命令的-r选项代表将递归地删除指定的目录,直到删除完整个目录树):

rm -r -bar

因为rm命令会试图把-bar解释为选项,正确命令应该是:

rm -r -- -bar

以下命令是错误的,因为循环使用getopt函数处理到--时已经结束,-r选项处理不到:

rm -- -r -bar

getopt函数支持以下4个外部变量:
1.optarg:如果一个选项需要参数,处理该选项时,getopt函数会设置optarg指向该选项的参数字符串。
2.opterr:如果一个选项发生了错误,getopt函数会默认打印一条出错消息。应用可通过设置opterr为0来禁止这个行为。
3.optind:用来存放下一个要处理的字符串在argv数组里的下标,它从1开始,每处理一个参数,getopt函数对其递增1。
4.optopt:如果处理选项时发生了错误,getopt函数会设置optopt指向导致出错的选项字符串。

open服务器进程的main函数定义全局变量,处理命令行选项,且调用loop函数。如果以-d选项调用服务器进程,则服务器进程将以交互方式而非守护进程方式运行:

#include "opend.h"
#include <syslog.h>

int debug, oflag, client_size, log_to_stderr;
char errmsg[MAXLINE];
char *pathname;
Client *client = NULL;

void log_open(const char *, int, int);

int main(int argc, char *argv[]) {
    int c;
    
    // LOG_PID使syslog函数记录日志消息时包含进程id
    log_open("open.serv", LOG_PID, LOG_USER);

    opterr = 0;    /* don't want getopt() writing to stderr */
    // 此处通过判断getopt函数的返回值是否是EOF来判断是否选项已被处理完
    // getopt函数返回-1表示选项已被处理完,且EOF的典型值为-1
    // 但在某些特殊的系统或编译环境中,其值可能是其他负数,此处最好使用-1代替EOF
    while ((c = getopt(argc, argv, "d")) != EOF) {
        switch (c) {
		case 'd':
		    debug = log_to_stderr = 1;
		    break;
	
		case '?':
		    err_quit("unrecognized option: -%c", optopt);
		}
    }

    if (debug == 0) {
        daemonize("opend");
    }

    loop();    /* never returns */
}

loop函数是服务器进程的无限循环,给出该函数的两种版本,分别是使用select函数和poll函数的版本,以下是使用select函数的版本:

#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\n");
    }
    FD_SET(listenfd, &allset);
    maxfd = listenfd;
    maxi = -1;

    for (; ; ) {
        rset = allset;    /* rset gets modified each time around */
		if ((n = select(maxfd + 1, &rset,  NULL, NULL, NULL)) < 0) {
		    log_sys("select error\n");
		}
	
		if (FD_ISSET(listenfd, &rset)) {
		    /* accept new client request */
		    if ((clifd = serv_accpet(listenfd, &uid)) < 0) {
		        log_sys("serv_accept error: %d", clifd);
		    }
		    i = client_add(clifd, uid);
		    FD_SET(clifd, &allset);
		    if (clifd > maxfd) {
		        maxfd = clifd;    /* max fd for select() */
		    }
		    if (i > maxi) {
		        maxi = i;    /* max index in client[] array */
		    }
		    log_msg("new connection:uid %d, fd %d", uid, clifd);
		    continue;
		}
	
		for (i = 0; i <= maxi; ++i) {    /* go through client[] array */
		    if ((clifd = client[i].fd) < 0) {
		        continue;
		    }
		    if (FD_ISSET(clifd, &rset)) {
		        /* read argument buffer from cilent */
				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_del(clifd);    /* client has closed cxn(cxn是connection的简写) */
				    FD_CLR(clifd, &allset);
				    close(clifd);
				} else {    /* process client's request */
				    handle_request(buf, nread, clifd, client[i].uid);
				}
		    }
		}
    }
}

我们总是可以知道客户已经终止了,无论终止是主动的还是被动的,因为客户进程的所有的描述符都能自动被内核关闭。这与XSI IPC机制不同。

使用poll函数的loop函数:

#include "opend.h"
#include <stdlib.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");
    }
    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;

    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");
    }
    client_add(listenfd, 0);    /* we use [0] for listenfd */
    pollfd[0].fd = listenfd;

    for (; ; ) {
        if (poll(pollfd, numfd, -1) < 0) {
		    log_sys("poll error");
		}
	
		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 conenction: uid %d, fd %d", uid, clifd);
		}
	
		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 */
				    // 书中这句代码应该是有问题的,在一个客户断开连接后,会执行下面的pack the array过程
				    // 这一过程中,只修改了pollfd数组中内容,没有修改client数组中内容
				    // 会导致client[i]和pollfd[i]不再对应同一个客户连接
				    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;    /* recheck this entry */
				    }
				    --numfd;
				} else {    /* process client's request */
				    handle_request(buf, nread, pollfd[i].fd, client[i].uid);
				}
		    }
		}
    }
}

以上代码中,对于一个现有的客户进程,应当处理来自poll的两个不同事件:由POLLHUP指示的客户进程终止;由POLLIN指示的来自现有客户进程的一个新请求。在服务器端还有数据未读取时,客户就可以关闭客户端的连接。即使客户端已经被标记挂起(连接已中断),服务器端仍然可以读服务器端仍未读取的队列中的数据,但以上代码中,当我们收到客户端的挂起时,我们直接关闭了到客户端的连接,这会直接丢弃队列中的未读数据,因为我们已不能将应答送回,已经没有必要处理剩余的请求了。

守护进程版本open服务器的handle_request函数相比其子进程版本,在日志文件中记录出错消息,而不是在标准错误上打印:

#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 */
}

使用本章的文件描述符传送函数和第8章中的父进程和子进程同步例程,编写具有以下功能的程序:程序调用fork,子进程打开一个现有文件并将文件描述符传给父进程,然后子进程调用lseek改变该文件的当前读、写位置,通知父进程,父进程查看当前该文件的偏移量,并打印它验证。文件描述符从子进程传到父进程后,父子进程共享同一文件表项,因此当子进程每次更改文件当前偏移量时,也会影响父进程的描述符:

#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define sys_chk(call) if ((call) == -1) { \
    printf("Error in `" #call "` syscall (%s:%d)", __FILE__, __LINE__); \
    }

int main() {
    int pid, fd;
    int fdpair[2];

    sys_chk(socketpair(AF_UNIX, SOCK_STREAM, 0, fdpair));

    TELL_WAIT();

    sys_chk(pid = fork());

    if (pid == 0) {    // child
        sys_chk(fd = open("/etc/passwd", O_RDONLY));
		syschk(send_fd(fdpair[0], fd));
	
		WAIT_PARENT();
		sys_chk(lseek(fd, 10, SEEK_SET));
		TELL_PARENT(getppid());
	
		WAIT_PARENT();
		syschk(lseek(fd, 20, SEEK_SET));
		TELL_PARENT(getppid());
	
		return EXIT_SUCCESS;
    } else {    // parent
        sys_chk(fd = recv_fd(fdpair[1], write));
		printf("Parent: got fd: %d, seek %lld\n", fd, (long long)lseek(fd, 0, SEEK_CUR));
		TELL_CHILD(pid);
	
		WAIT_CHILD();
		printf("Parent: seek after child changed it: %lld (should be 10)\n", (long long)lseek(fd, 0, SEEK_CUR));
		TELL_CHILD(pid);
	
		WAIT_CHILD();
		printf("Parent: seek after child changed it second time: %lld (should be 20)\n", (long long)lseek(fd, 0, SEEK_CUR));
	
		return EXIT_SUCCESS;
    }
}

open服务器端每次select或poll函数有描述符可读返回时就遍历所有描述符,可进行优化,由于select或poll函数返回就绪的描述符个数,当处理了相应个数的描述符后就可以不遍历后边的描述符了。

单次调用sendmsg传递多个文件描述符的方法:
1.将两个文件描述符放在一个控制消息中发送,每一个文件描述符存储在相邻的内存位置中:

#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>

#define MAX_FDS_LEN 1024

#define sys_chk(call) if ((call) == -1) { \
    printf("Error in `" #call "` syscall (%s:%d)", __FILE__, __LINE__); \
    }

void send_fds(int sockfd, int fds[], int fds_len) {
    // 聚合初始化,把所有成员都初始化为0
    struct msghdr msg = { 0 };
    struct cmsghdr *cmsg;
    int fds_size = sizeof(int) * fds_len;
    char buf[CMSG_SPACE(fds_size)];
    struct iovec iov[1];
    int iov_buf[1];

    assert(fds_len < MAX_FDS_LEN);
    iov_buf[0] = fds_len;

    iov[0].iov_base = iov_buf;
    iov[0].iov_len = sizeof(iov_buf);
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(fds_size);

    memcpy(CMSG_DATA(cmsg), fds, fds_size);

    printf("my_send_fd: about to send fds:");
    for (int i = 0; i < fds_len; ++i) {
         printf(" %d", fds[i]);
    }
    printf("\n");
    sys_chk(sendmsg(sockfd, &msg, 0));
    printf("my_send_fd: send\n");
}

void recv_fds(int sockfd, int **fds, int *fds_len) {
    struct msghdr msg = { 0 };
    struct cmsghdr *cmsg = malloc(CMSG_SPACE(sizeof(int) * MAX_FDS_LEN));
    if (cmsg == NULL) {
        printf("malloc error\n");
		exit(1);
    }
    struct iovec iov[1];
    int iov_buf[1];

    iov[0].iov_base = iov_buf;
    iov[0].iov_len = sizeof(iov_buf);
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    msg.msg_control = cmsg;
    msg.msg_controllen = CMSG_LEN(sizeof(int) * MAX_FDS_LEN);

    printf("my_recv_fd: about to receive fd\n");
    sys_chk(recvmsg(sockfd, &msg, 0));
    printf("my_recv_fd: received\n");
    cmsg = CMSG_FIRSTHDR(&msg);
    *fds = (int *)CMSG_DATA(cmsg);
    *fds_len = iov_buf[0];
}

int main() {
    int pid;
    int fds[3];
    int spair[2];

    sys_chk(socketpair(AF_UNIX, SOCK_STREAM, 0, spair));

    sys_chk(pid = fork());
    if (pid == 0) {   
        sys_chk(fds[0] = open("/etc/passwd", O_RDONLY));
		sys_chk(lseek(fds[0], 5, SEEK_SET));
		sys_chk(fds[1] = open("/etc/group", O_RDONLY));
		sys_chk(lseek(fds[1], 10, SEEK_SET));
		sys_chk(fds[2] = open("/bin/sh", O_RDONLY));
		sys_chk(lseek(fds[2], 20, SEEK_SET));
		send_fds(spair[0], fds, 3);
		return EXIT_SUCCESS;
    } else {    
        int *fds;
		int fds_len;
		recv_fds(spair[1], &fds, &fds_len);
		printf("parent: got fds:");
		for (int i = 0; i < fds_len; ++i) {
		    printf(" %d (fp = %lld)", fds[i], (long long)lseek(fds[i], 0, SEEK_CUR));
		}
		printf("\n");
		return EXIT_SUCCESS;
    }
}

运行它,它在本书涉及的4种平台上都可以运行:
在这里插入图片描述
上例中,CMSG_SPACE宏返回一个cmsghdr结构对齐后的总大小,CMSG_LEN宏并不包括可能的结尾的对齐填充字符。CMSG_SPACE用于给辅助数据分配空间,而CMSG_LEN的返回值只用于赋值给cmsghdr.cmsg_len。

2.将两个独立的cmsghdr结构打包到一个辅助数据成员msghdr.msg_control中,在本书所涉及的4种平台中,这种方法只能在FreeBSD 8.0上工作:

struct msghdr msg;
struct cmsghdr *cmptr;

// 假设传两个文件描述符
if ((cmptr = calloc(1, 2 * CMSG_LEN(sizeof(int))) == NULL) {
    err_sys("calloc error");
}
msg.msg_control = cmptr;
msg.msg_controllen = 2 * CMSG_LEN(sizeof(int));
/* continue initializing msghdr */
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*(int *)CMSG_DATA(cmptr) = fd1;
cmptr = CMPTR_NXTHDR(&msg, cmptr);
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*(int *)CMSG_DATA(cmptr) = fd2;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值