UNIX 进程间通讯(IPC)概念(Posix,System V IPC)

   IPC(Inter-Process Communication,进程间通讯)可以有三种信息共享方式(随文件系统,随内核,随共享内存)。(当然这里虽然说是进程间通讯,其实也是可以和线程相通的)。

       相对的IPC的持续性(Persistence of IPC Object)也有三种:

  1. 随进程持续的(Process-Persistent IPC)

    IPC对象一直存在,直到最后拥有他的进程被关闭为止,典型的IPC有pipes(管道)和FIFOs(先进先出对象)

  2. 随内核持续的(Kernel-persistent IPC)

    IPC对象一直存在直到内核被重启或者对象被显式关闭为止,在Unix中这种对象有System v 消息队列,信号量,共享内存。(注意Posix消息队列,信号量和共享内存被要求为至少是内核持续的,但是也有可能是文件持续的,这样看系统的具体实现)。

  3. 随文件系统持续的(FileSystem-persistent IPC)

    除非IPC对象被显式删除,否则IPC对象会一直保持(即使内核才重启了也是会留着的)。如果Posix消息队列,信号量,和共享内存都是用内存映射文件的方法,那么这些IPC都有着这样的属性。

  不同的Unix IPC的持续性:

  1. 随进程:

    Pipe, FIFO, Posix的mutex(互斥锁), condition variable(条件变量), read-write lock(读写锁),memory-based semaphore(基于内存的信号量) 以及 fcntl record lock,TCP和UDP套接字,Unix domain socket

  2. 随内核:

    Posix的message queue(消息队列), named semaphore(命名信号量), System V Message queue, semaphore, shared memory。

  要注意的是,虽然上面所列的IPC并没有随文件系统的,但是我们就像我们刚才所说的那样,Posix IPC可能会跟着系统具体实现而不同(具有不同的持续性),举个例子,写入文件肯定是一个文件系统持续性的操作,但是通常来说IPC不会这样实现。很少有IPC会实现文件系统持续,因为这会降低性能,不符合IPC的设计初衷。

       Unix的IPC有一些是有名的,有一些是无名的,到具体使用的时候就知道了,如果是无名IPC(典型是Pipe),必须是依赖于进程的,但是有名IPC(典型是FIFOs),就可以使用在两个没有依赖性的进程上(依赖性可以表现在,一个进程是另一个进程的子进程)。

Posix IPC


       Posix的全称是 "Portable Operating System Interface",Posix不仅仅是一个单一标准,而且是IEEE(Institute for Electrical and Electronics Engineers, Inc. IEEE)指定的一个标准族。
       Posix IPC一共有三个,就是Message Queue(消息队列),semaphores(信号量),Shared Memory(共享内存),在Posix.1中,这三个IPC的命名规则为:

  • 必须符合已有的路径名规则(必须最多由PATH_MAX个字节构成,包括结尾的空字符)。
  • 如果它以斜杠开头,那么对这些函数的不同调用将访问同一个队列,如果它不以斜杠符开头,那么效果取决于实现
  • 名字中的额外斜杠符的解释由实现来定义。

       由于Unix系统不同发行版的命名系统的规则都很不一样,所以使用Posix的时候需要注意命名规则。为了预防这种移植性问题,Unix系统给了定义了三个宏:

S_TPYEISMQ(buf)
S_TYPEISSEM(buf)
S_TYPEISSHM(buf)

       这三个宏的作用就是为了检测当前系统是否有着对Posix IPC(message queue, semaphores, shared memory)的不同实现方式,buf是一个指向stat结构体的一个结构体(就是那个可以给fstat,lstat或者stat函数填充的那个缓冲结构)。如果当前系统对于特定的IPC有着不同的实现方式,那么对应的宏将会返回非0值,否则就会返回0。 
       然而这三个宏很少使用,因为没有一个系统保证这三个宏对应的Posix IPC(message queue, semaphores, shared memory)会在不同的系统有不同的实现方式。比如在Solaris2.6系统,这三个宏都是返回0的。

       我们可以使用下面的函数来新建一个Posix IPC的名字:

char *px_ipc_name(const char *name)  
{
    char *dir, *dst, *slash;
    if((dst = malloc(PATH_MAX)) == NULL)
    {
        if((dir = getenv("PX_IPC_NAME")) == NULL)
        {
            #ifdef POSIX_IPC_PREFIX
            dir = POSIX_IPC_PREFIX
            #else
            dir = "/tmp/";      
        }
    }
    slash = (dir[strlen(dir) - 1] == '/') ? "" : "/";
    snprintf(dst, PATH_MAX, "%s%s%s", dir, slash, name);
    return dst;
}   

Posix IPC的通道的打开与创建

       打开Posix的三种IPC通道其实用的是三个不同的函数mq_open(打开消息队列),sem_open(打开信号量),shm_open(打开共享内存),这三种个函数都可以用不同的打开方式来创建IPC通道,除了最平常的O_RDONLY(只读), O_WRONLY(只写), O_RDWR(可读可写)外,还有四个方式

  1. O_CREAT: 
    创建一个不存在的消息通道,信号量或者共享内存IPC,当创建一个新的消息队列,信号量,或者共享内存时,它们的userID会被设置为进程有效的userID。信号量,共享内存的groupID会被设置为进程有效的groupID或者是系统默认groupID;消息队列的groupID会被设置为进程的groupID。
  2. O_EXCL: 当这个标志和O_CREAT一起用的时候,只能创建不存在的消息通道,信号量或者共享内存IPC,如果所创建的IPC已经存在,创建函数(就是那三个函数)将会返回EEXIST错误。(注意单独的O_EXCL是没有意义的)
  3. O_NONBLOCK: 
    这个标志可以让消息队列读一个空的队列或者写一个满的队列。
  4. O_TRUNC 
    只作用于共享内存,如果共享内存以读写方式打开,那么创建的IPC将会以0的长度创建。

关于Posix IPC权限
       新的消息队列,有名信号量或者共享内存区的IPC对象是由oflags参数中含有O/_CREAT标志的mq_open,sem_open或者shm_open函数创建的,这些权限位与IPC类型的每个对象相关联,就像它们与每个Unix文件相关联一样(这个是很容易想象的,因为Unix就是把内存的操作对象映射到文件当中方便操作的。)

       当同样由这三个函数打开一个已经存在的消息队列,信号量或者共享内存对象的时候(或者未指定O_CREAT,或者制定了O_CREAT但没有指定O_EXCL,同时对象已经存在),将基于如下信息执行权限测试:

  1. 创建时赋予该IPC对象的权限位;
  2. 所请求的访问类型(O/RDONLY,O/WRONLY或者O_RDWR);
  3. 调用进程的有效用户ID,有效组ID以及各个辅助组ID(如果支持辅助组的话)

  

  大多数Unix内核按照如下步骤执行权限测试(如果如下步骤有哪一步不满足,那么其下面的步骤都不执行,操作视为失败)

  1. 如果当前进程的有效用户ID为0(superuser),那就允许访问
  2. 在当前进程的有效用户ID就等于该IPC对象的属主ID的前提下,如果相应的用户访问权限位已经设置,那就允许访问,否则拒绝访问。 
           这里的相应的访问权限位的意思是:如果当前进程为读访问而打开IPC对象,那么用户读权限位必须设置,如果当前进程为写访问而打开该IPC对象,那么用户写权限位必须设置
  3. 在当前进程的有效组ID或它的某个辅助组ID等于该IPC对象的组ID的前提下,如果相应的组访问权限位已经设置,那么就允许访问,否则拒绝访问。
  4. 如果相应的其他用户权限位已经设置,那么就允许访问,否则拒绝访问。

  

  

System V IPC


       System V IPC和Posix IPC其实是本质上是差不多的,不过需要注意的是,System V IPC不是随进程持续的,是随内核持续的。 Posix的IPC的名字可以像文件系统找文件一样找到它们的名字,但是System V IPC不可以找到它们(这些IPC)的名字。

key_t Keys和ftok Function

       System V IPC系统创建新的IPC的三个函数分别是msgget(),senget(),shmget()这三个函数的参数都是(key_t key, int mode),其实key是由ftok函数创建的一个键值,ftok函数的声明为:

#include <sys/ipc.h>

key_t ftok(const char *pathname,int id);

       这个函数假定了这个程序使用了System V IPC进行通讯,客户端和服务器端同意使用具有一定意义的pathname(是已存在的路径名),如果客户端和服务器只用单一通道,那么可以将id为1;如果客户端和服务器需要双向通道(而且是两条),那么可以令一个通道的id为1,另一个为2,只要pathname是一样的,那可以认为客户端和服务器使用是同一种通道进行通讯的。

  

  使用ftok函数内部实现是调用了stat函数,然后进行如下行为(典型实现,不是强制要求,一定要注意pathname是已存在路径):

  1. pathname所在文件系统信息(stat结构的st_dev)(12位)
  2. pathname对应的文件的在本文件系统的索引节点号(stat结构的st_ino)(12位)
  3. 低8位是id值(不能为0(8位)

        

   System V IPC不保证当不同路径时Keys是不一样的,id值绝对不能0(所以很多实现都把id值为0的键值定义为IPC_PRIVATE)。

ipc_perm Structure

       System V IPC中,由kernel维持一个IPC的信息结构,就像文件信息结构一样:(注意这个结构和书上的有点出入)

struct ipc_perm
{
    __key_t __key;          /* Key.  */
    __uid_t uid;            /* Owner's user ID.  */
    __gid_t gid;            /* Owner's group ID.  */
    __uid_t cuid;           /* Creator's user ID.  */
    __gid_t cgid;           /* Creator's group ID.  */
    unsigned short int mode;        /* Read/write permission.  */
    unsigned short int __pad1;
    unsigned short int __seq;       /* Sequence number.  */
    unsigned short int __pad2;
    __syscall_ulong_t __glibc_reserved1;
    __syscall_ulong_t __glibc_reserved2;
};

       System IPC V的两个标志位(IPC_CREAT, IPC_EXCL)的用法基本上和Posix的是一样的,注意System IPC V还多了个IPC_PRIVATE的标志位,这个标志位就是专门用来创建独立的IPC通道的(没有一种pathname和id的组合能创建IPC_PRIVATE)。

IPC权限

       事实上System V IPC的权限是由上面的所说的IPC_CREAT, IPC_EXCL和IPC_PRIVATE加上读写权限组成的,读写权限由系统定义的6个宏决定MSG_R, MSG_W, SEM_R, SEM_A, SHM_R, SHM_W共同组成的,具体怎么组成看下表:

  

       注意ipc_perm Structure的cuid、 uid创建IPC时会被设置为调用者的user id,cgid、 gid会被设置为调用者的组group id,唯一区别就是creator的ids是不允许被改变的,但是owner的ids是可以通过ctlXXX指令来改变的(ctLXXX指令对应于三种不同的IPC有着三个不同的函数,这三个函数不使用文件模式的掩码修改权限,而是设置为对应函数指定的准确的值)

       每当一个进程访问某个IPC对象时,IPC就执行两级检查,该IPC对象被打开(调用getXXX函数)执行一次,以后每次使用该对象时执行一次:

  1. 当每一个进程以某个getXXX函数建立访问某个已经存在的IPC对象的通道时,IPC就执行一次初始检查。验证调用者的oflag参数有没有指定不在该对象ipc_perm结构mode成员中的任何访问位。任何调用进程创建一个IPC时,如果其所指定的oflag对应权限位被禁止,该函数将会返回一个错误。但是其实这种测试是没用的,因为它假定调用者知道自己的权限范畴(用户,组成员或者其他用户),调用进程只用把oflag指定为0就可以绕过这个检查。
  2. 每次调用IPC的权限测试和Posix的权限检查方式一样。

  

identifier Reuse标识符重用

       ipc_perm结构还有一个名为seq的变量,它是一个槽位使用情况序列号,该该变量是一个由内核为系统中每个潜在的IPC对象维护的计数器,每当删除一个IPC对象时,内核就递增相应的槽位号,如果溢出则循环为0(注意这只是普通的SVR4实现,Unix98没有强制使用这个技巧,比如我在Ubuntu 16里面就没有这样的实现)。 
       为了防止恶意进程乱读取System V IPC来截获信息,IPC标识符的可能值被设计的非常大,当一个IPC表项被访问时,获取到的IPC值将增加一个IPC表项数,下面以一个进程为例:

int i, msqid;
for (i = 0; i < 10;i++)
{
    msqid = msgget((key_t)IPC_PRIVATE, SVMSG_MODE | IPC_CREAT);
    printf("msgid = %d\n",msqid);
    msgctl(msqid, IPC_RMID, NULL);
}
return 0;

  输出:

  

ipcs and ipcrm Programs

       由于System V IPC的三种类型不是以文件系统中的路径名标识的,所以标准的ls和rm程序无法看到他们,也没办法删除,不过任何实现了System V IPC的系统都提供了两个特殊的程序,ipcs和ipcrm,ipcs输出有关System V IPC特性的各种信息,ipcrm删除一个System V 消息队列,信号量或者共享内存区。

Kernel Limits

       System V IPC的多数实现有内在的内核限制,比如最大数目等,这些限制往往很小,但是还是可以修改的。

posted @ 2017-01-14 00:16  PhiliAI  阅读(3197)  评论(1编辑  收藏  举报