Linux系统应用编程(三)进程间通信
本篇主要内容:
一、什么是IPC机制?/ 什么是进程间通信?
操作系统中的进程并发执行时,可以是独立的也可以是协作的。独立的就是说该进程运行是不与其他进程共享数据,协作的就是说该进程能与其他进程共享数据,能影响其他进程或者被其他进程所影响。这种协作进程之间进行数据交互的过程就称作进程间通信机制,也就是IPC。进程间通信的方式在Linux中主要有:①管道(匿名管道和命名管道)、②消息队列、③共享内存、④信号、⑤Socket等
(PS:《Unix环境高级编程(第2版)》中,还介绍了Streams这一种方式,Steams和Socket可用于不同主机间的进程通信)
二、进程间通信的方式(单机通信)
1.管道
(1)匿名管道

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <wait.h>
#include <string.h>
int main(){
int pipefd[2];
char *writeBuffer = "parents write to child";
char *readBuffer = (char *)malloc(128);
int retn = pipe(pipefd);
if(retn == -1){
printf("error:%d\n",errno);
perror("pipe");
}
printf("create pipe succeed\n");
pid_t pid = fork();
if(pid == -1){
perror("fork");
exit(-1);
}
if(pid > 0){ //parents process:write
close(pipefd[0]); //close read fd
write(pipefd[1],writeBuffer,strlen(writeBuffer));
wait(NULL);
}else{ //child process:read
close(pipefd[1]); //close write fd
read(pipefd[0],readBuffer, strlen(writeBuffer));
printf("child read: %s\n",readBuffer);
}
return 0;
}
(2)命名管道

2.消息队列
(1)消息队列的理解
消息队列是一种进程间通信机制,它允许不同进程之间通过共享消息实现通信和同步。在Linux中,消息队列由内核管理维护,每个消息队列都有唯一的标识符(也就是队列ID)。
<通过(2)消息队列的使用,可以进一步知道>:调用msgsnd()发送消息时,该消息是被添加到了消息队列的队尾,在调用msgrcv时,如果不指定接收的消息的类型,则遵循队列的"先进先出"的规则,也就是此时接收的消息是队列的第一条消息;如果指定接收的消息类型,则可以选择接收对应的消息,无论该消息是在队尾还是队首,从这里也可以看出,消息队列的结构实际上在内核中是一个链式结构。每一个节点是一条消息,节点与节点之间按照队列的顺序排列。
(2)消息队列的使用


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <wait.h>
typedef struct Msg{
long msgType; //消息类型(一个正整数来表示)
char msgText[128]; //消息内容
}Msg;
int main(){
key_t key = ftok(".",'a'); //ftok()获取消息队列的键值key
int msgid = msgget(key,IPC_CREAT|0600); // 打开/创建消息队列
/* 准备发送的两条类型不同的消息:一条以当前pid号表示消息类型,一条以2022标识 */
Msg msgSendType1 = {getpid(),"Child send message(1) to Parents"};
Msg msgSendType2 = {2022,"Child send message(2) to Parents"};
Msg *msgRcv = (Msg *)malloc(sizeof(Msg));
if(msgid == -1){
perror("msgget");
exit(-1);
}
/* 创建进程:父进程接收消息;子进程发送消息 */
int pid = fork();
if(pid == -1){
perror("fork");
exit(-1);
}
if(pid == 0){ /* 子进程发送消息,0表示发送完成再返回 */
int nsend1 = msgsnd(msgid,&msgSendType1,strlen(msgSendType1.msgText),0);
int nsend2 = msgsnd(msgid,&msgSendType2,strlen(msgSendType2.msgText),0);
if(nsend1 == -1 || nsend2 == -1)perror("msgsnd");
printf("child send done\n");
}else{ /* 父进程接收消息,只接受消息类型为2022的那一条 */
msgrcv(msgid,msgRcv,sizeof(msgRcv->msgText) ,2022,0);
printf("Parents process received: message type=%ld\n%s\n",msgRcv->msgType,msgRcv->msgText);
wait(NULL);
}
//msgctl(msgid,IPC_RMID,NULL);
return 0;
}

3.共享内存
共享内存是进程间通信的一种高效率的方式,通过向内核申请一块内存空间,进程将其映射到自己的虚拟内存空间,多个进程可以通过读写操作自己的这片虚拟内存,对共享内存内的共享数据进行读写,实现进程间的数据共享。在这个过程中,由于允许多个进程操作共享数据,可能会造成类似Java的线程安全问题,所以需要同步机制来保证共享数据的安全。
<同步机制可用信号量,见文末>
#include "ipcAll.h"
#define SHM_SIZE 4096 //共享内存的大小
int main(){
/* 获取key */
key_t key = ftok(".",'a');
/* 获取共享内存,权限0666 */
int shmid = shmget(key,SHM_SIZE,IPC_CREAT|0666);
if(shmid == -1){
perror("shmget");
exit(-1);
}
printf("share memory get succeed\n");
/* 共享内存映射到shmAddr */
char *shmAddr = NULL;
shmAddr = (char *)shmat(shmid,NULL,0);
if(shmAddr == NULL){
perror("shmat");
exit(-2);
}else{
printf("share memory map succeed\n");
}
/* 通过shmAddr读写共享内存 */
printf("write shared memory:");
fgets(shmAddr,128,stdin);
printf("read shared memory:%s\n",shmAddr);
/* 取消映射 */
shmdt(shmAddr);
/* 移除共享内存 */
shmctl(shmid,IPC_RMID,NULL);
system("ipcs -m"); //查看共享内存
return 0;
}
4.信号
(1)信号的理解
信号实际上是Linux系统的软中断,为Linux提供了一种处理异步通信事件的方法,信号可以由内核、进程发送,其原理是利用进程的中断机制,当信号到达时,中断当前进程的执行,转而执行信号处理函数。每一个信号都有一个id唯一标识。进程在收到信号时有三种处理方式:①忽略、②捕捉响应、③默认动作
(2)信号的使用

▲发送信号和信号处理kill( )和signal( )的使用 ——(略)
▲发送信号并携带消息sigqueue( )和sigaction( )的使用

- siginfo_t结构体和sigval联合体:

#include "ipcAll.h"
void sig10Handler(int signum, siginfo_t * siginfo, void *infoRetn){
int shmid = shmget(ftok(".",'a'),2048,IPC_CREAT|0666);
siginfo->si_value.sival_ptr = shmat(shmid,NULL,0);
if(infoRetn != NULL) {
printf("Received signal %d from %d\n", siginfo->si_signo, siginfo->si_pid);
printf("Received message: <%s> form %d\n", (char *) (siginfo->si_value.sival_ptr),siginfo->si_pid);
}
}
int main(){
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = sig10Handler;
sigaction(SIGUSR1,&act,NULL);
printf("%d:waiting signal...\n",getpid());
while (1){
sleep(1);
}
return 0;
}
#include "ipcAll.h"
int main(int argc,char **argv){
if(argc < 3){
printf("Usage: %s [signum] [pid]\n",argv[0]);
exit(-1);
}
int signum = atoi(argv[1]);
pid_t pid = atoi(argv[2]);
/* 创建共享内存 */
int shmid = shmget(ftok(".",'a'),2048,IPC_CREAT|0666);
union sigval val = {
.sival_ptr = shmat(shmid,NULL,0) //将val.sival_ptr执行共享内存
};
/* 往val.sival_ptr写数据,再通过sigqueue发送信号并携带数据,发送到另一进程*/
while (1) {
printf("please input the message (\"exit\" to quit):");
scanf("%s",(char *)val.sival_ptr);
if (sigqueue(pid, signum, val) == -1) { //发送
printf("Send signal %s error\n", argv[1]);
}
if (strstr(val.sival_ptr, "exit") != NULL) { //exit退出,取消共享内存映射,删除
shmdt(val.sival_ptr);
shmctl(shmid, IPC_RMID, NULL);
break;
}
}
return 0;
}

5.信号量
(1)信号量的理解
Linux中的信号量(Semaphore)是一种用于协调多个进程对共享资源访问的同步机制。它本质上是一个计数器,用于限制对共享资源的访问数量,并保证在同一时间内只能有一个进程访问该资源。信号量的基本操作包括三个原语:创建信号量(semget)、销毁信号量(semctl)和信号量PV操作。其中,P操作用于获取(申请)信号量,V操作用于释放(归还)信号量。
在<3.共享内存>提到了多个进程操作共享资源时可能会出现类似Java的线程安全问题,也就是多个进程同时访问和修改共享资源时,没有进行同步而发生了竞争条件,导致数据不一致等问题。例如以下代码:
- 经典同步问题:多窗口售票(C语言版,Java线程版可见文章: 第九篇 Java多线程)
(PS:这里fork()出两个进程充当售票窗口,同时售卖100张票,这100张票在共享内存中,两个进程共享此资源,代码加入一丢丢面向对象思想)
#include "ipcAll.h"
/* 结构体:售票窗口"类" */
typedef struct {
int *ticketNum; //窗口余票数
char *windowsName; //窗口名字
void (*sell) (int *,char *); //函数指针:窗口售票"方法"
}Window;
/* 售票方法 */
void ticketSell(int *tickNum,char *windowName){
static int cnt = 1;
if(*tickNum > 0){
//usleep(50*1000); //50ms 加入睡眠,为了看到出现错票的概率
(*tickNum)--;
printf("(%d)windoes: %s sell ticket,now ticket sum: %d\n",cnt++,windowName,*tickNum);
}
}
int main(){
/* 共享资源:100张票,共享内存中 */
int shmid = shmget(ftok(".",'a'),2048,IPC_CREAT|0666);
if(shmid == -1){
perror("shmget");
}
int *ticketSum = (int *)shmat(shmid,NULL,0);
if(ticketSum != NULL)
*ticketSum = 100;
else perror("shmat");
/* 001售票窗口 */
Window window1 = {
.ticketNum = ticketSum,
.windowsName = "001",
.sell = ticketSell
};
/* 002售票窗口 */
Window window2 = {
.ticketNum = ticketSum,
.windowsName = "002",
.sell = ticketSell
};
pid_t pid = fork(); //父子进程模拟售票窗口
if(pid == 0){
while (*(window1.ticketNum) > 0) {
window1.sell(window1.ticketNum, window1.windowsName);
}
}else{
while (*(window2.ticketNum) > 0) {
window2.sell(window2.ticketNum, window2.windowsName);
}
}
return 0;
}


为了解决这种问题,必须采用同步机制,保证在同一时间内只能有一个进程访问该资源,直到该进程操作完成才能有其他进程进行操作
(2)信号量的使用


- 经典同步问题:多窗口售票BUG解决
#include "ipcAll.h"
#include <pthread.h>
/* 结构体:售票窗口"类" */
typedef struct {
int *ticketNum; //窗口余票数
char *windowsName; //窗口名字
void (*sell)(int *, char *); //函数指针:窗口售票"方法"
} Window;
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) */
};
/* P操作:sem_op<0 获得信号量0 */
void operation_p(int semid){
struct sembuf set = {
.sem_flg = SEM_UNDO,
.sem_num = 0,
.sem_op = -1
};
semop(semid,&set,1);
}
/* V操作:sem_op>0 释放信号量0 */
void operation_v(int semid){
struct sembuf set = {
.sem_flg = SEM_UNDO,
.sem_num = 0,
.sem_op = 1
};
semop(semid,&set,1);
}
int semid;
/* 售票方法(同步方法) */
void ticketSell(int *tickNum, char *windowName) {
operation_p(semid);
static int cnt = 1;
if (*tickNum > 0) {
usleep(20 * 1000); //200ms 加入睡眠,为了看到出现错票的概率
(*tickNum)--;
printf("(%d)windoes: %s sell ticket,now ticket sum: %d\n", cnt++, windowName, *tickNum);
}
operation_v(semid);
}
int main() {
/* 共享资源:100张票,共享内存中 */
int shmid = shmget(ftok(".", 'a'), 2048, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
}
int *ticketSum = (int *) shmat(shmid, NULL, 0);
if (ticketSum != NULL)
*ticketSum = 100;
else perror("shmat");
/* 001售票窗口 */
Window window1 = {
.ticketNum = ticketSum,
.windowsName = "001",
.sell = ticketSell
};
/* 002售票窗口 */
Window window2 = {
.ticketNum = ticketSum,
.windowsName = "002",
.sell = ticketSell
};
/* 获得信号量(1个) */
semid = semget(ftok("..", 'a'), 1, IPC_CREAT | 0666);
union semun initSem = {.val = 1};
semctl(semid, 0,SETVAL,initSem);
pid_t pid = fork(); //父子进程模拟售票窗口
if (pid == 0) {
while (*(window1.ticketNum) > 0) {
window1.sell(window1.ticketNum, window1.windowsName);
}
} else {
while (*(window2.ticketNum) > 0) {
window2.sell(window2.ticketNum, window2.windowsName);
}
}
shmctl(shmid,IPC_RMID,NULL);
semctl(semid,0,IPC_RMID);
return 0;
}
三、第五篇 进程间通信的方式(多机通信)- 网络socket

1872

被折叠的 条评论
为什么被折叠?



