嵌入式Linux学习笔记(5)-进程间常见通讯方式(c语言实现)
一、概述
进程间通信(IPC,InterProcess Communication)是指在多个进程之间进行数据传输和共享的机制。在操作系统中,进程是运行中的程序的实例,每个进程都有自己的内存空间和资源。
进程间通信可以用于在不同的进程之间传递数据、共享资源、进行协同工作等。常见的进程间通信方式有以下几种:
-
管道(Pipe):管道是一种半双工的通信方式,它是通过创建一个管道文件用于两个进程之间的通信。一个进程可以将数据写入管道,而另一个进程可以从管道中读取数据。
-
有名管道(Named Pipe):有名管道也是一种管道,不同之处在于它是一个带有名称的管道,可以用于多个进程之间的通信。
-
共享内存(Shared Memory):共享内存是一种进程间通信的高效方式,它允许多个进程访问同一块内存区域,从而实现数据共享。
-
消息队列(Message Queue):消息队列是一种按照先进先出(FIFO)原则进行数据传输的机制。进程可以将消息放入队列,而其他进程可以从队列中读取消息。
-
信号量(Semaphore):信号量是一个特殊的变量,用于控制多个进程之间的临界区访问。通过信号量可以实现进程的互斥和同步操作。
-
套接字(Socket):套接字是一种网络编程中常用的进程间通信方式,它可以在不同的主机之间进行数据传输和通信。
二、管道
1、用法
管道(pipe)是一种在进程间通信中非常常用的机制,它可以将一个进程的输出(stdout)直接作为另一个进程的输入(stdin)。
pipe()函数:创建管道使用,它的原型为:
int pipe(int pipefd[2]);
该函数需要传入一个整型的数组,数组的长度必须是2。该函数会创建一个管道,并将管道的读写句柄分别存储在pipefd[0]和pipefd[1]中。pipefd[0]用于读取管道中的数据,pipefd[1]用于写入管道。
常用函数有:
read()函数:从管道中读取数据。其原型为:
ssize_t read(int fd, void *buf, size_t count);
其中,fd是管道的读取句柄(即pipefd[0]),buf是存储读取数据的缓冲区,count是读取的最大字节数。该函数返回实际读取的字节数。如果从终端读取数据,fd 的值为 STDIN_FILENO。
write()函数:向管道中写入数据。其原型为:
ssize_t write(int fd, const void *buf, size_t count);
其中,fd是管道的写入句柄(即pipefd[1]),buf是待写入的数据的缓冲区,count是待写入的字节数。该函数返回实际写入的字节数。如果往终端写入数据,fd 的值为 STDOUT_FILENO。
close()函数:关闭管道的读取或写入端。其原型为:
int close(int fd);
其中,fd是待关闭的管道的句柄。
2、用例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc, char const *argv[])
{
pid_t pid;
int pipefd[2];
if (argc != 2)
{
fprintf(stderr,"%s: 参数不够!\n",argv[0]);
exit(EXIT_FAILURE);
}
//创建管道
if (pipe(pipefd) == -1)
{
perror("创建管道数据");
exit(EXIT_FAILURE);
}
//创建子进程
pid = fork();
if (pid < 0)
{
fprintf(stderr,"%s: 邀请新学员失败!\n",argv[0]);
exit(EXIT_FAILURE);
}
else if (pid == 0)
{
//读取管道数据并打印
close(pipefd[1]);
char str[100]={0};
sprintf(str,"%d 读出数据",getpid());
write(STDOUT_FILENO,&str,sizeof(str));
char buf;
while (read(pipefd[0],&buf,1)>0)
{
write(STDOUT_FILENO,&buf,1);
}
write(STDOUT_FILENO,"\n",1);
close(pipefd[0]);
_exit(EXIT_SUCCESS);//直接退出不做任何操作
}
else
{
//写入管道数据,提供给子进程
close(pipefd[0]);
printf("%d 写入数据\n",getpid());
write(pipefd[1],argv[1],strlen(argv[1]));
close(pipefd[1]);
waitpid(pid,NULL,0);
}
return 0;
}
这个用例创建了一个子进程。随后父进程写入数据,子进程读取数据。
三、有名管道
1、用法
有名管道(Named Pipe)是一种在操作系统中用于进程间通信的通道。它可以在不相关的进程之间传递数据。以下是对有名管道及常用函数的详细介绍。
有名管道是一种特殊的文件,它以文件名的形式存在于文件系统中。与普通文件不同的是,有名管道允许不相关的进程之间进行通信。有名管道是以FIFO(先进先出)的方式传递数据,即先进入管道的数据首先被读取出来。有名管道可以用于在父进程和子进程之间传递数据,也可以用于不同进程之间的通信。
mkfifo()
:创建一个新的有名管道。它的原型为:
int mkfifo(const char *pathname, mode_t mode)
其中,pathname
是管道的路径名,mode
定义了管道的权限。该函数成功时返回0,失败时返回-1。
open()
:打开有名管道以进行读取或写入操作。它的原型为:
int open(const char *pathname, int flags)
其中,pathname
是管道的路径名,flags
定义了打开的方式和权限。该函数成功时返回文件描述符,失败时返回-1。
其余函数如:read,write,close与管道用法一致。
有名管道提供了一种简单的进程间通信方式,但也有一些限制。首先,它只适用于在同一计算机上的进程间通信。其次,有名管道是阻塞式的,即读取操作会一直等待直到有数据可读,写入操作会一直等待直到有空间可写。因此,在使用有名管道时要注意避免进程间出现死锁的情况。
2、用例
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
int fd;
//有名管道创建完可重复使用,但推荐每次用完删除,下次重新创建
char *pipe_path = "/tmp/myfifo";
if(mkfifo(pipe_path,0644) != 0){
perror("mkfifo");
if (errno != 17)//不是文件已存在
{
exit(EXIT_FAILURE);
}
}
fd = open(pipe_path,O_WRONLY);
if(fd == -1){
perror("open");
exit(EXIT_FAILURE);
}
char buf[100];
ssize_t read_num;
//从终端写入数据到管道中
while ((read_num = read(STDIN_FILENO,buf,100)) > 0)
{
write(fd,buf,read_num);
}
if (read_num < 0)
{
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("发送数据到管道完成,进程终止\n");
close(fd);
//清除管道
//清除对应特殊文件
if (unlink(pipe_path) == -1)
{
perror("unlink");
}
return 0;
}
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
int fd;
char *pipe_path = "/tmp/myfifo";
// if(mkfifo(pipe_path,0644) != 0){
// perror("mkfifo");
// exit(EXIT_FAILURE);
// }
fd = open(pipe_path,O_RDONLY);
if(fd == -1){
perror("open");
exit(EXIT_FAILURE);
}
char buf[100];
ssize_t read_num;
//读取管道信息
while ((read_num = read(fd,buf,100)) > 0)
{
write(STDOUT_FILENO,buf,read_num);
}
if (read_num < 0)
{
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("接收管道数据完成,进程终止\n");
close(fd);
return 0;
}
两个.c文件分别代表两个无关联的进程。一个从终端接收数据并将数据写入有名管道中,另一个从管道中接收数据并打印在终端。
四、共享内存
1、用法
在C语言中,进程间通信(IPC)是实现不同进程之间数据交换和共享的重要方式之一。共享内存是一种高效的IPC机制,可以实现不同进程之间的数据共享。
共享内存是一块由操作系统管理的内存区域,可以被多个进程同时访问。进程可以将共享内存映射到自己的地址空间中,从而可以直接读写该内存区域,实现数据共享。
shm_open():该函数用于创建或打开一个共享内存对象。它的原型如下:
int shm_open(const char *name, int oflag, mode_t mode);
参数:
name:共享内存对象的名称
oflag:打开标志,用于指定打开方式和操作权限
mode:权限位,用于指定该共享内存对象的操作权限
返回值:共享内存对象的文件描述符,若打开或创建失败则返回-1。
shm_unlink():该函数用于删除一个共享内存对象。它的原型如下:
int shm_unlink(const char *name);
参数:
name:共享内存对象的名称
返回值:成功返回0,失败返回-1。
mmap():该函数用于将共享内存映射到进程的地址空间中。它的原型如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
addr:指定映射的起始地址,通常设为NULL,让操作系统自动选择一个合适的地址。
length:映射的长度,单位为字节。
prot:映射内存区域的保护模式,如读、写、执行等。
flags:映射的标志,用于指定映射方式和映射类型。
fd:共享内存对象的文件描述符。
offset:共享内存对象中的偏移量。
返回值:映射成功时返回映射区域的起始地址,失败时返回MAP_FAILED。
munmap
():函数用于释放由mmap
函数映射的内存区域。它的原型如下:
int munmap(void *addr, size_t length);
参数:
addr
:指向要释放的内存区域的起始地址。这个地址必须是mmap
返回的地址,或者是页面大小的整数倍。
length
:要释放的内存区域的大小。
返回值:成功时,返回0;失败时,返回-1,并设置errno
。
2、用例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
char *share;
// 1.创建共享内存对象
char shm_name[100]={0};
sprintf(shm_name,"/letter%d",getpid());
int fd;
fd = shm_open(shm_name,O_RDWR | O_CREAT,0644);
if (fd < 0)
{
perror("shm_open");
exit(EXIT_FAILURE);
}
// 2.设置共享内存对象大小
ftruncate(fd,1024);
// 3.内存映射
share = mmap(NULL,1024,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if (share == MAP_FAILED)
{
perror("mmap");
exit(EXIT_FAILURE);
}
// 映射后关闭fd,不是释放
close(fd);
// 4.使用内存映射实现进程间的通讯
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}else if (pid == 0)
{
strcpy(share,"hello,my name is liuyuhui\n");
printf("子进程%d:发送完毕!\n",getpid());
}else{
waitpid(pid,NULL,0);
printf("父进程 %d 接收到了子进程 %d 发送的消息: %s\n",getpid(),pid,share);
// 5.释放映射区
int re = munmap(share,1024);
if (re == -1)
{
perror("munmap");
exit(EXIT_FAILURE);
}
}
// 6.释放共享内存对象
shm_unlink(shm_name);
return 0;
}
共享内存大致就分了这六步。这个例程实现的是父进程向共享内存中写入数据,子进程从共享内存中读出数据。
五、消息队列
1、用法
在C语言中,进程间通讯是指两个或多个进程之间进行数据交换或共享资源的过程。消息队列是一种进程间通讯的方式,允许进程通过发送和接收消息来交换数据。
mq_open():用于创建或打开一个消息队列。函数原型如下:
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
参数:
name:消息队列的名称。
oflag:打开标志。可以是O_CREAT(如果消息队列不存在,则创建)和O_EXCL(与O_CREAT一起使用,确保创建新的消息队列)。
mode:权限和创建文件时的权限位掩码(只有在创建消息队列时才使用)。attr:指向消息队列属性的指针。
mq_unlink():用于删除一个已经命名的消息队列。函数原型如下:
int mq_unlink(const char *name);
参数:
name:消息队列的名称。
clock_gettime():用于获取当前时间。函数原型如下:
int clock_gettime(clockid_t clk_id, struct timespec *tp);
clk_id:时钟ID,可以是CLOCK_REALTIME(系统实时时间)或CLOCK_MONOTONIC(运行时间)。
tp:保存时间的结构体指针。
mq_timedsend():用于向消息队列发送消息,并可以设置发送超时时间。函数原型如下:
int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio, const struct timespec *abs_timeout);
参数:
mqdes:消息队列描述符。
msg_ptr:指向发送消息的指针。
msg_len:发送消息的长度。
msg_prio:消息的优先级。
abs_timeout:绝对时间,若超过该时间则发送超时。
mq_timedreceive():用于从消息队列接收消息,并可以设置接收超时时间。函数原型如下:
ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio, const struct timespec *abs_timeout);
参数:
mqdes:消息队列描述符。
msg_ptr:指向接收消息的指针。
msg_len:接收消息的最大长度。
msg_prio:接收到的消息的优先级。
abs_timeout:绝对时间,若超过该时间则接收超时
2、用例
①:父子进程间通讯
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
struct mq_attr attr;
attr.mq_maxmsg=10;
attr.mq_msgsize=100;
attr.mq_flags=0;
attr.mq_curmsgs=0;
//创建消息队列
char *mq_name = "/father_son";
mqd_t mqdes = mq_open(mq_name,O_RDWR|O_CREAT,0644,&attr);
if (mqdes == -1)
{
perror("mq_open");
exit(EXIT_FAILURE);
}
//创建进程
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}else if (pid == 0)
{
//子进程接收消息
char read_buf[100];
struct timespec time_info;
//接受数据
for (int i = 0; i < 10; i++)
{
//清空并发送数据
memset(read_buf,0,100);
//设置等待时间
clock_gettime(0,&time_info);
time_info.tv_sec += 15;
//接收消息
if(mq_timedreceive(mqdes,read_buf,100,NULL,&time_info) == -1){
perror("mq_timedreceive");
}
printf("子进程接收到消息:%s\n",read_buf);
}
}else{
//父进程发送消息
char send_buf[100];
struct timespec time_info;
for (int i = 0; i < 10; i++)
{
//清空并发送数据
memset(send_buf,0,100);
sprintf(send_buf,"父进程发送的第 %d 条消息\n",(i+1));
//获取当前时间
clock_gettime(0,&time_info);
time_info.tv_sec += 5;
//发送消息
if(mq_timedsend(mqdes,send_buf,strlen(send_buf),0,&time_info) == -1){
perror("mq_timedsend");
}
printf("父进程休息1s\n");
sleep(1);
}
}
//释放两次消息队列的引用
close(mqdes);
//清除一次消息队列
if (pid > 0)
{
mq_unlink(mq_name);
}
return 0;
}
②:不同进程间通讯
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
struct mq_attr attr;
attr.mq_maxmsg=10;
attr.mq_msgsize=100;
attr.mq_flags=0;
attr.mq_curmsgs=0;
//创建消息队列
char *mq_name = "/p_c_mq";
mqd_t mqdes = mq_open(mq_name,O_RDWR|O_CREAT,0644,&attr);
if (mqdes == -1)
{
perror("mq_open");
exit(EXIT_FAILURE);
}
char write_buf[100];
struct timespec time_info;
//不断接收控制台消息 发送到消息队列
while (1)
{
memset(write_buf,0,100);
ssize_t read_count = read(STDIN_FILENO,write_buf,100);
clock_gettime(0,&time_info);
time_info.tv_sec += 5;
if (read_count == -1)
{
perror("read");
continue;
}else if (read_count == 0)
{
//ctrl + d 退出时
printf("EOF,exit...\n");
char eof = EOF;
if (mq_timedsend(mqdes,&eof,1,0,&time_info) == -1)
{
perror("mq_timedsend");
}
break;
}
//正常发送
if (mq_timedsend(mqdes,write_buf,strlen(write_buf),0,&time_info) == -1)
{
perror("mq_timedsend");
}
printf("正常发送成功!\n");
}
//释放消息队列描述符
close(mqdes);
//清除消息队列 (消费者执行一次即可)
// mq_unlink(mq_name);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
struct mq_attr attr;
attr.mq_maxmsg=10;
attr.mq_msgsize=100;
attr.mq_flags=0;
attr.mq_curmsgs=0;
//创建消息队列
char *mq_name = "/p_c_mq";
mqd_t mqdes = mq_open(mq_name,O_RDWR|O_CREAT,0644,&attr);
if (mqdes == -1)
{
perror("mq_open");
exit(EXIT_FAILURE);
}
char read_buf[100];
struct timespec time_info;
//不断接收控制台消息 发送到消息队列
while (1)
{
memset(read_buf,0,100);
clock_gettime(0,&time_info);
time_info.tv_sec += 15;
//正常接收
if (mq_timedreceive(mqdes,read_buf,100,NULL,&time_info) == -1)
{
perror("mq_timedreceive");
}
//判断是否结束
if (read_buf[0] == EOF)
{
printf("收到结束信息,准备退出...\n");
break;
}
printf("正常接收成功,信息为:%s\n",read_buf);
}
//释放消息队列描述符
close(mqdes);
//清除消息队列
mq_unlink(mq_name);
return 0;
}
需要注意的是因为公用同一片资源,所以在不同进程间通讯时接收方只需接收完消息后将消息队列清除掉即可,发送方不需要重复清除,也可避免接收方还没读取完消息队列的值就被发送方清除了消息队列。