目录
前言
写这篇文章是想让初学者了解Linux进程间通信的整体框架 以及使用方法,还有就是此文章可以当成快速开发手册,直接复制修改其中的程序到自己的项目中。最近实习太忙了,文章的最后还有两个通信方式没有具体讲解。有时间补上
一、Linux 进程通信有哪些
在Linux系统中,进程之间可以通过多种方式进行通信,这些通信方式可以用于在不同进程之间传递信息、共享数据等。以下是一些常见的 Linux 进程间通信方式:
1.管道(Pipe)
描述: 管道是一种半双工的通信方式,它连接一个写进程和一个读进程,允许单向的数据流动。
创建: 使用 pipe 系统调用创建,可以通过 fork 创建的子进程间使用管道进行通信。
2.命名管道(Named Pipe或FIFO)
描述: 与管道类似,但它是一种有名字的管道,它允许在不相关的进程之间进行通信。
创建: 使用 mkfifo 命令或相关系统调用。
3.消息队列(Message Queue)
描述: 进程可以通过消息队列发送消息,消息队列是一个消息的链表,多个进程可以通过它进行通信。
创建: 使用 msgget、msgsnd 和 msgrcv 等系统调用。
4.信号(Signal)
描述: 进程可以通过信号向另一个进程发送异步通知,用于通知某个事件的发生。
使用: 使用 kill 命令或相关系统调用发送信号,接收信号可以通过注册信号处理函数。
5.共享内存(Shared Memory)
描述: 进程之间可以映射同一块物理内存,实现共享数据。
创建: 使用 shmget、shmat 和 shmdt 等系统调用。
6.信号量(Semaphore)
描述: 用于进程间的同步和互斥,可以用于保护共享资源。
创建: 使用 semget、semop 等系统调用。
7.套接字(Socket)
描述: 进程可以通过套接字在网络上或本地进行通信。
创建: 使用 socket、bind、connect 等系统调用。
8.文件锁(File Locking)
描述: 进程可以使用文件锁定机制来实现对文件的互斥访问。
创建: 使用 fcntl 等系统调用。
二、管道(Pipe)
1.管道如何实现进程间的通信
(1)父进程创建管道,得到两个件描述符指向管道的两端
(2)父进程fork出子进程,子进程也有两个文件描述符指向同管道。
(3)父进程关闭fd[1],子进程关闭fd[0],即子进程关闭管道读端,父进程关闭管道写端(因为管道 只支持单向通信)。子进程可以往管道中写,父进程可以从管道中读,管道是由环形队列实现 的,数据从写端流入从读端流出,这样就实现了进程间通信。
2.管道特点
(1)管道只允许具有血缘关系的进程间通信,如父子进程间的通信。
(2)管道只允许单向通信。
(2)管道内部保证同步机制,从而保证访问数据的一致性。
(4)面向字节流
(5)管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消 失。
(6)大小64K
3.管道读取数据的四种的情况
(1)读端不读(fd[0]未关闭),写端一直写
(2)写端不写(fd[1]未关闭),但是读端一直读
(3)读端一直读,且fd[0]保持打开,而写端写了一部分数据不写了,并且关闭fd[1]
(4)读端读了一部分数据,不读了且关闭fd[0],写端一直在写且f[1]还保持打开状态
4.示例程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFFER_SIZE 1024
int main() {
int pipefd[2]; // 创建管道的文件描述符数组,0是读端,1是写端
pid_t pid;
char buffer[BUFFER_SIZE];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid = fork(); // 创建子进程
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid > 0) { // 父进程
close(pipefd[0]); // 关闭读端
// 向管道写入数据
char message[] = "Hello, child process!\n";
write(pipefd[1], message, sizeof(message));
close(pipefd[1]); // 关闭写端
printf("Parent process completed.\n");
} else { // 子进程
close(pipefd[1]); // 关闭写端
// 从管道读取数据
int nbytes = read(pipefd[0], buffer, BUFFER_SIZE);
printf("Child process received message: %s", buffer);
close(pipefd[0]); // 关闭读端
printf("Child process completed.\n");
}
return 0;
}
输出
Child process received message: Hello, child process!
Child process completed.
Parent process completed.
三.命名管道(Named Pipe或FIFO)
1.mkfifo函数
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
函数说明
mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限(mode%~umask),因此 umask值也会影响到FIFO文件的权限。Mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取。当使用open()来打开 FIFO文件时,O_NONBLOCK旗标会有影响
(1)当使用O_NONBLOCK 旗标时,打开FIFO 文件来读取的操作会立刻返回,但是若还没有其他进程打开FIFO 文件来读取,则写入的操作会返回ENXIO 错误代码。
(2)没有使用O_NONBLOCK 旗标时,打开FIFO 来读取的操作会等到其他进程打开FIFO文件来写入才正常返回。同样地,打开FIFO文件来写入的操作会等到其他进程打开FIFO 文件来读取后才正常返回。
返回值
若成功则返回0,否则返回-1,错误原因存于errno中。
2.特点
(1)命名管道可以用于任何两个进程间的通信,而不限于同源的两个进程。
(2)命名管道作为一种特殊的文件存放在文件系统中,而不是像管道那样存放在内核中。当进程对命名管道的使用结束后,命名管道依然存在于文件系统中,除非对其进行删除操作,否则该命名管道不会自行消失。
(3)和管道一样,命名管道也只能用于数据的单向传输,如果要用命名管道实现两个进程间数据的双向传输,建议使用两个单向的命名管道
3.示例程序
写入
#include <limits.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "/tmp/testp"
#define BUFFER_SIZE 4096
#define TEN_MEG (1024 * 1024 * 10)
int main(void)
{
int pipe_fd;
int res;
int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[BUFFER_SIZE + 1];
if(access(FIFO_NAME, F_OK) == -1)
{
res = mkfifo(FIFO_NAME, 0777);//读写执行
if(res != 0)
{
fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);
exit(EXIT_FAILURE);
}
}
printf("Process %d opening FIFO O_WRONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d opened fd %d\n", getpid(), pipe_fd);
if(pipe_fd != -1)
{
while(bytes_sent < TEN_MEG)
{
res = write(pipe_fd, buffer, BUFFER_SIZE);
if(res == -1)
{
fprintf(stderr, "Write error on pipe\n");
exit(EXIT_FAILURE);
}
bytes_sent += res;
}
(void)close(pipe_fd);
}
else
{
exit(EXIT_FAILURE);
}
printf("Process %d finished\n", getpid());
exit(EXIT_SUCCESS);
}
运行结果
读取
#include <limits.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "/tmp/testp"
#define BUFFER_SIZE 4096
int main(void)
{
int pipe_fd;
int res;
int open_mode = O_RDONLY;
int bytes_read = 0;
char buffer[BUFFER_SIZE + 1];
memset(buffer, '\0', sizeof(buffer));
printf("Process %d opening FIFO O_RDONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d opened fd %d\n", getpid(), pipe_fd);
if(pipe_fd != -1)
{
do
{
res = read(pipe_fd, buffer, BUFFER_SIZE);
bytes_read += res;
} while (res > 0);
(void)close(pipe_fd);
}
else
{
exit(EXIT_FAILURE);
}
printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
exit(EXIT_SUCCESS);
}
运行结果
四.消息队列(Message Queue)
1.Message Queue如何工作?
用户消息缓冲区
无论发送进程还是接收进程,都需要在进程空间中用消息缓冲区来暂存消息。该消息缓冲区的结构定义如下
struct msgbuf {
long mtype; /* 消息的类型 */
char mtext[1]; /* 消息正文 */
};
- 可通过mtype区分数据类型,同过判断mtype,是否为需要接收的数据
- mtext[]为存放消息正文的数组,可以根据消息的大小定义该数组的长度
2.创建消息队列
通过msgget创建消息队列
函数原型如下
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
key: 某个消息队列的名字
msgflg:由九个权限标志构成,用法和创建文件时使用的mode模式标志是一样的,这里举两个来说明
IPC_CREAT
如果消息队列对象不存在,则创建之,否则则进行打开操作
IPC_EXCL
如果消息对象不存在则创建之,否则产生一个错误并返回
返回值:
那么如何获取key值?
通过宏定义key值
通过ftok函数生成key值,这里就不具体介绍ftok函数用法
添加信息到消息队列
向消息队列中添加数据,使用到的是msgsnd()函数
函数原型如下
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
参数:
- msgid: 由msgget函数返回的消息队列标识码
- msg_ptr:是一个指针,指针指向准备发送的消息,
- msg_sz:是msg_ptr指向的消息长度,消息缓冲区结构体中mtext的大小,不包括数据的类型
- msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情
如:msgflg = IPC_NOWAIT 表示队列满不等待,返回EAGAIN错误
返回值:
成功返回0
失败则返回-1
从消息队列中读取消息
从消息队列中读取消息,我们使用msgrcv()函数,
函数原型如下
int msgrcv(int msgid, void *msg_ptr, size_t msgsz,
long int msgtype, int msgflg);
参数:
- msgid: 由msgget函数返回的消息队列标识码
- msg_ptr:是一个指针,指针指向准备接收的消息,
- msgsz:是msg_ptr指向的消息长度,消息缓冲区结构体中mtext的大小,不包括数据的类型
- msgtype:它可以实现接收优先级的简单形式
msgtype=0返回队列第一条信息
msgtype>0返回队列第一条类型等于msgtype的消息
msgtype<0返回队列第一条类型小于等于msgtype绝对值的消息 - msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事
msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。
msgflg=MSG_NOERROR,消息大小超过msgsz时被截断
注意
msgtype>0且msgflg=MSC_EXCEPT,接收类型不等于msgtype的第一条消息
返回值:
- 成功返回实际放到接收缓冲区里去的字符个数
- 失败,则返回-1
消息队列的控制函数
函数原型
int msgctl(int msqid, int command, strcut msqid_ds *buf);
参数:
- msqid: 由msgget函数返回的消息队列标识码
- command:是将要采取的动作,(有三个可取值)分别如下
IPC_STAT
取出系统保存的消息队列的msqid_ds 数据,并将其存入参数buf 指向的msqid_ds 结构
中。
IPC_SET
设定消息队列的msqid_ds 数据中的msg_perm 成员。设定的值由buf 指向的msqid_ds结构给出。
IPC_RMID
将队列从系统内核中删除。
注意:若选择删除队列,第三个参数传NULL
返回值:
如果操作成功,返回“0”;如果失败,则返回“-1”
3.示例程序
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
#define MSGKEY 123
//消息的数据结构是以一个长整型成员变量开始的结构体
struct msgstru
{
long msgtype;
char msgtext[2048];
};
int main()
{
struct msgstru msgs;
char str[256];
int msg_type;
int ret_value;
int msqid;
int pid;
int status = 0;
//检查消息队列是否存在
msqid = msgget(MSGKEY, IPC_EXCL);//(键名,权限)
if (msqid < 0)
{
//创建消息队列
msqid = msgget(MSGKEY, IPC_CREAT | 0666);
if (msqid <0)
{
printf("failed to create msq | errno=%d [%s]\n", errno, strerror(errno));
exit(-1);
}
}
pid = fork();//创建子进程
if (pid > 0)
{
//父进程
while (1)
{
printf("input message type:\n");//输入消息类型
scanf("%d", &msg_type);
if (msg_type == 0)
break;
printf("input message to be sent:\n");//输入消息信息
scanf("%s", str);
msgs.msgtype = msg_type;
strcpy(msgs.msgtext, str);
//发送消息队列(sizeof消息的长度,而不是整个结构体的长度)
ret_value = msgsnd(msqid, &msgs, sizeof(msgs.msgtext), IPC_NOWAIT);
if (ret_value < 0)
{
printf("msgsnd() write msg failed,errno=%d[%s]\n", errno, strerror(errno));
exit(-1);
}
pid_t res = waitpid(pid, &status, WNOHANG);//回收子进程
if (res > 0) {
printf("wait child success!\n");
printf("exit code: %d\n", WEXITSTATUS(status));
break;
}
}
}
else if (pid == 0)
{
//子进程
while (1)
{
msg_type = 1;//接收的消息类型为1
msgs.msgtype = msg_type;
//发送消息队列(sizeof消息的长度,而不是整个结构体的长度)
ret_value = msgrcv(msqid, &msgs, sizeof(msgs.msgtext), msgs.msgtype, IPC_NOWAIT);
if (ret_value > 0)
{
printf("read msg:%s\n", msgs.msgtext);
}
}
}
else
{
printf("fork error\n");
//删除消息队列
msgctl(msqid, IPC_RMID, 0);
exit(1);
}
return 0;
}
运行结果
5.共享内存和信号量(配合使用)
示例程序
写端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
/*创建信号量
初始化信号量
操作信号量(P、V操作)
删除信号量*/
/*创建共享内存
连接共享内存
读写共享内存
断开共享内存
删除共享内存*/
#define SHM_SIZE 1024 // 共享内存大小
// semctl函数控制信号量的相关信息
union semun{
int val; /*信号量的值*/
struct semid_ds *buf; /*信号量集合信息*/
unsigned short *array;/*信号量值的数组*/
struct seminfo *__buf;/*信号量限制信息*/
};
// P上锁
void P(int semid) {
struct sembuf set;
set.sem_num = 0; //信号量在数组里的序号
set.sem_op = -1; //信号量的操作值
set.sem_flg = SEM_UNDO; //信号量的操作标识
semop(semid, &set,1);
}
// V释放
void V(int semid) {
struct sembuf set;
set.sem_num = 0; //信号量在数组里的序号
set.sem_op = 1; //信号量的操作值
set.sem_flg = SEM_UNDO; //信号量的操作标识
semop(semid, &set,1);
}
int main() {
key_t key = ftok("/tmp", 1); // 生成一个唯一的key
// 创建共享内存
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
// 连接到共享内存
char *shm_ptr = shmat(shmid, NULL, 0);
if (shm_ptr == (char*)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
// 创建信号量
int semid = semget(key, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 初始化信号量
union semun seminit;
seminit.val = 1;
semctl(semid, 0, SETVAL, seminit);
// 写入数据到共享内存
P(semid); // 上锁
printf("Write content: ");
fgets(shm_ptr, SHM_SIZE, stdin);
shmdt(shm_ptr); // 分离共享内存
V(semid); // 解锁
shmctl(shmid,IPC_RMID,0); //删除共享内存段
semctl(semid,0,IPC_RMID); //删除信号量组
return 0;
}
读端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
/*获取信号量(标识符)
初始化信号量
操作信号量(P、V操作)*/
/*获取共享内存(标识符)
连接共享内存
读写共享内存
断开共享内存*/
#define SHM_SIZE 1024 // 共享内存大小
// P上锁
void P(int semid) {
struct sembuf set;
set.sem_num = 0; //信号量在数组里的序号
set.sem_op = -1; //信号量的操作值
set.sem_flg = SEM_UNDO; //信号量的操作标识
semop(semid, &set,1);
}
// V释放
void V(int semid) {
struct sembuf set;
set.sem_num = 0; //信号量在数组里的序号
set.sem_op = 1; //信号量的操作值
set.sem_flg = SEM_UNDO; //信号量的操作标识
semop(semid, &set,1);
}
int main() {
key_t key = ftok("/tmp", 1); // 生成一个唯一的key
// 获取共享内存标识符
int shmid = shmget(key, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
// 连接到共享内存
char *shm_ptr = shmat(shmid, NULL, 0);
if (shm_ptr == (char*)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
// 获取信号量标识符
int semid = semget(key, 1, 0666);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 读取共享内存中的数据
P(semid); // 进入临界区
printf("Read content: %s", shm_ptr);
shmdt(shm_ptr);// 分离共享内存
V(semid); // 离开临界区
return 0;
}
总结
如有不足之处还请指正!