Linux系统作为Unix系统的一种延伸,由多个进程共同组成系统环境。为了保证系统的安全和资源的独立性,每个进程都有独立的进程空间,不能够直接访问其它进程的资源。但现实世界的复杂性,不同的进程之间又需要了解和交互部分信息,来满足系统的功能实现。提供机制来实现不同进程之间在可控的环境下进行数据交互就是必要需求,进程间通讯(IPC)就是为了这种目的而诞生的。
本篇参考文档如下。
Linux系统继承了Unix系统的内核通讯机制,提供了多种进程间通讯方式,具体如下所示。
上述就是Linux进程间通讯的主要方式,具体实践目录如下所示。
管道(pipe)是Linux系统提供的一种进程间通信机制,分为匿名管道和命名管道两种,用于进程间单向数据传输。
匿名管道(pipe)是一种单向数据传输的方式,只能用于父子进程之间,用于单向数据传输。pipe相关接口如下所示。
// 头文件
#include <unistd.h>
// 函数说明
// 创建匿名管道,返回两个文件描述符,pipefd[0]为读端,pipefd[1]为写端
int pipe(int pipefd[2]);
// 向管道写入数据
int write(int fd, const void *buf, size_t count);
// 从管道读取数据
int read(int fd, void *buf, size_t count);
// 关闭管道
void close(int fd);
匿名管道的应用如下所示。
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
int fd[2];
int ret, size;
pid_t pid;
char buf[512]= {0};
// 初始化创建匿名管道
ret = pipe(fd);
if (ret < 0) {
printf("pipe init failed\n");
}
// 创建子进程
pid = fork();
if (pid < 0) {
printf("fork failed\n");
} else if (pid == 0) {
//child process
//read pipe fd[0]
size = read(fd[0], buf, sizeof(buf));
if (size <= 0) {
printf("read pipe failed\n");
} else {
buf[size] = '\0';
printf("child read pipe success, readsize=%d, buf=%s\n", size, buf);
}
//write pipe fd[1]
if (write(fd[1], "test for pipe", strlen("test for pipe")) > 0) {
wait(NULL);
} else {
printf("child write pipe failed\n");
}
close(fd[0]);
close(fd[1]);
}
else
{
//father process
//write pipe fd[1]
if (write(fd[1], "child test for pipe", strlen("child test for pipe\n"))>0) {
wait(NULL);
} else {
printf("father write pipe failed\n");
}
//read pipe fd[1]
size = read(fd[0], buf, sizeof(buf));
if (size <= 0) {
printf("father read pipe failed\n");
} else {
buf[size] = '\0';
printf("father read pipe success, readsize=%d, buf=%s\n", size, buf);
}
close(fd[0]);
close(fd[1]);
}
printf("test pipe end\n");
return 0;
}
相关代码参考:匿名管道应用代码
命名管道(fifo)是一种单向数据传输的方式,用于任意进程之间,允许无亲缘关系的进程间通信。fifo相关接口如下所示。
// 头文件
#include <sys/types.h>
#include <sys/stat.h>
// 函数说明
// 创建命名管道,返回文件描述符
int mkfifo(const char *pathname, mode_t mode);
// 移除FIFO管道
int unlink (const char *__name);
// 打开命名管道,返回文件描述符
int open(const char *pathname, int flags);
// 向命名管道写入数据
int write(int fd, const void *buf, size_t count);
// 从命名管道读取数据
int read(int fd, void *buf, size_t count);
// 关闭命名管道
void close(int fd);
命名管道类似于文件访问,需要先创建命名管道,然后再打开命名管道,进行读写操作;应用如下所示。
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define FIFO_NAME "/tmp/1.fifo"
#define FIFO_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define STR_TEST "hello world"
int main(int argc, char* argv[])
{
pid_t system_pid;
int fd;
int bytes;
char buffer[40] = {0};
unlink(FIFO_NAME);
if (mkfifo(FIFO_NAME, FIFO_MODE) < 0) {
if (errno != EEXIST) {
printf("create fifo %s error:%s\n", FIFO_NAME, strerror(errno));
}
return -1;
}
system_pid = fork();
if (system_pid < 0) {
printf("system fork error\n");
}
else if (system_pid == 0) {
// child process
// 打开fifo,以可读写的方式(O_RDONLY也可)
fd = open(FIFO_NAME, O_RDWR);
if (fd < 0) {
printf("open fifo %s error:%s\n", FIFO_NAME, strerror(errno));
return -1;
}
// 从fifo中读取数据
bytes = read(fd, buffer, 40);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("fifo %d read:%s, read size:%d\n", fd, buffer, bytes);
close(fd);
} else {
printf("fifo read %s error:%s\n", FIFO_NAME, strerror(errno));
}
} else {
// 打开fifo,以可读写的方式(O_WRONLY也可)
fd = open(FIFO_NAME, O_RDWR);
if (fd < 0) {
printf("open fifo %s error:%s\n", FIFO_NAME, strerror(errno));
return -1;
}
// 向fifo中写入数据
bytes = write(fd, STR_TEST, strlen(STR_TEST));
printf("fifo %d send:%s, size:%d\n", fd, STR_TEST, bytes);
close(fd);
}
printf("fifo test end\n");
return 0;
}
相关代码参考:命名管道应用代码
信号(signal)是Linux系统提供的一种进程间通信机制,用于进程间的异步通知,用于进程间的事件通知。signal可以用于处理系统信号,包含应用层信号以及内核信号,也可以用于处理用户自定义信号。信号相关接口如下所示。
// 头文件
#include <signal.h>
// 函数说明
// 发送信号
int kill(pid_t pid, int sig);
// 注册信号处理函数
void signal(int sig, void (*handler)(int));
// 向进程发送信号
void raise(int sig); // 发送信号
信号的应用如下所示。
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <sys/time.h>
/*
SIGUSR1 用户定义的信号,可用于应用程序
SIGUSR2 另一个用户定义的信号,可用于应用程序
SIGALRM 当用alarm函数或settimer设置的定时器超时,产生此信号
kill -USR1 <id> 发送SIGUSR1信号
kill -(signo) <id> 向指定进程发送指定信号
*/
static int count = 0;
static void alarm_handler_callback(int signo)
{
printf("alarm_handler_callback:%d\n", ++count);
}
static void virtual_alarm_callback(int signo)
{
printf("virtual_alarm_callback:%d\n", signo);
}
static void signal_usr_callback(int signo)
{
switch (signo)
{
case SIGUSR1:
printf("sigusr1 action\n");
break;
case SIGUSR2:
printf("sigusr2 action\n");
break;
default:
break;
}
}
int main(int argc, char* argv[])
{
struct itimerval tick = {0};
signal(SIGALRM, alarm_handler_callback);
signal(SIGVTALRM, virtual_alarm_callback);
signal(SIGUSR1, signal_usr_callback);
signal(SIGUSR2, signal_usr_callback);
kill(getpid(), SIGUSR1);
raise(SIGUSR2);
alarm(1);
pause();
printf("start setitimer test!\n");
// 设置定时器初次触发时间为1秒,间隔时间也为1秒
tick.it_value.tv_sec = 1;
tick.it_value.tv_usec = 0;
tick.it_interval.tv_sec = 1;
tick.it_interval.tv_usec = 0;
/*
ITIMER_REAL 系统真实的时间来计算,它送出SIGALRM信号
ITIMER_VIRTUAL 以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号。
ITIMER_PROF 以该进程在用户态下和内核态下所费的时间来计算,它送出SIGPROF信号
*/
if (setitimer(ITIMER_REAL, &tick, NULL) < 0) {
printf("Set timer failed!\n");
}
while (1) {
pause();
}
return 0;
}
相关代码参考:信号应用代码
消息队列基于链表机制管理进程间通讯的消息,存放在内核中并由消息队列标识符标识。消息队列相关接口如下所示。
// 头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// 函数说明
// 创建消息队列,返回消息队列标识符
int msgget(key_t key, int msgflg);
// 发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 操作消息队列(删除、获取消息队列信息等)
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
消息队列的应用如下所示。
#include <sys/msg.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h> //for strerror
#define MESSAGE_QUEUE_ID 2345
struct msgsys
{
long mtype;
int size;
char text[512];
};
void update_msg(struct msgsys *msg, long mtype, char *text, int size)
{
msg->mtype = mtype;
msg->size = size;
memcpy(msg->text, text, size);
}
int main(int argc, char *argv)
{
int mq_id;
struct msgsys msg_val;
int msg_size;
pid_t system_pid;
system_pid = fork();
if (system_pid < 0) {
printf("system fork error\n");
} else if (system_pid == 0) {
//msg queue server
mq_id = msgget((key_t)MESSAGE_QUEUE_ID, 0666 | IPC_CREAT);
if (mq_id < 0) {
printf("msg queue create failed, error:%s", strerror(errno));
return -1;
}
//type: 0返回消息队列中的第一条消息,>0返回队列中mtype等于该值的消息
//<0返回mtype绝对值最小的消息
if ((msg_size = msgrcv(mq_id, (void *)&msg_val, BUFSIZ, 0, 0)) >= 0) {
msg_val.text[msg_size] = '\0';
printf("server queue recv data: %s, size: %d\n", msg_val.text, msg_size);
update_msg(&msg_val, 1, "server recv queue msg", strlen("server recv queue msg"));
msgsnd(mq_id, (void *)&msg_val, msg_size, 0);
}
sleep(1);
//控制删除消息队列
msgctl(mq_id, IPC_RMID, 0);
} else {
//msg queue client
sleep(1);
//申请消息队列
mq_id = msgget((key_t)MESSAGE_QUEUE_ID, 0666);
if (mq_id < 0) {
printf("msg queue request failed");
return -1;
}
update_msg(&msg_val, 1, "client send queue msg", strlen("client send queue msg"));
//消息数据发送
msgsnd(mq_id, (void *)&msg_val, msg_val.size+8, 0);
if ((msg_size = msgrcv(mq_id, (void *)&msg_val, BUFSIZ, 0, 0)) != -1) {
msg_val.text[msg_size] = '\0';
printf("client recv data: %s, size:%d\n", msg_val.text, msg_size);
}
}
printf("msg queue test end\n");
return 0;
}
相关代码参考:消息队列应用代码
信号量是一个计数器,用于为多个进程提供对共享数据对象的访问控制。信号量用于实现进程间的同步和互斥,用于进程间的资源共享。
// 头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 函数说明
// 创建信号量集,返回信号量集标识符
// 权限标志,如IPC_CREAT表示创建新的信号量集。
int semget(key_t key, int nsems, int semflg);
// 执行信号集操作(包括获取信号量集状态、设置信号量集状态、删除信号量集)
int semctl(int semid, int semnum, int cmd, ...);
// 对信号量集进行操作
int semop(int semid, struct sembuf *sops, unsigned nsops);
SystemV信号量集的应用如下所示。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#define KEY 1234
#define SEM_NUM 1
static int index = 0;
// semaphore_p 操作
void semaphore_p(int semid) {
struct sembuf sop = {0, -1, 0};
semop(semid, &sop, 1);
}
// semaphore_v 操作
void semaphore_v(int semid) {
struct sembuf sop = {0, 1, 0};
semop(semid, &sop, 1);
}
void *thread_loop_func0(void *arg) {
int semid = *(int *)arg;
for (int i = 0; i < 10; i++) {
semaphore_p(semid);
index += 1;
printf("thread0: index = %d\n", index);
semaphore_v(semid);
usleep(100);
}
}
void *thread_loop_func1(void *arg) {
int semid = *(int *)arg;
for (int i = 0; i < 10; i++) {
semaphore_p(semid);
index += 1;
printf("thread1: index = %d\n", index);
semaphore_v(semid);
usleep(100);
}
}
int main(int argc, char *argv[])
{
int semid;
pid_t pid;
pthread_t thread0, thread1;
// 创建信号量集
semid = semget(KEY, SEM_NUM, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
return 1;
}
// 初始化信号量值为 1
if (semctl(semid, 0, SETVAL, 1) == -1) {
perror("semctl");
return 1;
}
pthread_create(&thread0, NULL, thread_loop_func0, &semid);
pthread_create(&thread1, NULL, thread_loop_func1, &semid);
pthread_join(thread0, NULL);
pthread_join(thread1, NULL);
// 删除信号量集
printf("index = %d\n", index);
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl");
return 1;
}
return 0;
}
具体代码参考:SystemV semaphore信号量集应用
// 头文件
#include <semaphore.h>
#include <unistd.h>
// 函数说明
// 创建信号量集,返回信号量集标识符
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 等待信号量
int sem_wait(sem_t *sem);
// 释放信号量
int sem_post(sem_t *sem);
// 删除信号量集
int sem_destroy(sem_t *sem);
POSIX信号量应用如下所示。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
static int index = 0;
sem_t semaphore;
void *thread_loop_func0(void *arg)
{
for (int i = 0; i < 10; i++) {
sem_wait(&semaphore);
index += 1;
printf("thread0 index = %d\n", index);
sem_post(&semaphore);
usleep(100);
}
pthread_exit(NULL); //线程退出(非阻塞)
}
void *thread_loop_func1(void *arg)
{
for (int i = 0; i < 10; i++) {
sem_wait(&semaphore);
index += 1;
printf("thread1 index = %d\n", index);
sem_post(&semaphore);
usleep(100);
}
pthread_exit(NULL); //线程退出(非阻塞)
}
int main(int argc, char *argv[])
{
pthread_t thread0, thread1;
// 初始化信号量,初始值为 1
if (sem_init(&semaphore, 0, 1) != 0) {
perror("sem_init");
return 1;
}
pthread_create(&thread0, NULL, thread_loop_func0, NULL);
pthread_create(&thread1, NULL, thread_loop_func1, NULL);
pthread_join(thread0, NULL);
pthread_join(thread1, NULL);
// 删除信号量集
printf("index = %d\n", index);
if (sem_destroy(&semaphore) != 0) {
perror("sem_destroy");
return 1;
}
return 0;
}
具体代码参考:Posix信号量集应用
socket是一种用于进程间通信的机制,用于在网络中进行数据传输。socket可以用于实现进程间的网络通信,也可以用于实现进程间的本地通信。socket相关接口如下所示(可以采用UDP,TCP,VCAN和VXCAN等通讯接口进行通讯)。
关于socket接口已经在相关章节进行详细说明,可到如下章节进行学习掌握。
共享内存是一种用于进程间通信的机制,用于在多个进程之间共享内存区域。共享内存可以用于实现进程间的共享数据,也可以用于实现进程间的共享文件;被共享的内容本质上是共享同一块虚拟内存,在不同进程间具有相同的内容,不需要将数据拷贝到内核中,是目前进程通讯中最快的方法。共享内存的生命周期是随内核的,且不存在同步或者互斥机制,因此需要用户自行维护数据安全。共享内存相关接口如下所示。
// 头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
// 函数说明
// 创建共享内存,返回共享内存标识符
int shmget(key_t key, size_t size, int shmflg);
// 连接共享内存,返回共享内存地址
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 断开共享内存连接
int shmdt(const void *shmaddr);
// 管理共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
共享内存应用如下所示。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#define SHM_SIZE 1024 // 共享内存大小
#define SHM_KEY 0x1234 // 共享内存键值
sem_t semaphore;
void *thread_shm_read(void *arg)
{
int shmid = *(int *)arg;
// 连接共享内存
char *shmaddr = shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1) {
perror("shmat");
return NULL;
}
// 读取共享内存中的数据
for (int i = 0; i < 26; i++) {
sem_wait(&semaphore);
printf("Shared memory data: %s\n", shmaddr);
}
// 断开共享内存连接
if (shmdt(shmaddr) == -1) {
perror("shmdt");
return NULL;
}
pthread_exit(NULL);
}
void *thread_shm_write(void *arg)
{
int shmid = *(int *)arg;
char buffer[1024] = {0};
// 连接共享内存
char *shmaddr = shmat(shmid, NULL, 0);
// 写入共享内存中的数据
for (int i = 0; i < 26; i++) {
sem_post(&semaphore);
shmaddr[i] = 'a' + i;
shmaddr[i+1] = '\0';
sleep(1);
}
// 断开共享内存连接
if (shmdt(shmaddr) == -1) {
perror("shmdt");
return NULL;
}
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
int shmid;
pthread_t tid1, tid2;
shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 初始化信号量,初始值为0
// 共享内存不支持同步或互斥机制,因此使用sempahore信号量进行同步操作
if (sem_init(&semaphore, 0, 0) != 0) {
perror("sem_init");
return 1;
}
// 创建线程
pthread_create(&tid1, NULL, thread_shm_read, &shmid);
pthread_create(&tid2, NULL, thread_shm_write, &shmid);
// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 删除共享内存
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(1);
}
if (sem_destroy(&semaphore) != 0) {
perror("sem_destroy");
return 1;
}
}
具体代码参考:共享内存应用代码
内存映射(Memory Mapping)是一种将文件或设备映射到进程地址空间的技术,可用于进程间通信或直接访问硬件设备。内存映射可以将文件或设备的内容映射到进程的虚拟地址空间中,使得进程可以像访问内存一样访问文件或设备;和共享内存类似,内存映射也不存在同步或者互斥机制,需要用户自行维护。内存映射的相关接口如下所示。
// 头文件
#include <sys/mman.h>
// 函数说明
// 将文件或设备映射到进程地址空间
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 解除文件或设备的映射
int munmap(void *addr, size_t length);
内存映射应用如下所示。
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
#define FILE_SIZE 1024
#define FILE_NAME "mmap_file.txt"
sem_t semaphore;
void *thread_mmap_read(void *arg)
{
int fd = *(int *)arg;
char *map_addr;
// 内存映射文件
map_addr = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_addr == MAP_FAILED) {
perror("mmap");
return NULL;
}
// 从映射内存中读取数据
for (int i = 0; i < 26; i++) {
sem_wait(&semaphore);
printf("Read from shared memory: %s\n", map_addr);
}
// 释放映射内存
munmap(map_addr, FILE_SIZE);
pthread_exit(NULL);
}
void *thread_mmap_write(void *arg)
{
int fd = *(int *)arg;
char *map_addr;
// 内存映射文件
map_addr = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_addr == MAP_FAILED) {
perror("mmap");
return NULL;
}
// 向映射内存中写入数据
for (int i = 0; i < 26; i++) {
map_addr[i] = 'a' + i;
map_addr[i+1] = '\0';
sem_post(&semaphore);
usleep(100); // 等待100ms
}
// 释放映射内存
munmap(map_addr, FILE_SIZE);
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
int fd;
pthread_t tid1, tid2;
// 打开或创建文件
fd = open(FILE_NAME, O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
perror("open");
return 1;
}
// 扩展文件大小
if (ftruncate(fd, FILE_SIZE) == -1) {
perror("ftruncate");
close(fd);
return 1;
}
// 初始化信号量,初始值为0
if (sem_init(&semaphore, 0, 0) != 0) {
perror("sem_init");
return 1;
}
// 创建线程
pthread_create(&tid1, NULL, thread_mmap_read, &fd);
pthread_create(&tid2, NULL, thread_mmap_write, &fd);
// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 关闭文件
close(fd);
if (sem_destroy(&semaphore) != 0) {
perror("sem_destroy");
return 1;
}
}
具体代码参考:内存映射应用代码
至此,关于进程间通讯的应用介绍完毕。可以看到Linux系统中,进程通讯是可以有多种选择来实现进程交互的。数据量比较大,性能有需求,可以使用共享内存的方式;数据量小,属于父子进程,可以使用匿名管道,不属于可以使用消息队列,命名管道;只是通知,可以使用信号量;对于内核的消息,使用signal信号量;需要支持多设备的交互,使用socket通讯。可以看到虽然大部分接口都能够实现进程间交互,在合适的场景使用合适的进程交互方式,才能够提高性能,更好的满足需求。
直接开始下一节说明: 嵌入式Linux硬件交互访问