六种进程间通信方式详解

进程间通信是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享内存Socket、Streams等。其中Socket和Streams支持不同主机上的两个进程IPC。

一、管道

管道,通常指无名管道,是UNIX系统IPC最古老的形式。

1、特点

  •  它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
  • 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
  • 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。 

2、原型

#include 

int pipe(int pipefd[2]);

pipefd[2]    用于存放读和写的文件描述符  大小为2的数组

返回值:成功返回 0   失败返回-1 ,errno被设置

        当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开(巧记:我们说话时说01和读写,0对应读,1对应写)。如下图:

六种进程间通信方式详解_第1张图片

 3、demo1

思路:

  • pipe创建管道
  • fork创建进程
  • 父进程往管道里写,子进程读管道里的数据并打印
#include 
#include 
#include 
#include 

int main()
{
        int pid;
        int fd[2];
        char r_buf[128] = {0};

        if ((pipe(fd)) == -1) {		//创建管道
                perror("pipe fail");
                exit(-1);
        }

        if ((pid = fork()) == -1) {		//创建进程
                perror("fork fail");
                exit(-1);
        }

        if (pid > 0) {		//父进程
                close(fd[0]);	//关闭读端
                write(fd[1], "hallo world!", strlen("hallo world!"));	//往管道里写
                close(fd[1]);	//关闭管道
        } else if (pid == 0) {
                close(fd[1]);	//关闭写端
                read(fd[0], r_buf, 128);	//读取管道中的数据
                printf("%s\n", r_buf);		//打印数据
                close(fd[0]);		//关闭管道
        }

        return 0;
}

运行结果:

二、FIFO

FIFO,也称为命名管道,它是一种文件类型。

1、特点

  • FIFO可以在无关的进程之间交换数据,与无名管道不同。
  • FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。 

2、原型

#include 
#include 

int mkfifo(const char *pathname, mode_t mode);

pathname	创建一个名字为"pathname"的FIFO特殊文件
mode	文件权限

返回值:成功返回 0   失败返回 -1 ,errno被设置

        其中的mode参数与open函数中的mode相同。一旦创建了一个FIFO,就可以用一般的文件I/O函数操作它。当open一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认),只读open要阻塞到某个其他进程为写而打开此FIFO。类似的,只写open要阻塞到某个其他进程为读而打开它。
  • 若指定了O_NONBLOCK,则只读open立即返回,而只写open将出错返回 -1,如果没有进程已经为读而打开该FIFO,其errno置ENXIO。

3、demo

         FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。下面的例子演示了使用FIFO进行IPC的过程。

思路:

  • mkfifo创建FIFO
  • fork创建进程
  • 父进程只写打开FIFO,并往FIFO里写如数据
  • 子进程只读打开FIFO,并读取FIFO里的数据并打印
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
        int fd;
        int pid;
        int sum = 3;
        char r_buf[128] = {0};

        if (((mkfifo("file", 0600)) == -1) && (errno != EEXIST)) {	//创建FIFO,如果FIFO存在条件为假
                perror("mkfifo fail");
                exit(-1);
        }

        if ((pid = fork()) == -1) {	//创建进程
                perror("fork fail");
                exit(-1);
        } else if (pid > 0) {	//父进程
                fd = open("file", O_WRONLY);	//打开FIFO
                while(sum)	//间隔一秒往FIFO里写入数据
                {
                        write(fd, "message form fifo", strlen("message from fifo"));
                        sleep(1);
                        sum--;
                }
                close(fd);	//关闭FIFO
        } else if (pid == 0) {	//子进程
                fd = open("file", O_RDONLY);	//打开FIFO
                while (sum)		//间隔一秒读取一次FIFO里的数据并打印
                {
                        read(fd, r_buf, 128);
                        puts(r_buf);
                        sleep(1);
                        sum--;
                }
                close(fd);	//关闭FIFO
        }

        return 0;
}

运行结果:

六种进程间通信方式详解_第2张图片

三、消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

1、特点

  •  消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  • 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。 

 2、原型

//头文件
#include 
#include 
#include 

int msgget(key_t key, int msgflg);
//创建或打开队列,成功返回队列ID,失败返回-1,errno被设置

int msgsnd(int msqid, const void *msgp, size_t msgsz, int flag);
//写入数据,成功返回0,失败返回-1, errno被设置

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int flag);
//读取数据,成功返回数据的字节数,失败返回-1, errno被设置

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//根据cmd(删除队列、设置数据元素、读取数据结构)命令执行操作。失败返回-1,errno被设置
key ftok()生成,key为队列ID的组成部分
msgflg 标志位(IPC_CREAT、IPC_EXCL、 IPC_NOWAIT)还应加上队列权限(读、写、执行)
msqid 队列ID
msgp 消息缓冲区指针(结构体指针)
flag 为0表示阻塞方式(队列空间不够就会堵塞),设置IPC_NOWAIT 表示非阻塞方式
msgsz 消息数据长度,不含数据类型长度4个字节
msgtyp 数据类型 type == 0 返回队列中的第一个消息。type > 0 返回队列中消息类型为type的第一个消息。type < 0返回队列中消息类型值小于或等于type绝对值,而且在这种消息中,其类型值又最小的消息
cmd 控制命令(IPC_STAT(读取数据结构)、IPC_SET(设置数据元素)、IPC_RMID(删除队列))
buf 读取和设置操作的缓冲区指针,不使用设置为NULL

3、demo

思路:

  • fork创建进程
  • ftok创建key
  • 父进程创建消息队列,并往消息队列里写入数据
  • 子进程打开消息队列,读取数据并打印,最后删除该消息队列 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

struct msgbuf {
        long mtype;
        char mtext[128];
};

int main()
{
        int key;
        int pid;
        int msgid;
        struct msgbuf wbuf;
        struct msgbuf rbuf;

        pid = fork();	//创建进程
        if (pid == -1) {
                perror("fork fail");
                exit(1);
        }

        key = ftok(".", 1);	//创建key
        if (key == -1) {
                perror("ftok fail");
                exit(2);
        }

        if (pid > 0) {	//父进程
                msgid = msgget(key, IPC_CREAT | 0777);	//创建消息队列
                if (msgid == -1) {
                        perror("msgget fail");
                        exit(3);
                }
                wbuf.mtype = 1;		//创建数据
                memset(wbuf.mtext, '0', sizeof(wbuf.mtext));
                strcpy(wbuf.mtext, "message from message queuing");
                msgsnd(msgid, &wbuf, sizeof(wbuf) - sizeof(long), 0);	//发送数据,空间不够就会堵塞
        } else if (pid == 0) {	//子进程
                msgid = msgget(key, IPC_CREAT | 0777);	//打开消息队列
                if (msgid == -1) {
                        perror("msgget fail");
                        exit(4);
                }
                msgrcv(msgid, &rbuf, sizeof(rbuf) - sizeof(long), 1, 0);	//读取数据,无数据就会堵塞
                printf("type = %ld  msg: %s\n", rbuf.mtype, rbuf.mtext);
                msgctl(msgid, IPC_RMID, NULL);	//删除消息队列
        }

        return 0;
}

运行结果:

四、共享内存

1、特点

  • 共享内存是最快的一种IPC,因为进程是直接对内存进行存取。
  • 因为多个进程可以同时操作,所以需要进行同步。
  • 信号量 + 共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

2、原型

#include 
#include 

//创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);

//连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);

//断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(const void *addr);

//控制共享内存的相关消息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
key 由ftok()生成的key标识,标识系统的唯一IPC资源。
size 需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
只获取共享内存时指定为0
flag

如果是已经存在的,可以使用IPC_CREAT或直接传0。

如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL。
若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。
shm_id 共享存储段的标识符。
*addr addr = 0,则存储段连接到由内核选择的第一个可以地址上(推荐使用),连接以后返回的地址。
cmd IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中。
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID:删除这片共享内存
*buf 设置为NULL即可。

3、一般操作步骤

  • 创建/打开共享内存:shmget,当用shmget函数创建一段共享内存时,必须指定其size;而如果引用一个已存在的共享内存,则将size指定为0。
  • 映射:shmat,当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。
  • 数据交换
  • 释放共享内存:shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
  • 移除共享内存:shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。

4、demo

思路:

  • fork创建进程
  • ftok获取key
  • 父进程shmget创建共享内存,shmat挂载共享内存到进程当中,strcpy写入数据,shmdt卸载共享内存
  • 子进程shmget创建共享内存,shmat挂载共享内存到进程当中,打印数据,shmdt卸载共享内存,shmctl删除共享内存
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
        int pid;
        int key;
        int shmid;
        char *shmaddr;

        pid = fork();	//创建进程
        if (pid == -1) {
                perror("fork fail");
                exit(-1);
        }

        key = ftok(".", 2);	//获取key
        if (key == -1) {
                perror("ftok fail");
                exit(-1);
        }

        if (pid > 0) {	//父进程
                shmid = shmget(key, 1024*4, IPC_CREAT | 0666);	//创建共享内存
                if (shmid == -1) {
                        perror("shmget fail");
                        exit(-1);
                }
                shmaddr = shmat(shmid, NULL, 0);	//挂载内存到进程空间中
                strcpy(shmaddr, "message from share memory");	//写入数据
                shmdt(shmaddr);	//卸载共享内存
        } else if (pid == 0) {	//子进程
                shmid = shmget(key, 0, IPC_CREAT);	//获取共享内存ID
                if (shmid == -1) {
                        perror("shmget fail");
                        exit(-1);
                }
                shmaddr = shmat(shmid, NULL, 0);	//挂载内存到进程空间中
                printf("%s\n", shmaddr);	//打印共享内存中的数据
                shmdt(shmaddr);		//卸载共享内存
                shmctl(shmid, IPC_RMID, NULL);	//删除共享内存
        }

        return 0;
} 

运行结果:

五、信号

        对于Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为Linux提供了一种处理异步事件的方法。比如,终端用户输入了ctrl+c来中断程序,会通过信号机制停止一个程序。

1、信号概述

        信号的名字和编号:每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO”、“SIGCHLD”等等。信号定义在 signal.h 头文件中,信号名都定义为正整数。具体的信号名称可以使用 kill -l 来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0有特殊的应用。

CLC@Embed_Learn:~/IPC$ kill -l
 1) SIGHUP	    2) SIGINT	     3) SIGQUIT	     4) SIGILL	     5) SIGTRAP
 6) SIGABRT	    7) SIGBUS	     8) SIGFPE	     9) SIGKILL	    10) SIGUSR1
11) SIGSEGV	    12) SIGUSR2	    13) SIGPIPE	    14) SIGALRM	    15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	    18) SIGCONT	    19) SIGSTOP	    20) SIGTSTP
21) SIGTTIN	    22) SIGTTOU	    23) SIGURG	    24) SIGXCPU	    25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	    28) SIGWINCH	29) SIGIO	    30) SIGPWR
31) SIGSYS	    34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

2、信号的处理

        信号的处理方式有三种方法,分别是:忽略捕捉默认动作。

  • 忽略信号 ,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是SIGKILL 和 SIGSTOP )。因为它们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就会变成了没人能管理的进程,显然是内核设计者不希望看到的场景。
  • 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
  • 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。具体的信号默认动作可以使用 man 7 signal 来查看系统的具体定义。

例子:

        其实对于常用的 kill 命令就是一个发送信号的工具, kill  9  PID 来杀死进程。比如,我在后台运行了一个 a.out 的程序,通过ps命令可以查看它的 PID ,通过 kill  9 来发送了一个终止的信号来结束了 a.out 进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL ,正是杀死该进程的信号。而以下执行过程实际也就是执行了9号的默认动作——杀死进程。

六种进程间通信方式详解_第3张图片

对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通信的手段。

3、信号处理函数的注册、信号发送函数

信号处理函数的注册不只一种方法,分为入门版和高级版

  • 入门版:函数 signal
  • 高级版:函数 sigaction

信号发送函数也不止一种,同样分为入门版和高级版

  • 入门版:kill
  • 高级版:sigqueue

入门版本:

signal函数

#include 
 
typedef void (*sighandler_t)(int);//void型函数,里面一个int型的信号参数
sighandler_t signal(int signum, sighandler_t handler);
/*
根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。
对于 sighandler_t signal(int signum, sighandler_t handler) 函数来说,signum 显然是信号的编号,handler 是中断函数的指针。
同样,typedef void (*sighandler_t)(int) 中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。
*/

kill函数

#include 
#include 
 
int kill(pid_t pid, int sig);
//pid:要发送信号的进程pid号。    sig:发送的信号。

/*
pid > 0:将发送该 pid 的进程。
pid == 0:将会把信号发送给与发送进程属于同一进程组的所有进程,并且发送进程具有权限想这些进程发送信号。
pid < 0:将信号发送给进程组ID 为 pid 的绝对值,并且发送进程具有权限向其发送信号的所有进程。
pid == -1:将该信号发送给发送进程的有权限向他发送信号的所有进程。(不包括系统进程集中的进程)。
*/

demo思路:

  • 编写demo5.c调用信号注册函数signal,当捕获SIGINT或SIGKILL立即执行自定义处理函数handler,如果捕获到SIGUSR1,则忽略该信号
  • 编写demo6.c通过调用程序去发送信号

demo5.c

#include 
#include 

void handler(int signum)
{
        printf("get signum = %d\n", signum);

        switch(signum)
        {
                case 2:
                        printf("SIGINT\n");
                        break;
                case 9:
                        printf("SIGKILL\n");
                        break;
                case 10:
                        printf("SIGUSR1\n");
        }
        printf("never quit!\n");
}

int main()
{
        signal(SIGINT, handler);
        signal(SIGKILL, handler);
        signal(SIGUSR1, SIG_IGN);

        while(1);
        return 0;
}

 demo6.c

#include 
#include 
#include 
#include 

int main(int argc, char **argv)
{
        int signum;
        int pid;
        char cmd[20];

        if (argc != 3) {
                printf("sum false!\n");
                exit(-1);
        }
		//方式一
        pid = atoi(argv[2]);
        signum = atoi(argv[1]);
        kill(pid, signum);
		/*方式二
        sprintf(cmd, "kill -%s %s", argv[1], argv[2]);
        system(cmd);
		*/        
        return 0;
}

 运行结果:

        简单的总结一下,我们通过 signal 函数注册一个信号处理函数,分别注册了三个信号(SIGINT,SIGKILL和 SIGUSER1),随后主程序就一直“长眠”了。通过 kill 命令发送信号之前,我们需要先查看到接收者,通过 ps 命令查看了之前所写的程序的 PID,通过 kill 函数来发送。
对于已注册的信号,使用 kill 发送都可以正常接收到,并执行对应的命令。

在此还有两个问题需要说明一下:

  • 当执行一个程序时,所有信号的状态都是系统默认或者忽略状态的。除非是调用exec进程忽略了某些信号。exec 函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不会改变 。
  • 当一个进程调动了 fork 函数,那么子进程会继承父进程的信号处理方式。

高级版本

sigaction函数

#include 

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
   void       (*sa_handler)(int); //信号处理不接受数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理接受数据和sigqueue配合使用,void *为空无数据,不为空有数据
   sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先设置。
   int        sa_flags;//设置为 SA_SIGINFO 表示接受数据
 };
//回调函数句柄sa_handler、sa_sigaction只能任选其一

siginfo_t {
     	 int      si_signo;    /* 信号的编号 */
         int      si_errno;    /* 一个errno值 */
         int      si_code;     /* 信号码 */
         int      si_trapno;   /* 导致硬件生成信号(在大多数体系结构上未使用)*/
         pid_t    si_pid;      /* 发送进程ID */
         uid_t    si_uid;      /* 发送进程的实际用户ID */
         int      si_status;   /* 退出值或信号 */
         clock_t  si_utime;    /* 消耗的用户时间 */
         clock_t  si_stime;    /* 消耗的系统时间 */
         sigval_t si_value;    /* 存放信号携带的信息,si_value为共用体与发送信号时的value共用体一致 */
         int      si_int;      /* POSIX.1b信号 */
         void    *si_ptr;      /* POSIX.1b信号 */
         int      si_overrun;  /* 计时器溢出计数;POSIX.1b计时器*/
         int      si_timerid;  /* 计时器ID;POSIX.1b计时器 */
         void    *si_addr;     /* 导致故障的内存位置*/
         long     si_band;     /* band事件(was int in)glibc 2.3.2及更早版本)*/
         int      si_fd;       /* 文件描述符 */
         short    si_addr_lsb; /* 地址的最低有效位(从内核2.6.32开始*/
           }

        sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数 signum是注册信号的编号;第二个参数 *act 如果不为空说明需要对该信号有新的配置;第三个参数 *oldact 如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

六种进程间通信方式详解_第4张图片

 sigqueue函数

#include 
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
    int   sival_int;
    void *sival_ptr;
};

六种进程间通信方式详解_第5张图片

demo思路:

  • 编写 receive.c 用于接收并处理信号
  • 编写 send.c 用于发送信号,信号携带一个整型数和一个字符串,由于收发端为不同程序信号无法直接携带字符串,将使用共享内存实现

receive.c

#include 
#include 
#include 
#include 
#include 

void handler(int signum, siginfo_t *info, void *content)
{
        int shmid;
        int key;
        char *shmaddr;
		//创建共享内存
        key = ftok(".", 10);
        shmid = shmget(key, 0, IPC_CREAT | 0666);
        shmaddr = shmat(shmid, NULL, 0);
	
        printf("get signum = %d\n", signum);
		//打印信号携带的信息
        if (content != NULL) {  //判断信号是否有携带信息
                printf("from pid = %d\n", info->si_pid);
                printf("get message character string = %s   int data = %d\n",shmaddr, info->si_value.sival_int);

        }
        shmdt(shmaddr);
        shmctl(shmid, IPC_RMID, NULL);
}

int main()
{
        struct sigaction act;
        struct sigaction oldact;

        act.sa_sigaction = handler;
        act.sa_flags = SA_SIGINFO;

        printf("my pid = %d\n", getpid());	
        sigaction(10, &act, &oldact);	

        while(1);
        return 0;
}

send.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char **argv)
{
        int pid;
        int key;
        int shmid;
        int signum;
        char *shmaddr;
        union sigval value;

        if (argc != 3) {
                printf("sum false!\n");
                exit(-1);
        }
		//创建共享内存
        key = ftok(".", 10);
        shmid = shmget(key, 1024, IPC_CREAT | 0666);
        shmaddr = shmat(shmid, NULL, 0);
        strcpy(shmaddr, "hallo");
        shmdt(shmaddr);
		//发送信号
        pid = atoi(argv[2]);
        signum = atoi(argv[1]);
        printf("my pid = %d\n", getpid());
        value.sival_int = 100;
        sigqueue(pid, signum, value);

        return 0;
}   

运行结果:

六、信号量

        信号量与IPC结构不同,它是一个计数器。信号量用于实现进程间的互斥同步,而不是用于存储进程间通信数据。

        信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。

  • 当它的值大于0时,表示当前可用资源的数量;
  • 当它的值小于0时,其绝对值表示等待使用该资源的进程个数。

1、特点

  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

  • 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

  • 支持信号量组。

        注意,信号量的值仅能由PV操作来改变。

2、原型

        Linux下信号量函数都是在通用信号量数组上进行操作的,不是一个单一的二值信号量进行操作。

#include 
#include 
#include 

创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
/*
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1;
如果是引用一个现有的集合,则将num_sems指定为0。
*/


对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
/*
semid:信号量的id    
numops:配置信号量个数,如果是1个,做一个struct sembuf semoparray[]

struct sembuf 
{
    short sem_num; // 信号量组中对应的序号,0~sem_nums-1
    short sem_op;  // 信号量值在一次操作中的改变量
    short sem_flg; // IPC_NOWAIT, SEM_UNDO
};

如果信号量集有两个或以上,需要做数组,man 手册例子:
struct sembuf sops[2];
int semid;
sops[0].sem_num = 0;        //Operate on semaphore 0 
sops[0].sem_op = 0;         //Wait for value to equal 0 
sops[0].sem_flg = 0;
sops[1].sem_num = 0;        // Operate on semaphore 0 
sops[1].sem_op = 1;         // Increment value by one 
sops[1].sem_flg = 0; 
if (semop(semid, sops, 2) == -1) {
   perror("semop");
   exit(EXIT_FAILURE);
}
*/


控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
/*
在semctl函数中的命令(cmd)有多种,这里就说两个常用的:
1、SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
2、IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

联合semun需要用户自行定义,通常只使用第一个元素 
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 (Linux-specific) 
};
*/

 3、demo

功能说明:使用信号量配合共享内存、消息队列进行进程间通信。服务端根据用户的输入进行发送操作或者退出操作。

服务端:

  • 发送操作:先执行P操作,用户输入需要发送的信息,发送数据,消息队列用于提示接收端读取数据,最后执行V操作。 
  • 退出操作:使用消息队列向接收端发送信息提示退出,卸载共享内存后退出。

接收端:

  • 读取消息队列里的提示信息(无数据就会堵塞)。
  • 若提示信息为读取数据:执行P操作,然后读取共享内存里的数据并打印,最后执行V操作。
  • 若提示消息为退出:卸载共享内存,删除消息队列、共享内存、信号量后退出。

服务端:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

struct msgbuf
{
        long mtype;
        char mtext[128];
};

union semun{
        int              val;
        struct semid_ds *buf;
        unsigned short  *array;
        struct seminfo  *__buf;
};

void seminit(int semid, int num)
{
        union semun sem;
        sem.val = 1;
        semctl(semid, num, SETVAL, sem);
}

void sem_p(int semid, int num)
{
        struct sembuf sbuf;
        sbuf.sem_num = num;
        sbuf.sem_op = -1;
        sbuf.sem_flg = SEM_UNDO;
        semop(semid, &sbuf, 1);
}

void sem_v(int semid, int num)
{
        struct sembuf sbuf;
        sbuf.sem_num = num;
        sbuf.sem_op = 1;
        sbuf.sem_flg = SEM_UNDO;
        semop(semid, &sbuf, 1);
}

int main()
{
        int key;
        int shmid,msgid,semid;
        char *shmaddr;
        char *buf;
        struct msgbuf r_buf;

        key = ftok(".", 10);

        msgid = msgget(key, IPC_CREAT | 0777);

        shmid = shmget(key, 1024, IPC_CREAT | 0666);
        shmaddr = shmat(shmid, NULL, 0);

        semid = semget(key, 1, IPC_CREAT | 0666);
        seminit(semid, 0);

        while(1)
        {
                msgrcv(msgid, &r_buf, sizeof(r_buf) - sizeof(long), 666, 0);  //读取队列里的数据
                if (strcmp(r_buf.mtext, "r") == 0) {
                        sem_p(semid, 0);
                        printf("%s\n", shmaddr); //读取共享内存中的内容
                        sem_v(semid, 0);
                } else if (strcmp(r_buf.mtext, "q") == 0) {
                        shmdt(shmaddr);
                        union semun un;
                        shmctl(shmid, IPC_RMID, NULL);
                        msgctl(msgid, IPC_RMID, NULL);
                        semctl(semid, 0, IPC_RMID, un);
                        exit(-1);
                        break;
                }

        }

        return 0;
}

客户端:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

struct msgbuf
{
        long mtype;
        char mtext[128];
};

union semun{
        int              val;
        struct semid_ds *buf;
        unsigned short  *array;
        struct seminfo  *__buf;
};

void seminit(int semid, int num)
{
        union semun sem;
        sem.val = 1;
        semctl(semid, num, SETVAL, sem);
}

void sem_p(int semid, int num)
{
        struct sembuf sbuf;
        sbuf.sem_num = num;
        sbuf.sem_op = -1;
        sbuf.sem_flg = SEM_UNDO;
        semop(semid, &sbuf, 1);
}

void sem_v(int semid, int num)
{
        struct sembuf sbuf;
        sbuf.sem_num = num;
        sbuf.sem_op = 1;
        sbuf.sem_flg = SEM_UNDO;
        semop(semid, &sbuf, 1);
}

void welcome()
{
        printf("----------------------------------------\n");
        printf("--------------welcome to ipc------------\n\n");
        printf("------------input r send datas----------\n");
        printf("---------------input q quit------------\n\n");
        printf("----------------------------------------\n");
}

int main()
{
        int key;
        int shmid,msgid,semid;
        char *shmaddr;
        struct msgbuf r_buf;

        key = ftok(".", 10);

        msgid = msgget(key, IPC_CREAT | 0777);

        shmid = shmget(key, 0, IPC_CREAT | 0666);
        shmaddr = shmat(shmid, NULL, 0);

        semid = semget(key, 1, IPC_CREAT | 0666);

        welcome();

        while(1)
        {
                printf("please input your operation\n");
                memset(r_buf.mtext, '0', sizeof(r_buf.mtext));
                scanf("%s", r_buf.mtext);      //往队列写入数据,用于判断是r还是q
                if (strcmp(r_buf.mtext, "r") == 0) {
                        sem_p(semid, 0);
                        printf("plase input you want send datas\n");
                        scanf("%s", shmaddr);                   //往共享内存写入数据
                        r_buf.mtype = 666;
                        msgsnd(msgid, &r_buf, sizeof(r_buf) - sizeof(long), 0);
                        sem_v(semid, 0);
                } else if (strcmp(r_buf.mtext, "q") == 0) {
                        r_buf.mtype = 666;
                        msgsnd(msgid, &r_buf, sizeof(r_buf) - sizeof(long), 0);
                        shmdt(shmaddr);
                        break;
                }
        }

        return 0;
}

运行结果:

客户端
----------------------------------------
--------------welcome to ipc------------

------------input r send datas----------
---------------input q quit------------

----------------------------------------

please input your operation

服务端:
r

客户端
plase input you want send datas

服务端:
hallo

客户端

hallo
please input your operation

服务端:
q

七、总结

1、无名管道:速度慢,容量有限,只有父子进程能通讯。

2、有名管道:任何进程间都能通讯,但速度慢。

3、消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。

4、共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

5、信号: 传递字符串消息只能在同一程序下的进程间。

6、信号量:不能传递复杂消息,只能用来同步 ,通常配合消息队列、共享内存使用。

你可能感兴趣的:(Linux,linux,c语言)