[单刷APUE系列]第十七章——高级进程间通讯

引言

前面讲述了各类进程间通讯和网络IPC的内容,除此之外,还有一种很是经常使用的IPC——UNIX域套接字。这种套接字实际上就是一种文件,可以让本机的进程之间互相通讯。node

Unix域套接字

Unix域套接字用于同一台电脑上运行的进程通讯。虽然TCP/IP协议的套接字很方便,可是在某些状况下须要保证更高的通讯效率,因此Unix域套接字更加适合,由于它是经过内核转发的信息,而网络协议通讯须要经过网卡发送,可能效率更低些。
虽然不经过网络传输数据,可是Unix域套接字也有流和数据报两种接口。Unix域套接字就像是套接字和管道的组合,在数据传输上很像管道,可是是以套接字的形式使用。数组

int socketpair(int domain, int type, int protocol, int socket_vector[2]);

socketpair函数建立一对未命名的已链接的套接字在指定的域(domain)中,而且以指定的类型(type),可选指定协议(protocol),描述符将存放在socket_vector数组中。须要注意的是,这对套接字其实是全双工的,在不少Unix实现中,全双工的管道之类的实际上就是经过Unix域套接字实现的。网络

命名Unix域套接字

前面的socketpair虽然很方便,可是它建立的是未命名的套接字,也就是说不一样进程没法使用,在前面的网络套接字章节中讲了套接字如何绑定一个地址和端口,可是咱们也能够将其绑定到路径上,使其成为一个文件,这样就能让不一样进程使用。dom

#include "include/apue.h"
#include <sys/socket.h>
#include <sys/un.h>

int main(int argc, char *argv[])
{
    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)
        err_sys("socket failed");
    size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
    if (bind(fd, (struct sockaddr *)&un, size) < 0)
        err_sys("bind failed");
    printf("UNIX domain socket bound\n");
    exit();
}

上面的例程很是简单,首先是建立一个Unix域地址,使用strcpy函数将地址复制到地址族变量,而后使用socket函数建立一个套接字,而后计算路径成员在结构体中的偏移量加上路径自己的长度,求出size变量,而后使用bind函数将地址族结构体和套接字绑定在一块儿。
当咱们运行程序完毕,就能在当前目录下看到一个foo.socket文件,可能就有人要问了,为何程序结束这个文件仍然存在,套接字不是文件描述符吗,不是在程序结束的时候就回收了吗?实际上,这是搞混了文件描述符和文件的区别,文件描述符只是0、一、2之类的数字,用于指向文件表项,实际上文件的打开是由内核维护的。套接字也同样,当咱们建立套接字的时候,而且将其绑定到具体路径,内核就会帮助咱们建立一个S_IFSOCK类型的文件,可是实际上这个文件并无什么用,它不会用于实际的写入,否则这不就是和普通的文件同样了吗,因此这个文件纯粹就是个flag,用于标记地址,就跟一般使用的xxx.pid文件这种形式相似。socket

惟一链接

这小节没什么重要内容,除了三个封装函数,其中有一些内容多是有困惑的,这里笔者将本身的理解讲一下。首先,咱们先须要知道各个平台实际上实现是有差别的,例如sockaddr_un的结构体不一样,在Linux和Solaris中,是以下所示:函数

struct sockaddr_un {
    sa_family_t sun_family;
    char sun_path[108];
};

而在FreeBSD和OSX系统中,是以下的:ui

struct sockaddr_un {
    unsigned char sun_len;
    sa_family_t sun_family;
    char sun_path[104];
};

先给出serv_listen函数this

#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.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);    /* in case it already exists */

    /* fill in socket address structure */
    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域套接字,若是文件已经存在则删除原先文件,而后构造sockaddr_un结构体,而后使用bind函数将地址和套接字绑定,系统自动生成套接字文件,而后使用listen函数侦听套接字。这里基本没什么须要讲解的。
而后是serv_accept函数编码

#include "apue.h"
#include <sys/socket.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 terminate */
    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);
}

首先是使用accept函数阻塞等待客户进程链接。当accept返回的时候,返回的是新的套接字描述符,也就是存在链接的套接字,而且从第二个参数获得套接字的路径名,接着复制路径名,最后调用stat函数检查路径名。
其中len -= offsetof(struct sockaddr_un, sun_path);可能有些人不是很明白,这里实际上用了点编码技巧,实际上就是结构体总长度减去sun_path成员的内存偏移量,最终就是sun_path的长度。还有,accept第二个餐宿实际上和bind是不同的,由于这个套接字是已经链接的套接字,因此会包含客户进程ID的名字。spa

#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>

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

/*
 * Create a client 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());
printf("file is %s\n", un.sun_path);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);

    unlink(un.sun_path);        /* in case 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);
}

首先是建立一个套接字,而后用客户端进程ID附加到路径上,造成本身的套接字路径,而且将这个套接字绑定在地址上,也就是系统会在生成客户端的socket文件,可能有人要奇怪了,为何不直接使用connect而是要bind地址,由于若是不绑定地址,咱们就没法区分链接是属于哪一个客户端进程的,也就是相似于网络上客户端特地绑定一个端口链接服务端。

小结

最后还有三节,实际上都是属于实际操做的内容了,因此这里就不讲了。这篇文章就算是最终的系列结尾,由于最后章节是项目源码解析了。因此就再也不讲述

相关文章
相关标签/搜索