进程间的通信

进程间的通信主要分为本机器进程间的通信和不同机器间进程的通信。本文主要描述本机进程间的通信。

一、传统Linux的通信机制

1. 无名管道(PIPE)

无名管道是一种半双工通信方式,数据只能单方向流动,而且只能用于具有亲缘关系的进程,并且这种亲缘关系常常指的是父子进程。

pipe(建立管道):

头文件: #include<unistd.h>
函数的一般形式: int pipe(int filedes[2]);
函数说明:

    pipe()会建立管道,并将文件描述词由参数filedes数组返回。
           filedes[0]为管道里的读取端
           filedes[1]则为管道的写入端。

返回值: 若成功则返回零,否则返回-1,错误原因存于errno中。
错误代码:

    EMFILE: 进程已用完文件描述词最大量。
    ENFILE: 系统已无文件描述词可用。
    EFAULT: 参数 filedes 数组地址不合法。
// 案例:
#include <stdio.h>
#include <unistd.h> //pipe, read,write,fork
#include <stdlib.h> //exit
#include <string.h>
#define MAX_NUM 32
int main()
{
  int pipe_fd[2];
  int ret = -1;
  pid_t pid = -1;
  char message[MAX_NUM] = {'\0'}; 
  pipe(pipe_fd);
  write(pipe_fd[1], "HelloWorld", 10); //管道初始化
  pid = fork();
  if (0 == pid)
  {
    while(1)
    {
      memset(message, '\0', MAX_NUM);
      ret = read(pipe_fd[0], message, MAX_NUM); //读取管道中的信息
      if (-1 == ret)
      {
        perror("read:");
        exit(1);
      } 
      else if (0 == ret)
      {
        printf("There is nothing to say from child;\n");
      }
      else
      {
        printf("The child say: %s", memssage);
      }
      sleep(1);
      printf("parent input:\n");
      scanf("%s",message);
      ret = write(pipefd[1], message, strlen(message));
      if (-1 == ret)
      {
        perror("write:");
        exit(1);
      }
      sleep(1);
    }
  }
  else if (0 < pid)
  {
    while(1)
    {
      memset(message, '\0', MAX_NUM);
      ret = read(pipe_fd[0], message, MAX_NUM); //读取管道中的信息
      if (-1 == ret)
      {
        perror("read:");
        exit(1);
      } 
      else if (0 == ret)
      {
        printf("There is nothing to parent from child;\n");
      }
      else
      {
        printf("The parent say: %s", message);
      }
      sleep(1);
      printf("child input:\n");
      scanf("%s",message);
      ret = write(pipefd[1], message, strlen(message));
      if (-1 == ret)
      {
        perror("write:");
        exit(1);
      }
      sleep(1);
    }
  }
  else
  {
    perror("fork:");
    exit(1);
  }
  return 0;
}

2. 有名管道(FIFO)

有名管道同样是一种半双工通信方式,只不过它允许无亲缘关系的进程进行通信。
mkfifo():
头文件:#include <sys/types.h> #include <sys/stat.h>
函数的一般形式:int mkfifo( const char *pathname, mode_t mode );
函数说明:创建一个有名管道
参数设置:

    pathname:在文件系统中创建一个专用文件;
    mode:规定专用文件的读写权限。(读、写、执行权限)

返回值:成功返回0,失败返回 -1。
错误代码:

    EACCESS 参数pathname所指定的目录路径无可执行的权限
    EEXIST 参数pathname所指定的文件已存在。
    ENAMETOOLONG 参数pathname的路径名称太长。
    ENOENT 参数pathname包含的目录不存在
    ENOSPC 文件系统的剩余空间不足
    ENOTDIR 参数pathname路径中的目录存在但却非真正的目录。
    EROFS 参数pathname指定的文件存在于只读文件系统内。
// 案例:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#define PATHNAME "./myfile"
int main(void)
{
    char out_buf[80];
    int fd;
    int ret = -1;
    ret = mkfifo( PATHNAME, 0777 );  //创建有名管道
    pid_t pid = -1;
    pid = fork();
    if (0 == pid)
    {
        char input_buf[] = "Hello!\n";
        fd = open( PATHNAME, O_WRONLY );
        ret = write( fd, input_buf, strlen(input_buf) );
        close( fd );
    }
    else
    {
        fd = open( PATHNAME, O_RDONLY);
        ret = read( fd, out_buf, sizeof(buf) );
        printf("The message from the FIFO is:%s\n", out_buf );
        close( fd );
    }
    return 0;
}

3. 信号(signal)

信号的API
1. 传送信号给指定进程

kill()
头文件: #include <sys/types.h> #include <signal.h>
函数的一般形式int kill(pid_t pid,int sig)
函数功能:该函数可以将参数sig指定的信号传给参数pid指定的进程.
参数设置:

  pid:
    1. pid>0 :将信号传给进程识别码为pid的进程.
    2. pid=0 :将信号传给和当前进程相同进程族的所有进程.比如父进程产生子进程后,父子进程均属于一个进程组.
    3. pid=-1:将信号广播传送给系统中所有的进程.
    4. pid<0 :将信号传给进程组识别码为pid绝对值的所有进程.
  sig: 传递的信号.由于信号在linux中以一个数字编码相对应,因此该参数为int型.

返回值:成功返回0,失败返回-1。
错误代码:

EINVAL 一个无效的信号;
EPERM  该进程没有权限发送信号给目标进程;
ESRCH  pid或者process group不存在。
// 案例:主进程向子进程发信号以终止子进程.
   #include <stdio.h>
   #include <stdlib.h>
   #include <unistd.h>
   #include <string.h>
   #include <sys/stat.h>
   #include <sys/types.h>
   #include <signal.h>
   int main(void)
  {
     pid_t pid;
     pid=fork();
     if(pid<0)
        perror("fork");
     else if(0==pid)
    {
        while(1)
       {
           printf("I am child process\n");
           sleep(1);
       }
     }
  else
  {
      sleep(3);
      printf("I am a father process, i will send signal now\n");
      kill(pid,SIGINT);   //也可以写成:kill(pid,2)
  }
  return 0;
 }
2. 向自己发送一信号

raise()
头文件:#include<signal.h>
函数的一般形式:int raise(int sig)
函数功能:该函数可以向自己发送一个sig信号.当然也可以使用kill()函数来实现.
返回值:成功返回0,失败返回一个非零值。

// 案例:
   int main()
   {
      int i=0;
      while(1)
      {
         i++;
         if(i==3)
            raise(SIGINT);
         printf("i am father process\n");
         sleep(1);
      }
      return 0;
   }

执行结果:当i=3时,函数raise()向自己进程发送了终止信号,因此只打印两条信息.
i am father process
i am father process

在linux的64个信号中,大多数在默认情况下都是终止当前信号。包括SIGINT,当到了定时时间后,内核发出SIGINT信号,该信号会终止当前进程。

3. 设置信号传送闹铃

alarm()
头文件:#include<unistd.h>
函数的一般形式:unsigned int alarm(unsigned int seconds)
函数功能:用于设置信号SIGALRM经过参数seconds所指定的秒数后传送给当前进程。
返回值:返回剩余的秒数,直到任何之前预定的报警是由于交付,或者零如果没有previ在预定的报警。

// 案例:
   int main(void)
   {
      if(alarm(3)<0) 
      //当调用该函数时,系统就启动了定时器,定时到3s后向当前进程发送一个SIGALRM信号.
      //该信号默认情况下是终止当前进程.
      perror("alarm");
      while(1)
      {
          sleep(1);
          printf("i am father process\n");
      }
      return 0;
   }

执行结果:
i am father process
i am father process

在linux的64个信号中,大多数在默认情况下都是终止当前信号.包括SIGALRM,当到了定时时间后,内核发出SIGALRM信号,该信号会终止当前进程.

4. 让进程暂停直到信号出现

pause()
头文件:#include<unistd.h>
函数的一般形式:int pause(void)
函数功能:用于将调用进程挂起直到捕捉到信号为止.该信号可以判断信号是否已到。
返回值:只有当一个信号被signal-catching函数返回。在这种情况下暂停()返回 -1,errno设置捕获。
错误代码:

EINTR 捕获一个信号被signal-catching函数返回
// 案例:
   int main(void)
   {
      pid_t pid = -1;
      pid = fork();
      if(pid<0)
         perror("fork");
      else if(0 == pid)
      {
         if(pause()<0)
            perror("pause");
         while(1)
         {
            printf("hello i am child process\n");
            sleep(1);
         }
      }
      else
      {
            sleep(3);
            printf("I am a father process, I will send signal now\n");
            kill(pid,SIGINT);
      }
      return 0;
   }

执行结果:
系统在延迟3s后打印输出"i am a father process,i will send signal now",然后结束当前进程.
注意,程序并不会打印输出"hello i am child process".

在linux的64个信号中,大多数在默认情况下都是终止当前信号.包括SIGALRM,当到了定时时间后,内核发出SIGALRM信号,该信号会终止当前进程

信号的处理函数
在linux中,对于信号的处理主要有两种.一种是使用signal()函数,另一种是使用信号集函数组。
1. 使用signal()函数

头文件:#include<signal.h>
函数原型:

void (*signal(int signo,void (*func)(int)))(int)
sighandler_t signal(int signum, sighandler_t handler);

函数功能: 该函数会根据函数指针func所指向的函数来处理参数signo所指定的信号。
参数设置:

第一个参数:一个整数
第二个整数:是函数指针,它所指向的就是需要处理该信号的函数。

返回值:是一个函数地址,返回上一次的信号处理函数的指针.若出错,则返回SIG_ERR.

 // 案例:
     void my_func1(int sign_no)
     {
        if(sign_no == SIGINT)
           printf("ni hao\n");
        else if(sign_no == SIGQUIT)
           printf("hello world \n");
     }
     void my_func2(int sign_no)
     {
        if(sign_no == SIGINT)
           printf("love china\n");
        else if(sign_no == SIGQUIT)
           printf("kill japanese\n");
      }
      int main()
      {  
         void (*p)(int sig_no);         //定义参数为int型的函数指针p
         printf("Waiting for SIGINT of SIGQUIT\n");
         signal(SIGINT, my_func1);       //使用函数my_func1来处理信号SIGINT
         pause();              //等待用户输入SIGINT类的信号,比如ctrl+c
         p=signal(SIGINT,my_func2);    
        //使用函数my_func2来处理信号SIGINT,并将返回值赋给p
        //signal()返回上一次信号处理函数的指针,而上一次处理函数是my_func1,因此p指向函数my_func1.
         pause();                      
        //等待用户输入SIGINT类的信号,比如ctrl+c
         signal(SIGQUIT,p);     
        //使用函数my_func1来处理信号SIGQUIT
         pause();
         exit(0); 
      }

执行结果:
Waiting for SIGINT of SIGQUIT
ni hao //按下ctrl+c (SIGINT类信号)
love china //按下ctrl+c (SIGINT类信号)
hello world //按下ctrl+\ (SIGQUIT类信号)

该函数的第二个参数func是信号处理函数,它有3中写法:
1、SIG_IGN :忽略该信号
2、SIG_DEL :采用默认方式处理该函数.在linux中多数信号信号的默认是终止当前进程.
3、自定义信号处理函数 :如上例所示.

2. 信号集函数组

在上文中的"使用signal()函数来处理信号",每次只能处理一个信号.如果批量处理,则会显得非常麻烦.因此,在linux中就支持信号集函数组来批处理信号.这是我自己的理解。当然,为了支持信号集处理,需要下面几个函数:

  1. int sigemptyset(sigset_t *set)
    初始化信号集合set,并将其设置为空,即信号集合中无信号.
  2. int sigfillset(sigset_t *set)
    初始化信号集合,将信号集合设置为所有信号的集合.比如将linux中的64个信号都放在信号集合set内.
  3. int sigaddset(sigset_t *set, int signo)
    将信号signo加入到信号集合set中.
  4. int sigdelset(sigset_t *set, int signo)
    将信号signo从信号集中删除.
  5. int sigismember(sigset_t *set int signo)
    查询信号signo是否在信号集合set中.
  6. int sigprocmask(int how, const sigset_t *set, sigset_t *oset)
    将指定的信号集合加入到进程的信号阻塞集合中,或从中删除.
参数设置:
    第一个参数how决定了函数的操作方式:
        SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合中.
        SIG_UNBLOCK:从当前阻塞集合中删除一个信号集合.
        SIG_SETMASK:将当前信号集合设置为信号阻塞集合.
    第二个参数set:阻塞信号的集合.
    第三个参数oset:若提供了oset,则当前进程信号阻塞集合将会保存在oset中.若不想保存,
    则设置为NULL.
// 案例:将信号SIGINT加入到阻塞信号集合中,再将信号SIGINT从阻塞信号集合中删除.
      int main(void)
     {
         sigset_t intmask; //设置阻塞集合intmask
         int i;
         sigemptyset(&intmask); //初始化阻塞集合,并清空
         //将信号SIGINT加入到阻塞集合中,即ctrl+c(SIGINT信号的一种)不再有效.
         sigaddset(&intmask,SIGINT);  
         while(1)
        {
           fprintf(stdout,"SIGINT signal blocked\n");
           //添加阻塞集合intmask到系统的阻塞集合中
           sigprocmask(SIG_BLOCK,&intmask,NULL); 
           for(i=0;i<10;i++) 
           //每秒打印出字符串,在此期间执行ctrl+c(SIGINT信号的一种),程序无法停止
          //SIGINT信号被加入了系统的阻塞集合中,因此系统将不会对该信号作出反应
          {
              fprintf(stdout,"Blocked calculation is finished\n");
              sleep(1);
          }
        fprintf(stdout,"SIGINT signal unblocked\n");
        //将阻塞集合intmask从系统的阻塞集合中删除
        sigprocmask(SIG_UNBLOCK,&intmask,NULL); 
        for(i=0;i<10;i++) 
        //每秒打印出字符串,在此期间执行ctrl+c(SIGINT信号的一种),程序可以停止
        //SIGINT信号从了系统的阻塞集合中删除了,因此系统对该信号将作出反应
        {
            fprintf(stdout,"Unblocked calculation is finished\n");
            sleep(1);
        }
     }
     exit(0);
    }  

二、System V IPC:

1. 共享内存( shared memory)

共享内存是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

注意:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。

  1. shmget()
    头文件:#include <sys/ipc.h> #include <sys/shm.h>
    函数的一般形式:int shmget(key_t key, size_t size, int shmflg);
    函数说明:创建共享内存
    参数设置:
key:为有效的共享内存段命名
size:以字节为单位指定共享内存的容量大小
shmflg:权限标志,和open函数的mode参数一样;如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。

返回值:成功返回一个共享内存标识符(非负整数),失败返回 -1。

  1. shmat()
    头文件: #include <sys/types.h> #include <sys/shm.h>
    函数的一般形式:
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

函数说明:第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
参数设置:

shmid:共享内存标识符。
shmaddr:指定共享内存连接到当前进程中的地址位置,通常为NULL,表示让系统来选择共享内存的地址。
shmflg:一组标志位,通常为0。

返回值:成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1。

  1. shmdt()
    头文件:#include <sys/types.h> #include <sys/shm.h>
    函数的一般形式:
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

函数说明:用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。
参数设置:

shmid:共享内存标识符。
shmaddr:指定共享内存连接到当前进程中的地址位置,通常为NULL,表示让系统来选择共享内存的地址。
shmflg:一组标志位,通常为0。

返回值:成功返回0,失败返回 -1。
错误代码:

  1. shmctl()
    头文件:#include <sys/ipc.h> #include <sys/shm.h>
    函数的一般形式:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    函数说明:用来控制共享内存
    参数设置:
shmid:共享内存标识符
cmd:command是要采取的操作,它可以取下面的三个值 :
      IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
      IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
      IPC_RMID:删除共享内存段
buf:是一个结构指针,它指向共享内存模式和访问权限的结构。
// shmid_ds 的结构体
    struct shmid_ds
    {
        struct ipc_perm shm_perm;/* 操作权限*/
        int shm_segsz;             /*段的大小(以字节为单位)*/
        time_t shm_atime;          /*最后一个进程附加到该段的时间*/
        time_t shm_dtime;          /*最后一个进程离开该段的时间*/
        time_t shm_ctime;          /*最后一个进程修改该段的时间*/
        unsigned short shm_cpid;   /*创建该段进程的pid*/
        unsigned short shm_lpid;   /*在该段上操作的最后1个进程的pid*/
        short shm_nattch;          /*当前附加到该段的进程的个数*/
        /*下面是私有的*/
        unsigned short shm_npages;  /*段的大小(以页为单位)*/
        unsigned long *shm_pages;   /*指向frames->SHMMAX的指针数组*/
        struct vm_area_struct *attaches; /*对共享段的描述*/
    };
    struct ipc_perm 
    {
        key_t          __key; /*Key supplied to shmget(2)*/
        uid_t          uid;   /*Effective UID of owner*/
        gid_t          gid;   /*Effective GID of owner*/
        uid_t          cuid;  /*Effective UID of creator*/
        gid_t          cgid;   /*Effective GID of creator*/
        unsigned short mode;  /*Permissions + SHM_DEST and SHM_LOCKED flags*/
        unsigned short __seq; /*Sequence number*/
           };

返回值:失败返回 -1。

// shmdata.h
#ifndef _SHMDATA_H
#define _SHMDATA_H

#define SIZE 1024
struct chm_mes
{
  int shm_num;      //作为一个标志,非0:表示可读,0表示可写
  char text[SIZE];  //记录写入和读取的文本
};
#endif
// shmread.c
#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>  
#include "shmdata.h"  
  
int main()  
{  
    int running = 1;//程序是否继续运行的标志  
    void *shm = NULL;//分配的共享内存的原始首地址  
    struct shm_mes *shared;//指向shm  
    int shmid;//共享内存标识符  
    //创建共享内存  
    shmid = shmget((key_t)1234, sizeof(struct shm_mes), 0666|IPC_CREAT);  
    if(shmid == -1)  
    {  
        fprintf(stderr, "shmget failed\n");  
        exit(EXIT_FAILURE);  
    }  
    //将共享内存连接到当前进程的地址空间  
    shm = shmat(shmid, 0, 0);  
    if(shm == (void*)-1)  
    {  
        fprintf(stderr, "shmat failed\n");  
        exit(EXIT_FAILURE);  
    }  
    printf("\nMemory attached at %X\n", (int)shm);  
    //设置共享内存  
    shared = (struct chm_mes*)shm;  
    shared->shm_num = 0;  
    while(running)//读取共享内存中的数据  
    {  
        //没有进程向共享内存定数据有数据可读取  
        if(shared->shm_num != 0)  
        {  
            printf("You wrote: %s", shared->text);  
            sleep(rand() % 3);  
            //读取完数据,设置shm_num使共享内存段可写  
            shared->shm_num = 0;  
            //输入了end,退出循环(程序)  
            if(strncmp(shared->text, "end", 3) == 0)  
                running = 0;  
        }  
        else//有其他进程在写数据,不能读取数据  
            sleep(1);  
    }  
    //把共享内存从当前进程中分离  
    if(shmdt(shm) == -1)  
    {  
        fprintf(stderr, "shmdt failed\n");  
        exit(EXIT_FAILURE);  
    }  
    //删除共享内存  
    if(shmctl(shmid, IPC_RMID, 0) == -1)  
    {  
        fprintf(stderr, "shmctl(IPC_RMID) failed\n");  
        exit(EXIT_FAILURE);  
    }  
    exit(EXIT_SUCCESS);  
} 
// shmwrite.c
#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <string.h>  
#include <sys/shm.h>  
#include "shmdata.h"  
  
int main()  
{  
    int running = 1;  
    void *shm = NULL;  
    struct chm_mes *shared = NULL;  
    char buffer[SIZE + 1];//用于保存输入的文本  
    int shmid;  
    //创建共享内存  
    shmid = shmget((key_t)1234, sizeof(struct chm_mes), 0666|IPC_CREAT);  
    if(shmid == -1)  
    {  
        fprintf(stderr, "shmget failed\n");  
        exit(EXIT_FAILURE);  
    }  
    //将共享内存连接到当前进程的地址空间  
    shm = shmat(shmid, (void*)0, 0);  
    if(shm == (void*)-1)  
    {  
        fprintf(stderr, "shmat failed\n");  
        exit(EXIT_FAILURE);  
    }  
    printf("Memory attached at %X\n", (int)shm);  
    //设置共享内存  
    shared = (struct chm_mes*)shm;  
    while(running)//向共享内存中写数据  
    {  
        //数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本  
        while(shared->shm_num == 1)  
        {  
            sleep(1);  
            printf("Waiting...\n");  
        }  
        //向共享内存中写入数据  
        printf("Enter some text: ");  
        fgets(buffer, BUFSIZ, stdin);  
        strncpy(shared->text, buffer, SIZE);  
        //写完数据,设置shm_num使共享内存段可读  
        shared->shm_num = 1;  
        //输入了end,退出循环(程序)  
        if(strncmp(buffer, "end", 3) == 0)  
            running = 0;  
    }  
    //把共享内存从当前进程中分离  
    if(shmdt(shm) == -1)  
    {  
        fprintf(stderr, "shmdt failed\n");  
        exit(EXIT_FAILURE);  
    }  
    sleep(2);  
    exit(EXIT_SUCCESS);  
} 

共享内存的优缺点:
1、优点:我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。
2、缺点:共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。

2. 信号量(semophore)

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。

1. semget()

头文件:#include <sys/types.h> <sys/ipc.h> <sys/sem.h>
函数的一般形式:int semget(key_t key, int nsems, int semflg)
函数说明:得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符
参数设置:

key:0(IPC_PRIVATE):1.会建立新信号量集对象。2.大于0的32位整数:视参数semflg来确定
    操作,通常要求此值来源于ftok返回的IPC键值
nsems:创建信号量集中信号量的个数,该参数只在创建信号量集时有效
semflg:
    0:取信号量集标识符,若不存在则函数会报错
    IPC_CREAT:当semflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的信号量集,
               则新建一个信号量集;如果存在这样的信号量集,返回此信号量集的标识符。
    IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的信号量集,则新建一个消息队列;
               如果存在这样的信号量集则报错。

返回值:成功返回信号量的标识符,失败返回-1。
错误代码:

    EACCESS:没有权限
    EEXIST:信号量集已经存在,无法创建
    EIDRM:信号量集已经删除
    ENOENT:信号量集不存在,同时semflg没有设置IPC_CREAT标志
    ENOMEM:没有足够的内存创建新的信号量集
    ENOSPC:超出限制
2. semop()

头文件:#include <sys/types.h> <sys/ipc.h> <sys/sem.h>
函数的一般形式:int semop(int semid, struct sembuf *sops, unsigned nsops)
函数说明:对信号量集标识符为semid中的一个或多个信号量进行P操作或V操作
参数设置:

semid:信号量集标识符
sops:指向进行操作的信号量集结构体数组的首地址。
    struct sembuf 
    {
        short semnum; /*信号量集合中的信号量编号,0代表第1个信号量*/
        short val;/*若val>0进行V操作信号量值加val,表示进程释放控制的资源 */
        /*若val<0进行P操作信号量值减val,若(semval-val)<0(semval为该信号量值),
         则调用进程阻塞,直到资源可用;若设置IPC_NOWAIT不会睡眠,进程直接返回
         EAGAIN错误*/
        /*若val==0时阻塞等待信号量为0,调用进程进入睡眠状态,直到信号值为0;
         若设置IPC_NOWAIT,进程不会睡眠,直接返回EAGAIN错误*/
        short flag;  /*0 设置信号量的默认操作*/
        /*IPC_NOWAIT设置信号量操作不等待*/
        /*SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,
         则根据这个进程的UNDO记录自动恢复相应信号量的计数值*/

  };
nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,
      只完成对一个信号量的操作

返回值:成功返回信号量的标识符,失败返回-1。
错误代码:

    E2BIG:一次对信号量个数的操作超过了系统限制
    EACCESS:权限不够
    EAGAIN:使用了IPC_NOWAIT,但操作不能继续进行
    EFAULT:sops指向的地址无效
    EIDRM:信号量集已经删除
    EINTR:当睡眠时接收到其他信号
    EINVAL:信号量集不存在,或者semid无效
    ENOMEM:使用了SEM_UNDO,但无足够的内存创建所需的数据结构
    ERANGE:信号量值超出范围
3. semctl()

头文件:#include <sys/types.h> <sys/ipc.h> <sys/sem.h>
函数的一般形式:int semctl(int semid, int semnum, int cmd, union semun arg)
函数说明:得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符
参数设置:

semid:信号量集标识符
semnum:信号量集数组上的下标,表示某一个信号量
cmd:
    IPC_STAT:从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中
    IPC_SET:设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值
    IPC_RMID:从内核中删除信号量集合
    GETALL:从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体成员的一个指针数组中
    GETNCNT:返回当前等待资源的进程个数
    GETPID:返回最后一个执行系统调用semop()进程的PID
    GETVAL:返回信号量集合内单个信号量的值
    GETZCNT:返回当前等待100%资源利用的进程个数
    SETALL:与GETALL正好相反
    SETVAL:用联合体中val成员的值设置信号量集合中单个信号量的值
arg:
    union semun 
    {
       short val;          /*SETVAL用的值*/
       struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/
       unsigned short* array; /*SETALL、GETALL用的数组值*/
       struct seminfo *buf;   /*为控制IPC_INFO提供的缓存*/
    } arg;

返回值:成功返回大于或等于0;失败返回-1。
错误代码:

    EACCESS:权限不够
    EFAULT:arg指向的地址无效
    EIDRM:信号量集已经删除
    EINVAL:信号量集不存在,或者semid无效
    EPERM:进程有效用户没有cmd的权限
    ERANGE:信号量值超出范围
// 案例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun {
        int val; /* value for SETVAL */
        struct semid_ds *buf; /* buffer for IPC_STAT, IPC_SET */
        unsigned short *array; /* array for GETALL, SETALL */
        struct seminfo *__buf; /* buffer for IPC_INFO */
        };
/***对信号量数组semnum编号的信号量做P操作***/
int P(int semid, int semnum)
{
        struct sembuf sops = {semnum, -1, SEM_UNDO};
        return (semop(semid, &sops,1));
}
/***对信号量数组semnum编号的信号量做V操作***/
int V(int semid, int semnum)
{
        struct sembuf sops = {semnum, +1, SEM_UNDO};
        return (semop(semid, &sops,1));
}
int main(int argc, char **argv)
{
        int key ;
        int semid,ret;
        union semun arg;
        struct sembuf semop;
        int flag ;
        key = ftok("/tmp", 0x66 ) ;
        if ( key < 0 )
        {
            perror("ftok key error") ;
            return -1 ;
        }
       /***本程序创建了三个信号量,实际使用时只用了一个0号信号量***/
        semid = semget(key, 3, IPC_CREAT | 0600);
        if (semid == -1)
        {
                perror("create semget error");
                return ;
        }
        if ( argc == 1 )
        {
            arg.val = 1;
            /***对0号信号量设置初始值***/
            ret =semctl(semid, 0, SETVAL, arg);
            if (ret < 0 )
            {
                    perror("ctl sem error");
                    semctl(semid, 0, IPC_RMID, arg);
                    return -1 ;
            }
        }
        /***取0号信号量的值***/
        ret = semctl(semid, 0, GETVAL, arg);
        printf("after semctl setval  sem[0].val = [%d]\n", ret);
        system("date") ;
        printf("P operate begin\n") ;
        flag = P(semid, 0)  ;
        if ( flag )
        {
            perror("P operate error") ;
            return -1 ;
        }
        printf("P operate end\n") ;
        ret = semctl(semid, 0, GETVAL, arg);
        printf("after P sem[0].val = [%d]\n", ret);
        system("date") ;
        if ( argc == 1 )
        {
            sleep(120) ;
        }
        printf("V operate begin\n") ;
    if (V(semid, 0) < 0)
        {
            perror("V operate error") ;
            return -1 ;
        }
        printf("V operate end\n") ;
        ret = semctl(semid, 0, GETVAL, arg);
        printf("after V sem[0].val = %d\n", ret);
        system("date") ;
        if ( argc >1 )
        {
            semctl(semid, 0, IPC_RMID, arg);
        }
        return 0 ;
}

3. 消息队列(message queue)

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

1. msgget()

头文件:#include <sys/types.h> <sys/ipc.h> <sys/msg.h>
函数的一般形式:int msgget(key_t key, int msgflg)
函数说明:得到消息队列标识符或创建一个消息队列对象并返回消息队列标识符
参数设置:

key:
    0(IPC_PRIVATE):会建立新的消息队列
    大于0的32位整数:视参数msgflg来确定操作。通常要求此值来源于ftok返回的IPC键值
msgflg:
    0:取消息队列标识符,若不存在则函数会报错
    IPC_CREAT:当msgflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回此消息队列的标识符
    IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错

返回值:成功返回消息队列标识符,失败返回 -1。
错误代码:

    EACCES:指定的消息队列已存在,但调用进程没有权限访问它
    EEXIST:key指定的消息队列已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志
    ENOENT:key指定的消息队列不存在同时msgflg中没有指定IPC_CREAT标志
    ENOMEM:需要建立消息队列,但内存不足
    ENOSPC:需要建立消息队列,但已达到系统的限制
2. msgctl()

头文件:#include <sys/types.h> <sys/ipc.h> <sys/msg.h>
函数的一般形式:int msgctl(int msqid, int cmd, struct msqid_ds *buf)
函数说明:获取或者设置消息队列的属性
参数设置:

    msqid:消息队列的标识符
    cmd:
        IPC_STAT:获得msgid的消息队列头数据到buf中
        IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中,可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes
    buf:消息队列管理结构体

返回值:成功返回0,失败返回 -1。
错误代码:

    EACCESS:参数cmd为IPC_STAT,确无权限读取该消息队列
    EFAULT:参数buf指向无效的内存地址
    EIDRM:标识符为msqid的消息队列已被删除
    EINVAL:无效的参数cmd或msqid
    EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
3. msgsnd()

头文件:#include <sys/types.h> <sys/ipc.h> <sys/msg.h>
函数的一般形式:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
函数说明:将msgp消息写入到标识符为msqid的消息队列
参数设置:

    msqid:消息队列标识符
    msgp:
        发送给队列的消息。msgp可以是任何类型的结构体,但第一个字段必须为long类型,
        即表明此发送消息的类型,msgrcv根据此接收消息。msgp定义的参照格式如下:
        struct s_msg
        { /*msgp定义的参照格式*/
           long type; /* 必须大于0,消息类型 */
           char mtext[256]; /*消息正文,可以是其他任何类型*/
        } msgp;
msgsz:要发送消息的大小,不含消息类型占用的4个字节,即mtext的长度
msgflg:
        0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
        IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
        IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,
                     且不通知发送进程。

返回值:成功返回0,失败返回 -1。
错误代码:

EAGAIN:参数msgflg设为IPC_NOWAIT,而消息队列已满
EIDRM:标识符为msqid的消息队列已被删除
EACCESS:无权限写入消息队列
EFAULT:参数msgp指向无效的内存地址
EINTR:队列已满而处于等待情况下被信号中断
EINVAL:无效的参数msqid、msgsz或参数消息类型type小于0

msgsnd()为阻塞函数,当消息队列容量满或消息个数满会阻塞。消息队列已被删除,则返回EIDRM错误;被信号中断返回E_INTR错误。
如果设置IPC_NOWAIT消息队列满或个数满时会返回-1,并且置EAGAIN错误。
msgsnd()解除阻塞的条件有以下三个条件:

  1. 不满足消息队列满或个数满两个条件,即消息队列中有容纳该消息的空间。
  2. msqid代表的消息队列被删除。
  3. 调用msgsnd函数的进程被信号中断。
4. msgrcv()

头文件:#include <sys/types.h> <sys/ipc.h> <sys/msg.h>
函数的一般形式:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
函数说明:从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除
参数设置:

    msqid:消息队列标识符
    msgp:存放消息的结构体,结构体类型要与msgsnd函数发送的类型相同
    msgsz:要接收消息的大小,不含消息类型占用的4个字节
    msgtyp:
        0:接收第一个消息
        >0:接收类型等于msgtyp的第一个消息
        <0:接收类型等于或者小于msgtyp绝对值的第一个消息
    msgflg:
        0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
        IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
        IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
        IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截
                     断,截断部分将被丢弃

返回值:成功返回实际读取的消息长度,失败返回 -1。
错误代码:

    E2BIG:消息数据长度大于msgsz而msgflag没有设置IPC_NOERROR
    EIDRM:标识符为msqid的消息队列已被删除
    EACCESS:无权限读取该消息队列
    EFAULT:参数msgp指向无效的内存地址
    ENOMSG:参数msgflg设为IPC_NOWAIT,而消息队列中无消息可读
    EINTR:等待读取队列内的消息情况下被信号中断

msgrcv()解除阻塞的条件有以下三个:

  1. 消息队列中有了满足条件的消息。
  2. msqid代表的消息队列被删除。
  3. 调用msgrcv()的进程被信号中断。
5. 消息队列案例
// msgqueue.h
#ifndef MSGQUEUE_H
#define MSGQUEUE_H
#include <stdio.h>  
#include <stdlib.h>  
#include <sys/ipc.h>  
#include <sys/msg.h>  
#include <sys/types.h>  
#include <error.h>  
#define BUF_SIZE 1024  
struct my_msg  //定义magsnd()的消息结构体
{  
    long mtype;  
    char mtext[BUF_SIZE];  
};  

int get_key();
void send_msg(int msgid, struct my_msg msg_buf);  
void receive_msg(int msgid, struct my_msg *qbuf, long type);  
void del_msg(int msgid);
#endif
// msgqueue.c
#include "msgqueue.h"

int get_key()  
{  
    int key;  
    key = ftok("mymsg.tmp", 1);  //ftok()是系统IPC键值的格式转换函数
}  

void send_msg(int msgid, struct my_msg msg_buf) //发送消息  
{     
    printf("enter the msg text\n");  
    fgets(msg_buf.mtext, BUF_SIZE, stdin); //消息的内容  
    msg_buf.mtype = 1; //消息的类型  
    //int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);  
    // 发送消息
    if(msgsnd(msgid, (void *)&msg_buf, BUF_SIZE,0) == -1)  
    {  
        perror("send msg error");  
        exit(1);  
    }  
    printf("send msg success\n");  
}  

void receive_msg(int msgid, struct my_msg *qbuf, long type)  
{  
    printf("正在接收消息\n");  
    /*ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
                     int msgflg);*/
    if(msgrcv(msgid, qbuf,  BUF_SIZE, type, IPC_NOWAIT) == -1)  
    {  
        printf("rcv msg error\n");  
        exit(1);  
    }  
    else 
        printf("Type:%ld, Text:%s", qbuf->mtype, qbuf->mtext);  
}

void del_msg(int msgid)  
{  
    if(msgctl(msgid, IPC_RMID,0) == -1)  
    {  
        perror("rm msg error");  
        exit(1);  
    }  
}  
//main.c 在同一个进程中
#include "msgqueue.h"
int main()  
{  
    struct my_msg msg_buf;  
    key_t key;  
    int msgid;  
    key = get_key();  
    if(key < 0)  
    {  
        perror("get key error");  
        exit(1);  
    }  
    msgid = msgget(key, IPC_CREAT | 0644);  //创建消息队列
    if(msgid < 0)  
    {  
        perror("msgget error");  
        exit(1);  
    }  
    printf("get msg success\n");  
    send_msg(msgid, msg_buf);  
    receive_msg(msgid, &msg_buf, 0);  
    del_msg(msgid);  
}

下面的是在不同的进程中通信

// send.c 发送端
#include "msgqueue.h"
int main()  
{  
    struct my_msg msg_buf;  
    key_t key;  
    int msgid;  
    key = get_key();  
    if(key < 0)  
    {  
        perror("get key error");  
        exit(1);  
    }  
    msgid = msgget(key, IPC_CREAT | 0644);  //创建消息队列
    if(msgid < 0)  
    {  
        perror("msgget error");  
        exit(1);  
    }  
    printf("get msg success\n");
    send_msg(msgid, msg_buf);  
    printf("send msg success\n");  
    del_msg(msgid);  
}
// receive.c 接收端
#include "msgqueue.h"
int main()  
{  
    struct my_msg msg_buf;  
    key_t key;  
    int msgid;  
    key = get_key();  
    if(key < 0)  
    {  
        perror("get key error");  
        exit(1);  
    }  
    msgid = msgget(key, IPC_CREAT | 0644);  //创建消息队列
    if(msgid < 0)  
    {  
        perror("msgget error");  
        exit(1);  
    }  
    printf("get msg success\n");
    receive_msg(msgid, &msg_buf, 0);  
    printf("receive msg success\n");  
    del_msg(msgid);  
}

备注:
套接字通信主要用于不同机器之间的通信,在此处不做说明,另外单做说明。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,088评论 0 23
  • 参考来源:unix进程间的通信方式 (1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一...
    形式主义_5adc阅读 694评论 0 1
  • 进程间通信 进程间通信即IPC(InerProcess Communication)Unix ipc 已经是而且继...
    千里山南阅读 454评论 0 2
  • 概述 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通...
    捉虫大师阅读 1,757评论 1 8
  • 枕石酣眠 【文案】 十里相思,岁岁红莲,枕石酣眠,不敌烟火人间。 (一) 边境的城说大不大,...
    小腐酱QVQ阅读 379评论 0 1