暗无天日

=============>DarkSun的个人博客

进程间通讯

1 管道

1.1 管道的局限性

  • 管道只能保证是半双工的(即数据只能在一个方向流动),虽然有些系统提供了全双工管道
  • 管道只能在具有公共祖先的进程之间使用. 通常管道由父进程创建,以实现父子进程之间的通讯

1.2 pipe函数创建管道

#include <unistd.h>

/*  成功返回0,出错返回-1 */
int pipe(int* fd);

该函数通过参数fd返回两个文件描述符,其中fd[ 0 ]为读而打开,fd[ 1 ]为写而打开, 且fd[ 1 ]的输出为fd[ 0 ]的输入

单进程中的管道几乎没有任何用处,因为进程内部的数据交换根本不需要通过管道来进行. 通常调用pipe的进程会接着fork一个子进程,并在父进程和子进程两端分别关闭读或写的文件描述符,通过这种方式就能实现父子进程间的通讯了.

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
  int fd[2];
  pipe(fd);

  pid_t pid = fork();
  if(pid >0)                    /* 父进程 */
    {
      printf("parent running\n");
      fflush(stdout);
      close(fd[0]);             /* 关闭读fd,父进程负责发送消息 */
      write(fd[1],"this is a messae from parent\n",255);
      close(fd[1]);
      waitpid(pid,NULL,0);
    }
  else
    {
      printf("child running\n");
      fflush(stdout);
      close(fd[1]);
      if(dup2(fd[0],STDIN_FILENO) != STDIN_FILENO)
        {
          printf("dup2 error\n");
          return -1;
        }
      close(fd[0]);
      if(execl("/bin/echo_from_stdin.sh","/bin/echo_from_stdin.sh",(char*)0) < 0)
        {
          printf("exec echo failed\n");
        }
      /* char s[255+1]=""; */
      /* printf("%s\n",gets(s)); */
    }
  return 0;
}

child running parent running

this is a messae from parent

1.3 对管道的读写说明

  • 读取一个写端已关闭的管道时,在所有剩余数据被读取后,read返回0,
  • 若写一个读端已关闭的管道时,则产生信号SIGPIPE. 若忽略该信号或捕捉该信号并从其处理函数返回,则write返回-1,errno设为EPIEP
  • 写管道(或FIFO)时,常量PIPE_BUF规定了内核中管道缓冲区的大小. 若对管道调用write的字节<=PIPE_BUF,则可以保证 该操作不会与其他进程对同一管道或FIFO的write操作穿插到一起. 否则写的数据可能穿插.

1.4 popen/pclose函数

这两个函数用于进程用system运行外部命令,并创建管道与该外部命令子进程进行交流

#include <stdio.h>

/* 若type为"r",则返回连接到cmd标准输出的FILE* */
/* 若type为"w",则返回连接到cmd标准输入的FILE* */
FILE* popen(const char* cmd,const char* type);

/* 关闭标准IO留,等待popen中cmd运行结束,再返回其终止状态,出错则返回-1 */
int pclose(FILE* fp);
  • popen只能连接到子进程的标准输入或标准输出,而无法通过type="rw"实现同时连接输入和输出

上面pipe的例子可以改写为:

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
  FILE* fp = popen("/bin/echo_from_stdin.sh","w");
  fputs("message from parent\n",fp);
  pclose(fp);
  return 0;
}

message from parent

1.5 使用管道时的注意事项

当用标准IO读写管道时,一定要注意 标准IO的缓冲机制. 缓冲很容易造成读写管道时的阻塞,甚至引起死锁.为此经常要用`setvbuf'函数更改缓冲类型.

当有多个进程同时对一个管道进行写操作时, 若一次写入的字节数大于PIPE_BUF,则可能会乱序写入.

2 FIFO

FIFO又叫命名管道,它与pipe的不同在于:pipe只能由共同祖先进程创建,然后通过fork函数在多个子进程之间使用. 而通过FIFO,无共同祖先进程的进程也能交换数据.

FIFO也是一种文件类型,stat结构中的st_mode可以标明文件是否为FIFO类型. 可以用S_ISFIFO宏对此进行测试.

2.1 FIFO的局限性

FIFO只能保证是半双工的.

2.2 创建FIFO

创建FIFO的类似于创建文件.

#include <sys/stat.h>

/* mode参数说明与open函数相同 */
int mkfifo(const char* fifo_path,mode_t mode);

2.3 使用FIFO

一旦用mkfifo创建了fifo,就可以使用open,close,read,write,unlink对其像处理文件一样使用FIFO.

但使用open打开FIFO时,非阻塞标志(O_NONBLOCK)的意义有点不同:

  • 若打开FIFO时未指定非阻塞标志,则只读open会阻塞到其他进程为写而打开次FIFO. 类似的,只写open会阻塞到其他进程为读而打开它.
  • 若打开FIFO时指定了非阻塞表示,则 只读open立即返回,但若 没有进程已经为读而打开FIFO,只写open会立即返回-1,errno为ENXIO.

2.4 对FIFO的读写说明

  • 若用write写一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE
  • 若某个FIFO的最后一个写进程关闭了该FIFO,则读取该FIFO会生产一个eof
  • 类似pipe,常量PIPE_BUF说明了可被原子地写到FIFO的最大数据量.

3 XSI IPC

XSI IPC包括三种IPC:信息队列,信号量,共享存储器. 它们之间有许多相似之处.

3.1 共同特征

3.1.1 标识符和键

每种XSI IPC结构中都有一个非负整数作为标识符. 但该标识符仅为IPC对象内部使用,其对外与一个键(key)相连.

创建IPC结构时都需要指定一个键(key_t),该键由内核变换为IPC结构的标识符.

多个进程共享同一个IPC结构一般有如下几种方法:

  • 某进程指定键为IPC_PRIVATE以创建一个新IPC结构,并将返回的标识符存放在数据库或文件中,以便其他进程取用.
  • 定义一个多个进程间都认可的键,然后进程使用该键来生成或获取IPC结构.
  • 通过函数`ftok'可以指定一个路径和一个id,并根据这个路径及id生成一个键值,多个进程间约定同一个路径和id以产生一个一致的IPC键,共享IPC结构.

    #include <sys/ipc.h>
    
    key_t fork(const char* path,int id);
    
    • 参数path必须是一个已存在的文件.
    • 产生键时,只使用 id参数的低8位
    • 如果参数id一致,即使不同的path参数也可能产生相同的键

3.1.2 权限结构

XSI IPC为每个IPC结构都设置了一个ipc_perm结构, 该结构规定了权限和所有者. 它至少包括下列成员:

#include <sys/ipc.h>

struct ipc_perm{
  uid_t uid;                    /* 所有者的有效用户id */
  gid_t gid;                    /* 所有者的有效组id */
  uid_t cuid;                   /* 创建者的有效用户id */
  git_t cgid;                   /* 创建者的有效组id */
  mode_t mode;                  /* access mode */
  /* 其他成员 */
};
  • mode成员类似文件的mode,但不存在执行权限.

3.1.3 XSI IPC的优缺点:

IPC的缺点有:

  • IPC结构是系统范围内都可访问,且即使读写的进程终止,消息与队列依然存在
  • IPC没有被抽象为一个文件,因此无法用IO函数操作它,也因此新增了十几条系统调用(msgget,semop,shmat等)
  • 由于IPC不使用文件描述符,因此无法对他们使用多路转接IO函数(select和poll). 这就难于一次使用多个IPC结构,以及在文件或设备IO中使用IPC结构.

IPC的优点有:

  • 可靠,因为这些IPC被限制在只能单机使用
  • 流是受控的,即当系统资源短缺(缓冲区满了),或接收进程能再接收更多消息,则发送进程会休眠. 当流控制条件消失,发送进程自动被唤醒
  • 面向记录
  • 可以用非先进先出方式处理.

3.2 消息队列(不推荐使用)

消息队列是消息的链接表,该链接表存放在内核中并由消息队列标识符标识.

3.2.1 打开/创建新消息队列

msgget用于创建一个新队列或打开一个现存的队列.

#include <sys/msg.h>

/* 成功则返回消息队列的ID,出错则返回-1 */
int msgget(key_t key,int flag);

每个消息队列都有一个msgid_ds结构体与之关联,用于说明队列的当前状态

struct msqid_ds{
  struct ipc_perm msg_perm;     /* ipc的权限说明 */
  msgqnum_t msg_qnum;           /* 队列中的消息个数 */
  msglen_t msg_qbytes;          /* 队列中能存储的消息最大容量 */
  pid_t msg_lspid;              /* 最后一次在该队列上调用msgsnd()的进程id */
  pid_t msg_lrpid;              /* 最后一次在该队列上调用msgrcv()的进程id */
  time_t msg_stime;             /* 最后一次在该队列上调用msgsnd()的时间 */
  time_t msg_rtime;             /* 最后一次在该队列上调用msgrcv()的时间 */
  time_t msg_ctime;             /* 最后一次在该队列发生变化的事件 */
  /* 其他结构成员 */
}

3.2.2 操作消息队列

msgctl函数对消息队列执行队中操作

#include <sys/msg.h>

int msgctl(int queueid,int cmd,struct msgid_ds* buf);

其中cmd参数说明了对queueid指定的队列要执行的命令:

  • IPC_SET

    按buf结构中的值,设置与消息队列msqid_ds结构中的下列四个字段:

    • msg_perm.uid
    • msg_perm.gid
    • msg_perm.mode
    • msg_qbytes

    执行该命令的进程,其有效用户ID必须等于msg_perm.cuid或msg_perm.uid或超级用户.

    只有超级用户才能增加msg_qbyes的值

  • IPC_RMID

    删除消息队列中的所有消息.

    执行该命令的进程,其有效用户ID必须等于msg_perm.cuid或msg_perm.uid或超级用户.

  • IPC_STAT

    取此消息队列的msgid_ds结构,并放在buf所指向的结构中

    #include <sys/msg.h>
    #include <sys/ipc.h>
    
    void show_queue_ds(const struct msqid_ds* queue_ds)
    {
      printf("msg_qnum=%d\n",queue_ds->msg_qnum);
      printf("msg_qbytes=%d\n",queue_ds->msg_qbytes);
      printf("msg_lspid=%d\n",queue_ds->msg_lspid);
      printf("msg_lrpid=%d\n",queue_ds->msg_lrpid);
    
    }
    int main(int argc, char *argv[])
    {
      int queueid = msgget(IPC_PRIVATE,IPC_CREAT);
      struct msqid_ds queue_ds;
      msgctl(queueid,IPC_STAT,&queue_ds);
      show_queue_ds(&queue_ds);
      return 0;
    }
    
    

    msg_qnum=1630404681 msg_qbytes=1627419888 msg_lspid=1628099819 msg_lrpid=192

3.2.3 收发消息

msgsnd将新消息添加到队列尾端.

#include <sys/msg.h>

/* 参数flag可以为0或IPC_NOWAIT. */
int msgsnd(int msqid,const msgbuf* ptr,size_t nbytes,int flag);

struct msgbuf
{
  long mtype;                   /* 消息类型 */
  char mtext[TEXTSIZE];                /* 信息数据,TEXTSIZE需要比msgsnd中的参数nbytes大 */
}

默认情况下,若消息队列已满,msgsnd函数会阻塞直到有空间要发送,或该队列被删除(返回EIDRM),或捕捉到一个信号,从信号捕捉函数返回(返回EINTR) 但若参数flag设置为IPC_NOWAIT,则msgsnd会立即出错返回EAGAIN.

msgrcv用于从队列中取消息.

#include <sys/msg.h>

/* 成功返回消息的数据部分的长度,出错返回-1 */
ssize_t msgrcv(int msqid,msgbuf* ptr,size_t nbytes,log type,int flag);

参数type指明了我们想获得哪种类型的消息

type == 0 返回队列中的第一个消息
type > 0 返回队列中消息类型为type的第一个消息
type < 0 返回队列中消息类型<=type绝对值的消息,类型值最小的消息优先(可作为优先级来用)

参数flag控制了msgrcv的行为

flag的值 说明
MSG_NOERROR 若消息大于nbytes,则消息被截断,否则会出错返回E2BIG(消息仍留在队列中)
IPC_NOWAIT 操作不阻塞,若找不到指定类型的消息,则返回-1,errno设置为ENOMSG
   

3.3 信号量集合

信号量是一个计数器,用于标明多进程间共享资源的剩余数量.

每个信号量集合由一个无名结构体表示

struct {
  unsigned short semval;        /* 信号量的值 */
  pid_t sempid;                 /* 上次操作该信号量的进程id */
  unsigned short semncnt;       /* 等待semval>curval的进程数 */
  unsigned short semzcnt;       /* 等待semval == 0的进程数 */
  /* 可能还有其他成员 */
};

3.3.1 新建/获取信号量集合

#include <sys/sem.h>

int semget(key_t key,int sem_num,int flag);
  • 若创建新信号量集合,则参数sem_num表示该信号量集合中包含了多少个信号量(编号从0到max_num-1)
  • 若获取一个现存的信号量,则将sem_num设置为0

3.3.2 操作信号量

  • semctl函数

    #include <sys/sem.h>
    
    /* 返回值的意义根据cmd的不同而不同 */
    int semctl(int semid,int semnum,int cmd);
    int semctl(int semid,int semnum,int cmd,union semun arg);
    
    union semnum{
      int val;                      /* cmd为SETVAL时 */
      struct semid_ds* buf;         /* cmd为IPC_STAT或IPC_SET时 */
      unsigned short* array;        /* cmd为GETALL或SETALL时 */
    };
    
    struct semid_ds
    {
      struct ipc_perm sem_perm;   /* ipc权限 */
      unsigned short sem_nsems;   /* 信号量集合中信号量的个数 */
      time_t sem_otime;           /* 最后执行semop()的时间 */
      time_t sem_ctime;           /* 最后改变semid_ds结构体的时间 */
      /* 其他成员 */
    };
    

    cmd参数说明

    IPC_STAT 取当前信号量集合的semid_ds结构,存放在arg.buf中
    IPC_SET 根据arg.buf中的值,更新该信号量集合的sem_perm.uid,sem_perm.gid或sem_perm.mode. 执行该函数的进程其有效用户ID必须为sem_perm.cuid/sem_perm.uid或超级用户权限
    IPC_RMID 立即删除该信号量集合.执行该函数的进程其有效用户ID必须为sem_perm.cuid/sem_perm.uid或超级用户权限
    GETVAL 返回信号量集合中第semnum个信号量的值
    SETVAL 设置信号集合中第semnum个信号量的值,该值由arg.val指定
    GETPID 返回上次操作信号量的进程PID
    GETNCNT 返回信号量集合中第semnum个信号量的semncnt的值
    GETZCNT 返回信号量集合中第semnum个信号量的semzcnt的值
    GETALL 获取信号量集合中所有信号量的值,并将它们存放在arg.array中
    SETALL 根据arg.array中的值设置信号量集合中所有信号量的值
  • semop函数 semop提供了操作信号量集合上各信号量值的原子操作

    #include <sys/sem.h>
    
    /* 成功返回0,失败返回-1 */
    int semop(int semid,struct sembuf semop_array[],size_t npos);
    
    struct sembuf
    {
      unsigned short sem_num;       /* 指明操作的是信号量集合中的第几个信号量 */
      short sem_op;                 /* 是何种操作,可以为负数,0或正数 */
      short sem_flg;                /* IPC_NOWAIT,SEM_UNDO */
    };
    
  • 参数npos指明了只按照semop_array中操作的数量.
  • sem_op的值说明:

    sem_op的值 说明
    >0 表示资源要释放资源. 增加信号量sem_op的值
    <0 表示进程要占用资源. 减少信号量值, 若信号量不够减,则操作被阻塞
    0 表示进程等待信号量变为0. 不为0则该操作被阻塞
  • sem_flg的值说明

    sem_flg的值 说明
    IPC_NOWAIT 不阻塞,若操作无法完成,则直接返回EAGAIN
    SEM_UNDO 修改进程的 信号量调整值. 进程在退出时,内核会根据该调整值释放资源. 推荐使用

3.4 共享存储器

共享存储器运行多个进程共享同一块存储区,由于数据不需要在进程间复制,所以这是最快的一种IPC.

由于不同进程要对同一块存储区操作,因此通常会使用记录锁或信号量来实现对共享存储区访问的同步.

内核为每个共享存储段设置了一个shmid_ds结构

struct shmid_ds{
  struct ipc_perm shm_perm;     /* IPC权限说明 */
  size_t shm_segsz;             /* 共享存储的大小 */
  pid_t shm_lpid;               /* 最后执行shmop()的进程ID */
  pid_t shm_cpid;               /* 创建该共享存储的进程ID */
  shmatt_t shm_nattch;          /* 当前attach到该共享内存的数量 */
  time_t shm_atime;             /* 最后一次attach的时间 */
  time_t shm_dtime;             /* 最后一次detach的时间 */
  time_t shm_ctime;             /* 最后一次更改shmid_ds结构的时间 */
  /* 其他成员 */
}

3.4.1 创建/获取共享存储

#include <sys/shm.h>

/* 成功返回共享存储ID,出错返回-1 */
int shmget(key_t key,size_t size,int flag);
  • 若创建的共享存储,则参数size指明了共享存储的大小,通常需要我诶系统页长的整数倍, 否则最后一页的余下部分是不可用的
  • 若获取原共享存储,则参数设置为0
  • 创建新的共享存储后,会自动初始化存储内容为0

3.4.2 操作共享存储段

#include <sys/shm.h>

int shmctl(int shmid,int cmd,struct shmid_ds* buf);

cmd参数说明:

cmd参数 说明
IPC_STAT 去该段的shmid_ds结构,存放在buf所指的结构中
IPC_SET 根据buf中的值设置该共享存储的shm_perm.uid,shm_perm.gid和shm_perm.mode
IPC_RMID 从系统中删除该存储段. 共享存储有一个计数器,只有当最后一个进程删除该共享存储后才回实际删除
SHM_LOCK 不让该共享存储交换到swap中
SHM_UNLOCK 允许共享存储交换到swap中

3.4.3 使用共享内存

  • attach共享内存

    #include <sys/shm.h>
    
    /* 成功则返回指向共享存储的指针,出错返回-1 */
    void* shmat(int shmid,const void* addr,int flag);
    
    • 若addr==0,则该共享存储连接到内核选择的第一个可用地址上 推荐使用
    • addr非0,flag没有指定SHM_RND,则该共享存储连接到addr指定的地址上.
    • addr为0,flag指定了SHM_RND,则会该addr取整处理,再将该共享内存挂到取整后的地址上
    • flag指定了SHM_RDONLY则表示以只读方式挂载共享内存,默认为读写方式.
  • detach共享内存 进程对共享存储段的操作结束后,调用shmdt脱节该段. 若成功,则shmdt会将shmid_ds结构体重的shm_nattach计数器减少1

    #include <sys/shm.h>
    
    int shmdt(void* addr);
    

4