Linux系统编程——进程间通信
目录
进程间通信
1.无名管道
2.有名管道
3.信号
3.1信号发送
3.2信号接收
3.3信号处理
编辑
4.共享内存
5.消息队列
6.信号量
进程间通信
进程间的通信应用也是很广泛的,比如后台进程和 GUI 界面数据传递,发送信号关机,Ctrl+C 终止正在运行的程序等。
Linux 进程间通信机制分三类:数据交互,同步,信号。理解了这些机制才能灵活运用操作系统提供的IPC 工具。
本章以常用的管道(包括有名管道和无名管道),System V IPC(消息队列,共享内存,信号灯),套接字(UNIX 域套接字和网络套接字)为例来说明 Linux 进程通信常用的方法,本文档中介绍的只是一小部分,如果想深入了解可以去翻看专业的书籍。
1.无名管道
无名管道是最古老的进程通信方式,有如下两个特点:
- 只能用于有关联的进程间数据交互,如父子进程,兄弟进程,子孙进程,在目录中看不到文件节点,读写文件描述符存在一个 int 型数组中。
- 只能单向传输数据,即管道创建好后,一个进程只能进行读操作,另一个进程只能进行写操作,读出来字节顺序和写入的顺序一样。
函数 | int pipe(int pipefd[2]) |
头文件 | #include <unistd.h> |
参数 pipefd[2] | 一个 int 型数组,表示管道的文件描述符,pipefd[0]为读,pipefd[1]为写,如下图所示: |
返回值 | 成功返回 0,失败返回-1 |
功能 | 创建无名管道 |
无名管道使用步骤:
- 调用 pipe()创建无名管道;
- fork()创建子进程,一个进程读,使用 read(),一个进程写,使用 write()。
实验代码
实现子进程和父进程之间的通信,创建无名管道,父进程从终端获取数据,写入管道,子进程从管道读数据并打印出来。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void)
{char buf[32] = {0};pid_t pid;// 定义一个变量来保存文件描述符// 因为一个读端,一个写端,所以数量为 2 个int fd[2];// 创建无名管道pipe(fd);printf("fd[0] is %d\n", fd[0]);printf("fd[1] is %d\n", fd[1]);// 创建进程pid = fork();if (pid < 0){printf("error\n");}if (pid > 0){int status;close(fd[0]);write(fd[1], "hello", 5);close(fd[1]);wait(&status);exit(0);}if (pid == 0){close(fd[1]);read(fd[0], buf, 32);printf("buf is %s\n", buf);close(fd[0]);exit(0);}return 0;
}
在 Ubuntu 上编译运行,如下图所示:

2.有名管道
有名管道中可以很好地解决在无关进程间数据交换的要求,并且由于它们是存在于文件系统中的,也提供了一种比匿名管道更持久稳定的通信办法。
有名管道在一些专业书籍中叫做命名管道,它的特点是
- 可以使无关联的进程通过 fifo 文件描述符进行数据传递;
- 单向传输有一个写入端和一个读出端,操作方式和无名管道相同。
我们使用 mkfifo()函数创建有名管道。函数详解如下所示:
函数 | int mkfifo(const char *pathname, mode_t mode) |
头文件 | #include <sys/types.h> #include <sys/stat.h> |
参数 pathname | 有名管道的路径和名称 |
参数mode | 权限 |
返回值 | 成功返回 0,失败返回-1 |
有名管道使用步骤:
- 使用 mkfifo()创建 fifo 文件描述符。
- 打开管道文件描述符。
- 通过读写文件描述符进行单向数据传输。
使用命令创建管道文件
输入以下命令创建管道文件,并查看,如下图所示:
mkfifo fifo ls
ls -al

实验代码
创建两个无关联的进程,一个进程创建有名管道并写数据,另一个进程通过管道读数据。
fifo_write.c:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{int ret;char buf[32] = {0};int fd;if (argc < 2){printf("Usage:%s <fifo name> \n", argv[0]);return -1;}if (access(argv[1], F_OK) == 1){ret = mkfifo(argv[1], 0666);if (ret == -1){printf("mkfifo is error \n");return -2;}printf("mkfifo is ok \n");}fd = open(argv[1], O_WRONLY);while (1){sleep(1);write(fd, "hello", 5);}close(fd);return 0;
}
fifo_read.c:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{char buf[32] = {0};int fd;if (argc < 2){printf("Usage:%s <fifo name> \n", argv[0]);return -1;}fd = open(argv[1], O_RDONLY);while (1){sleep(1);read(fd, buf, 32);printf("buf is %s\n", buf);memset(buf, 0, sizeof(buf));}close(fd);return 0;
}
在 Ubuntu 下编译 fifo_read.c,并运行如下图所示:

然后重新打开一个窗口,编译 fifo_write.c 并运行如下图所示:

fiforead 进程可以看到从管道读出的数据:

3.信号
3.1信号发送
信号是 Linux 系统响应某些条件而产生的一个事件,接收到该信号的进程会执行相应的操作。信号的产生有三种方式:
- 由硬件产生,如从键盘输入 Ctrl+C 可以终止当前进程
- 由其他进程发送,如可在 shell 进程下,使用命令 kill -信号标号 PID,向指定进程发送信号。
- 异常,进程异常时会发送信号
本章只关注在应用层对信号的处理。在 Ubuntu 终端输入 kill -l,查看所有的信号。1) SIGHUP2) SIGINT3) SIGQUIT4) SIGILL5) SIGTRAP6) SIGABRT7) SIGBUS8) SIGFPE9) SIGKILL10) SIGUSR111) SIGSEGV12) SIGUSR213) SIGPIPE14) SIGALRM15) SIGTERM16) SIGSTKFLT17) SIGCHLD18) SIGCONT19) SIGSTOP20) SIGTSTP21) SIGTTIN22) SIGTTOU23) SIGURG 24) SIGXCPU25) SIGXFSZ26) SIGVTALRM27) SIGPROF28) SIGWINCH29) SIGIO30) SIGPWR31) SIGSYS34) SIGRTMIN35) SIGRTMIN+136) SIGRTMIN+237) SIGRTMIN+3

下面是几个常用的函数:
函数 | int kill(pid_t pid, int sig) |
头文件 | #include <sys/types.h> #include <signal.h> |
参数 pid | 大于 0,时为向 PID 为 pid 的进程发送信号 等于 0,向同一个进程组的进程发送信号; 等于-1,除发送进程自身外,向所有进程 ID 大于 1 的进程发送信号。 小于-1,向组 ID 等于该 pid 绝对值的进程组内所有进程发送信号。 |
参数 sig | 设置发送的信号;等于 0 时为空信号,无信号发送。常用来进行错误检 查。 |
返回值 | 执行成功时,返回值为 0;错误时,返回-1,并设置相应的错误代码 errno |
功能 | 用于向任何进程组或进程发送信号 |
函数 | int raise(int sig) |
头文件 | #include <signal.h> |
参数 sig | 信号 |
功能 | 相当于 kill(getpid(),sig),向进程自身发送信号 |
函数 | unsigned int alarm(unsigned int seconds) |
头文件 | #include <unistd.h> |
参数 | 设定的时间 |
功能 | 设定的时间超过后产生 SIGALARM 信号,默认动作是终止进程。 |
注意 | 每个进程只能有一个 alarm()函数,时间到后要想再次使用要重新注册。 |
使用规则:
实验 1 代码:在程序中实现:自己给自己发送信号。
raise.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(void)
{printf("raise before\n");raise(9);printf("raise after\n");return 0;
}
编译运行,如下图所示:

实验 2 代码 kill.c 发送信号。
kill.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{pid_t pid;int sig;if(argc < 3){printf("Usage:%s <pid_t> <signal>\n",argv[0]);return -1;}sig = atoi(argv[2]);pid = atoi(argv[1]);kill(pid,sig);return 0;
}
test.c
#include <stdio.h>
#include <unistd.h>
void main(void)
{while(1){sleep(1);printf("hello world\n");}
}
编译运行 test,如下图所示,进程会循环打印 hello world。

重新打开另一个窗口,编译 kill.c,然后查看 test 进程的 pid 号,运行测试如下图所示;

与此同时,显示 test 的窗口显示,test 进程被杀死,如下图所示:

实验 3 代码
alarml.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{int i;alarm(3);while(1){sleep(1);i++;printf("i = %d\n",i);}return 0;
}
编译 alarm.c,并运行。如下图所示,设定的时间(3 秒)超过后产生 SIGALARM 信号,默认动作是终止进程。

3.2信号接收
接收信号:如果要让我们接收信号的进程可以接收到信号,那么这个进程就不能停止。让进程不停止有三种方法:
- while
- sleep
- pause
方法一;
while.c
#include <stdio.h>
#include <unistd.h>
void main(void)
{while(1){sleep(1);printf("hello world\n");}
}
编译运行结果如下所示,按 ctrl+C 会发送 SIGINT 信号:

方法二;
sleep.c
#include <stdio.h>
#include <unistd.h>
void main(void)
{sleep(60);
}
编译运行,如下图所示,会休眠 60s。

方法三
使用 pause()函数,函数详解如下:
函数 | int pause(void) |
头文件 | #include <unistd.h> |
返回值 | 进程被信号中断后一直返回-1 |
功能 | 将进程挂起,等待信号 |
pause.c
#include <stdio.h>
#include <unistd.h>
void main(void)
{printf("pause before\n");pause();printf("pause after\n");
}
编译程序并运行,如下图所示:

输入以下命令查看进程,如下图所示:

按 ctrl+C 键,pause 进程终止,再次查看 pause 的进程,如下图所示:

3.3信号处理
信号是由操作系统来处理的,说明信号的处理在内核态。信号不一定会立即被处理,此时会储存在信号的信号表中。
处理过程示意图:

由上图中可看出信号有三种处理方式:
- 默认方式(通常是终止进程),
- 忽略,不进行任何操作。
- 捕捉并处理调用信号处理器(回调函数形式)。
函数 | sighandler_t signal(int signum, sighandler_t handler); 可以简化成 signal(参数 1,参数 2); |
头文件 | #include <unistd.h> typedef void (*sighandler_t)(int); |
参数 1 | 我们要进行处理的信号,系统的信号我们可以在终端键入 kill -l 查看。 |
参数 2 | 处理的方式(是系统默认还是忽略还是捕获) 忽略该信号,填写“SIG_IGN”; 采用系统默认方式处理该信号,填写“SIG_DFL”; 捕获到信号后执行此函数内容, 定义格式为“typedef void (*sighandler_t)(int)”,sighandler_t 代表一个函数指针。 |
返回值 | 调用成功返回最后一次注册信号调用 signal()时的 handler 值;失败返回 SIG_ERR。 |
功能 | 改变收到信号后的动作。 |
实验 1 代码实现信号忽略
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(void)
{signal(SIGINT,SIG_IGN);while(1){printf("wait signal\n");sleep(1);}return 0;
}
编译运行程序,如下图所示,当我们按下 ctrl+C 键的时候,信号被忽略。

实验 2:代码实现采用系统默认方式处理该信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(void)
{signal(SIGINT,SIG_DFL);while(1){printf("wait signal\n");sleep(1);}return 0;
}
编译运行程序,如下图所示,按 ctrl+c 可以终止程序。
实验 3 代码实现捕获到信号后执行此函数内容
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void myfun(int sig)
{if(sig == SIGINT){printf("get sigint\n");}
}
int main(void)
{signal(SIGINT,myfun);while(1){sleep(1);printf("wait signal\n");}return 0;
}
编译运行程序如下图所示,当我们按下 ctrl+c 时,显示 myfun 函数里面的打印信息。

我们再打开另一个终端,输入如下图所示的命令也可以实现同样的效果。
4.共享内存
共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
Linux 操作系统的进程通常使用的是虚拟内存,虚拟内存空间是有由物理内存映射而来的。System V 共享内存能够实现让两个或多个进程访问同一段物理内存空间,达到数据交互的效果。

共享内存和其他进程间数据交互方式相比,有以下几个突出特点:
- 速度快,因为共享内存不需要内核控制,所以没有系统调用。而且没有向内核拷贝数据的过程,所以效率和前面几个相比是最快的,可以用来进行批量数据的传输,比如图片。
- 没有同步机制,需要借助 Linux 提供其他工具来进行同步,通常使用信号灯。
使用共享内存的步骤:
- 调用 shmget()创建共享内存段 id,
- 调用 shmat()将 id 标识的共享内存段加到进程的虚拟地址空间,
- 访问加入到进程的那部分映射后地址空间,可用 IO 操作读写。
常用 API 如下:
函数 | int shmget(key_t key, size_t size, int shmflg) |
参数 key | 由 ftok 生成的 key 标识,标识系统的唯一 IPC 资源 |
参数 size | 需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页 是 4k 字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。 |
参数 shmflg | 如果要创建新的共享内存,需要使用 IPC_CREAT,IPC_EXCL,如果是已经存在的,可以使用 IPC_CREAT 或直接传 0 |
返回值 | 成功时返回一个新建或已经存在的的共享内存标识符,取决于 shmflg 的参数。 失败返回-1 并设置错误码。 |
功能 | 创建共享内存 |
函数 | key_t ftok(const char *pathname, int proj_id) |
头文件 | #include <sys/types.h> #include <sys/ipc.h> |
参数 const char *pathname | 文件路径以及文件名 |
参数 int proj_id | 字符 |
返回值 | 成功返回 key 值,失败返回-1 |
功能 | 建立 IPC 通讯(如消息队列、共享内存时)必须指定一个 ID 值。通常情况下,该 id 值通过 ftok 函数得到。 |
函数 | void *shmat(int shmid, const void *shmaddr, int shmflg) |
头文件 | #include <sys/types.h> #include <sys/shm.h> |
参数 int shmid | 共享内存的标识符,也就是 shmget 函数的返回值 |
参数 const void *shmaddr | 映射到的地址,一般写 NULL,NULL 为系统自动帮我完成映射 |
参数 int shmflg | 通常为 0,表示共享内存可读可写,或者为 SHM_RDONLY,表示共享内存可读可写 |
返回值 | 成功返回共享内存映射到进程中的地址,失败返回-1 |
功能 | 挂接共享内存 |
函数 | int shmdt(const void *shmaddr) |
头文件 | #include <sys/types.h> #include <sys/shm.h> |
参数 const void *shmaddr | 共享内存映射后的地址 |
返回值 | 成功返回 0,失败返回-1 |
功能 | 去关联共享内存 |
注意 | shmdt 函数是将进程中的地址映射删除,也就是说当一个进程不需要共享内存的时候,就可以使用这个函数将他从进程地址空间中脱离,并不会删除内核里面的共享内存对象。 |
函数 | int shmctl(int shmid, int cmd, struct shmid_ds *buf) |
头文件 | #include <sys/ipc.h> #include <sys/shm.h> |
参数 int shmid | 要删除的共享内存的标识符 |
参数 int cmd | IPC_STAT (获取对象属性) IPC_SET (设置对象属性) IPC_RMID(删除对象) |
参数 struct shmid_ds *buf | 指定 IPC_STAT (获取对象属性) IPC_SET (设置对象属性) 时用来保存或者设置的属性 |
功能 | 销毁共享内存 |
实验代码:
在程序中,创建共享内存。
程序中,创建共享内存。
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{int shmid;shmid = shmget(IPC_PRIVATE, 1024, 0777);if (shmid < 0){printf("shmget is error\n");return -1;}printf("shmget is ok and shmid is %d\n", shmid);return 0;
}
编译运行程序如下图所示:

输入以下命令查看到创建的共享内存段的 id 和上面程序获取到的共享内存段的 id 是一样的。

实验代码
在程序中,父子进程通过共享内存通信。
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{int shmid;key_t key;pid_t pid;char *s_addr, *p_addr;key = ftok("./a.c", 'a');shmid = shmget(key, 1024, 0777 | IPC_CREAT);if (shmid < 0){printf("shmget is error\n");return -1;}printf("shmget is ok and shmid is %d\n", shmid);pid = fork();if (pid > 0){p_addr = shmat(shmid, NULL, 0);strncpy(p_addr, "hello", 5);wait(NULL);exit(0);}if (pid == 0){sleep(2);s_addr = shmat(shmid, NULL, 0);printf("s_addr is %s\n", s_addr);exit(0);}return 0;
}
编译运行程序如下图所示:

优点:我们可以看到使用共享内存进行进程之间的通信是非常方便的,而且函数的接口也比较简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,加快了程序的效率。
缺点:共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借助其他手段来保证进程之间的同步工作。
5.消息队列
System V IPC 包含三种进程间通信机制,有消息队列,信号灯(也叫信号量),共享内存。此外还有 System V IPC 的补充版本 POSIX IPC,这两组 IPC 的通信方法基本一致,本章以 System V IPC 为例介绍 Linux 进程通信机制。
可以用命令“ipcs”查看三种 IPC,“ipcrm”删除 IPC 对象。在 i.MX6ULL 终结者开发板终端输入“ipcs”查看系统中存在的 IPC 信息:

这些 IPC 对象存在于内核空间,应用层使用 IPC 通信的步骤为:

1. 获取 key 值,内核会将 key 值映射成 IPC 标识符,获取 key 值常用方法:
(1)在 get 调用中将 IPC_PRIVATE 常量作为 key 值。
(2)使用 ftok()生成 key。
2. 执行 IPC get 调用,通过 key 获取整数 IPC 标识符 id,每个 id 表示一个 IPC 对象。
接口 | 消息队列 | 共享内存 | 信号灯 |
创建/打开对象 | msgget() | shmget() | semget() |
3. 通过 id 访问 IPC 对象。
接口 | 消息队列 | 共享内存 | 信号灯 |
读写/调整 | msgsnd()/msgrcv() | shmat() | semop() |
4.通过 id 控制 IPC 对象
接口 | 消息队列 | 共享内存 | 信号灯 |
控制 | msgctl() | shmctl() | semctl() |
创建这三种 IPC 对象都要先获取 key 值,然后根据 key 获取 id,用到的函数如下:
函数 | key_t ftok(const char *pathname, int proj_id) |
头文件 | #include <sys/types.h> #include <sys/ipc.h> |
参数pathname | 路径名以及文件名 |
参数 proj_id | 同一个文件根据此值生成多个 key 值,int 型或字符型,多个若想访问同一 IPC 对象,此值必须相同。 |
返回值 | 成功返回 key 值,失败返回-1 |
功能 | 建立 IPC 通讯(如消息队列、共享内存时)必须指定一个 ID 值。通常情况下,该 id 值通过 ftok 函数得到。 |
下面介绍消息队列:
- 消息队列是类 unix 系统中一种数据传输的机制,其他操作系统中也实现了这种机制,可见这种通信机制在操作系统中有重要地位。
- Linux 内核为每个消息队列对象维护一个 msqid_ds,每个 msqid_ds 对应一个 id,消息以链表形式存储,并且 msqid_ds 存放着这个链表的信息。

消息队列的特点:
- 发出的消息以链表形式存储,相当于一个列表,进程可以根据 id 向对应的“列表”增加和获取消息。
- 进程接收数据时可以按照类型从队列中获取数据。
消息队列的使用步骤:
- 创建 key;
- msgget()通过 key 创建(或打开)消息队列对象 id;
- 使用 msgsnd()/msgrcv()进行收发;
- 通过 msgctl()删除 ipc 对象
通过 msgget()调用获取到 id 后即可使用消息队列访问 IPC 对象,消息队列常用 API 如下:
函数 | int msgget(key_t key, int msgflg) |
头文件 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> |
参数 key_t key | 和消息队列相关的 key 值 |
参数 int msgflg | 访问权限 |
返回值 | 成功返回消息队列的 ID,失败-1 |
功能 | 获取 IPC 对象唯一标识 id |
函数 | int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg) |
头文件 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> |
参数 int msqid | 消息队列 ID |
参数 const void *msgp | 指向消息类型的指针 |
参数 size_t msgsz | 发送的消息的字节数。 |
参数 int msgflg | 如果为 0,直到发送完成函数才返回,即阻塞发送 IPC_NOWAIT:消息没有发送完成, 函数也会返回,即非阻塞发送 |
返回值 | 成功返回 0,失败返回-1 |
功能 | 发送数据 |
函数 | int msgctl(int msqid, int cmd, struct msqid_ds *buf) |
头文件 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> |
参数 int msqid | 消息队列的 ID |
参数 int cmd | IPC_STAT:读取消息队列的属性,然后把它保存在 buf 指向的缓冲区。 IPC_SET:设置消息队列的属性,这个值取自 buf 参数 IPC_RMID:删除消息队列 |
参数 struct msqid_ds *buf | 消息队列的缓冲区 |
返回值 | 成功返回 0,失败返回-1 |
功能 | 控制操作,删除消息队列对象等 |
函数 | ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg) |
参数 msqid | IPC 对象对应的 id |
参数 msgp | 消息指针,消息包含类型和字段 |
参数 msgsz | 消息里的字段大小 |
参数 msgtyp | 消息里的类型 |
参数 msgflg | 位掩码,不止一个 |
返回值 | 成功返回接收到的字段大小,错误返回-1 |
功能 | 接收消息 |
实验代码
a.c 向消息队列里面写
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{long mtype;char mtext[128];
};
int main(void)
{int msgid;key_t key;struct msgbuf msg;//获取 key 值key = ftok("./a.c", 'a');//获取到 id 后即可使用消息队列访问 IPC 对象msgid = msgget(key, 0666 | IPC_CREAT);if (msgid < 0){printf("msgget is error\n");return -1;}printf("msgget is ok and msgid is %d \n", msgid);msg.mtype = 1;strncpy(msg.mtext, "hello", 5);//发送数据msgsnd(msgid, &msg, strlen(msg.mtext), 0);return 0;
}
b.c 从消息队列里面读
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{long mtype;char mtext[128];
};
int main(void)
{int msgid;key_t key;struct msgbuf msg;key = ftok("./a.c", 'a');//获取到 id 后即可使用消息队列访问 IPC 对象msgid = msgget(key, 0666 | IPC_CREAT);if (msgid < 0){printf("msgget is error\n");return -1;}printf("msgget is ok and msgid is %d \n", msgid);//接收数据msgrcv(msgid, (void *)&msg, 128, 0, 0);printf("msg.mtype is %ld \n", msg.mtype);printf("msg.mtext is %s \n", msg.mtext);return 0;
}
在 Ubuntu 上开一个终端,编译运行 a 如下图所示:

输入以下命令查看如下图所示:

在 Ubuntu 再开一个终端,编译运行 b 如下图所示,成功从消息队列里面读到信息。

6.信号量
本章节将讲述另一种进程间通信的机制——信号量。注意请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物。为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即 P(信号变量))和发送(即 V(信号变量))信息操作。最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。
由于信号量只能进行两种操作等待和发送信号,即 P(sv)和 V(sv),他们的行为是这样的:
P(sv):如果 sv 的值大于零,就给它减 1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待 sv 而被挂起,就让它恢复运行,如果没有进程因等待 sv 而挂起,就给它加 1。
举个例子,就是两个进程共享信号量 sv,一旦其中一个进程执行了 P(sv)操作,它将得到信号量,并可以进入临界区,使 sv 减 1。而第二个进程将被阻止进入临界区,因为当它试图执行 P(sv)时,sv 为 0,它会被挂起以等待第一个进程离开临界区域并执行 V(sv)释放信号量,这时第二个进程就可以恢复执行。
信号灯也叫信号量,它能够用来同步进程的动作,不能传输数据。它的应用场景就像红绿灯,控制各进程使用共享资源的顺序。Posix 无名信号灯用于线程同步, Posix 有名信号灯,System V 信号灯。信号灯相当于一个值大于或等于 0 计数器,信号灯值大于 0,进程就可以申请资源,信号灯值-1,如果信号灯值为0,一个进程还想对它进行-1,那么这个进程就会阻塞,直到信号灯值大于 1。
使用 System V 信号灯的步骤如下:
- 使用 semget()创建或打开一个信号灯集。
- 使用 semctl()初始化信号灯集,。
- 使用 semop()操作信号灯值,即进行 P/V 操作。
- P 操作:申请资源,申清完后信号灯值-1;
- V 操作:释放资源,释放资源后信号灯值+1;
Linux 提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件 sys/sem.h 中。
函数 | int semget(key_t key, int nsems, int semflg) |
头文件 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
参数 key_t key | 信号量的键值 |
参数 int nsems | 信号量的数量 |
参数 int semflg | 标识 |
返回值 | 成功返回信号量的 ID,失败返回-1 |
功能 | 创建一个新信号量或取得一个已有信号量 |
函数 | int semctl(int semid, int semnum, int cmd, union semun arg) |
头文件 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
参数 int semid | 信号量 ID |
参数 int semnum | 信号量编号 |
参数 cmd | IPC_STAT(获取信号量的属性) IPC_SET(设置信号量的属性)IPC_RMID (删除信号量) SETVAL(设置信号量的值) |
参数 arg | union semun { int val; struct semid_ds *buf; unsigned short *array; struct seminfo *__buf; } |
功能 | 初始化信号灯集合 |
函数 | int semop(int semid, struct sembuf *sops, size_t nsops) |
头文件 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
参数 int semid | 信号量 ID |
参数 struct sembuf *sops | 信号量结构体数组 |
参数 size_t nsop | 要操作信号量的数量 struct sembuf{ unsigned short sem_num; //要操作的信号量的编号 short sem_op; //P/V 操作,1 为 V 操作,释放资源。-1 为 P 操作,分配资源。0 为等待,直到信号量的值变成 0 short sem_flg; //0 表示阻塞,IPC_NOWAIT 表示非阻塞 } |
功能 | 在信号量上执行一个或多个操作。 |
实验代码:
指定哪个进程运行,可以使用进程间通信的知识,或者使用信号量,这里以使用信号量为例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
union semun
{int val;
};
int main(void)
{int semid;int key;pid_t pid;struct sembuf sem;union semun semun_union;key = ftok("./a.c", 0666);semid = semget(key, 1, 0666 | IPC_CREAT);semun_union.val = 0;semctl(semid, 0, SETVAL, semun_union);pid = fork();if (pid > 0){sem.sem_num = 0;sem.sem_op = -1;sem.sem_flg = 0;semop(semid, &sem, 1);printf("This is parents\n");sem.sem_num = 0;sem.sem_op = 1;sem.sem_flg = 0;semop(semid, &sem, 1);}if (pid == 0){sleep(2);sem.sem_num = 0;sem.sem_op = 1;sem.sem_flg = 0;semop(semid, &sem, 1);printf("This is son\n");}return 0;
}
编译运行程序如下图所示:

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即 P(信号变量))和发送(即 V(信号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。