操作系统C++实践:深入理解多进程与IPC

多进程

多进程是什么?怎么使用?带你入门多进程

写在前面

本篇仅作简单介绍,内容适合入门级别,可通过目录查看。重点在于熟悉每一个的经典使用模式

文中相关api就不做过多介绍,不然字数就太多了,可自行去api手册了解

语言不保证严谨且语言组织比较奇怪,内容也不保证完全,欢迎指正错误、补充

一、多进程基础

一个进程占有的空间是虚拟内存(4G),分为两块,用户空间(0-3G)和内核空间(3-4G)

一、目的

1> 实现多任务并发

2> 实现进程间通信,方便数据处理

二、时间片轮转

单个cpu核心工作时,并不能同时处理多个任务,但每次都一个任务干到底并不现实,当某一个任务卡到输入时,程序阻塞了,什么都干不了——时间片轮转的机制就能解决这一现象,给运行队列中的每一个任务分配一定的时间段,这个时间段非常短,时间结束后就继续执行下一个任务,如此循环,这就是时间片轮转。

三、特殊的进程

  • 0号进程:这是由linux操作系统启动后运行的第一个进程,也叫空闲进程,当没有其他进程运行时,会运行该进程。是1号进程和2号进程的父进程
  • 1号进程:由0号进程创建,完成一些必要的初始化操作,还会收养孤儿进程
  • 2号进程:由0号进程创建,完成任务调度问题,也称调度进程
  • 孤儿进程:当前进程还在运行时,其父进程退出了
  • 僵尸进程:当前进程已经退出,但是其父进程没有为其回收资源

四、linux进程相关指令

1
2
3
4
5
6
7
8
9
10
ps -ef		# 查看进程间关系
ps -ajx # 查看进程状态
ps -aux # 查看进程对CPU和内存占用
top # 动态查看进程相关属性
kill 信号号 进程号 # 发送信号给进程
pidof 进程名 # 查看进程的进程号
jobs -l # 查看停止进程的作业号
bg 作业号 # 实现将停止的进程进入后台运行的状态
fg 作业号 # 实现将后台运行的进程切换到前台运行
./可执行程序 & # 直接将可执行程序后台运行

五、多进程编程的一些基础api

c风格和C风格的多进程差别不大,多进程管理比较底层,本质依赖系统调用,而这些系统调用本身就是C接口,实际大多仍使用C风格。

1
2
3
4
进程创建:fork()
进程退出:exit(), _exit()
进程号获取:getpid(), getppid()
进程资源回收:wait(), waitpid()
  • 经典使用模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>

int main(int argc, const char *argv[])
{
pid_t pid = fork();
// 子进程享有fork之前父进程拥有的所有资源
if(pid > 0)
{
// 父进程
int num = 5;
while(num--)
{
printf("我是父进程\n");
sleep(1);
}
wait(NULL); // 回收子进程资源
}
else if(pid == 0)
{
// 子进程
int num = 2;
while(num--)
{
printf("我是子进程\n");
sleep(1);
}
exit(EXIT_SUCCESS); // 子进程退出
}
else
{
perror("fork error");
return -1;
}

return 0;
}

二、进程间通信IPC

  • 每个进程的用户空间时相互独立的,多个进程用户空间的数据交换需要进程间通信

  • 可以使用外部文件进行多个进程之间的数据传递,简单但不是很可靠,可以用来进行少量数据传递

  • 可以利用内核空间来完成数据交换,因为进程的内核空间是共享的,在内核空间创建出一个特殊的区域,一个进程往里面放数据,另一个进程从里面取数据

1
2
3
4
5
6
7
8
9
1、内核提供的通信方式(传统的通信方式)
无名管道
有名管道
信号
2、system V提供的通信方式
消息队列
共享内存
信号量(信号灯集)
3、套接字通信:socket 网络通信(跨主机通信)
IPC方式 传输速度 使用复杂度 适用场景
管道(pipe) 中等 简单 父子进程间简单通信
命名管道(FIFO) 中等 简单 无关进程间通信
消息队列 中等 中等 消息传递,有优先级
共享内存 最快 复杂 大量数据交换
信号量 中等 进程同步
信号 简单 进程控制,事件通知
套接字 复杂 网络通信,本地通信

一、无名管道

  • 像名字一样,提供一个传输数据的管道,该管道不存放数据,只用于传输数据

  • 存在于内存中,并不存在于文件系统

  • 仅用于亲缘进程之间

1
2
3
4
5
6
#include <unistd.h>

int pipe(int fildes[2]);
功能:创建一个无名管道,并返回该管道的两个文件描述符
参数:是一个整型数组,用于返回打开的管道的两端的文件描述符, fildes[0]表示读端 fildes[1]表示写端
返回值:成功返回0,失败返回-1并置位错误码
  • 涉及到文件描述符,可使用文件IO——write()、read()

  • 读端存在时:写端最多可写满至64K(64436个字节),之后在write处阻塞 读端不存在时:写端写入会导致管道破裂,之后向内核空间发送SIGPIPE信号,导致退出

  • 写端存在时:读端可读至管道空为止,之后在read处阻塞 写端不存在时:读端可读至管道空为止,之后不会在read处阻塞

  • 经典使用模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, const char *argv[])
{
int pipefd[2];
// 创建管道
if(pipe(pipefd) == -1)
{
perror("pipe error");
return -1;
}

pid_t pid = fork();
char wbuf[128] = "hello world";
char rbuf[128] = "";
if(pid > 0)
{
// 父进程:写入数据
close(pipefd[0]); // 关闭读端

// 将数据写入管道中
write(pipefd[1], wbuf, sizeof(wbuf));

close(pipefd[1]);
wait(NULL); // 回收子进程
}
else if(pid == 0)
{
// 子进程:读取数据
close(pipefd[1]); // 关闭写端

// 读取管道中的数据
read(pipefd[0], rbuf, sizeof(rbuf));
printf("收到父进程的数据为:%s\n", rbuf);

close(pipefd[0]);
exit(EXIT_SUCCESS);
}
else
{
perror("fork error");
return -1;
}
return 0;
}

二、有名管道

  • 也是提供一个用于传输数据的管道

  • 相比于无名管道,有名管道是存在于文件系统中的管道文件,因为“有名”嘛

  • 可用于亲缘进程之间,也可用于非亲缘进程之间

1
2
3
4
5
6
7
8
9
10
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
功能:创建一个管道文件,并存在与文件系统中
参数1:管道文件的名称
参数2:管道文件的权限,内容详见文件IO中open函数的mode参数
返回值:成功返回0,失败返回-1并置位错误码

注意:管道文件被创建后,其他进程就可以进行打开读写操作了,但是,必须要保证当前管道文件的两端都打开后,才能进行读写操作,否则函数会在open处阻塞
  • 经典使用模式

create.cpp(用于创建有名管道)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <sys./types.h>
#include <sys/stat.h>

int main(int argc, const char *argv[])
{
if(mkfifo("./myfifo"0664) == -1)
{
perror("mkfifo error");
return -1;
}
printf("管道创建成功\n");
}

send.cpp(用于向管道中写入数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <sys./types.h>
#include <sys/stat.h>
#include <string.h>

int main(int argc, const char *argv[])
{
int wfd = -1;
if((wfd = open("./myfifo", O_WRONLY)) == -1)
{
perror("open error");
return -1;
}
char wbuf[128] = "";

while(1)
{
fgets(wbuf, sizeof(wbuf), stdin); // 从终端读取数据
wbuf[strlen(wbuf) - 1] = '\0';

// 将数据写入管道
write(wfd, wbuf, strlen(wbuf));
// 结束标志
if(strcmp(wbuf, "quit") == 0)
{
break;
}
}
close(wfd); // 关闭文件
return 0;
}

recv.cpp(用于从管道中读取数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <sys./types.h>
#include <sys/stat.h>
#include <string.h>

int main(int argc, const char *argv[])
{
int rfd = -1;
if((rfd = open("./myfifo", O_RDONLY)) == -1)
{
perror("open error");
return -1;
}
char rbuf[128] = "";

while(1)
{
// 从管道中读取数据
read(rfd, rbuf, sizeof(rbuf));
// 结束标志
if(strcmp(rbuf, "quit") == 0)
{
break;
}
printf("收到数据:%s\n", rbuf);

}
close(rfd); // 关闭文件
// 删除有名管道文件
unlink(myfifo);
return 0;
}

三、信号

信号可用字面义来理解,就是起到通知进程的作用。A进程发出通知给B进程,B进程收到通知就会执行相关处理,就像人一样

  • 信号绑定

两个特殊信号:SIGKILL和SIGSTOP,这两个信号既不能被捕获,也不能被忽略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
功能:将信号与信号处理方式绑定到一起
参数1:要处理的信号
参数2:处理方式
SIG_IGN:忽略
SIG_DFL:默认,一般信号的默认操作都是杀死进程
typedef void (*sighandler_t)(int):用户自定义的函数
返回值:成功返回处理方式的起始地址,失败返回SIG_ERR并置位错误码

注意:只要程序与信号绑定一次,后续但凡程序收到该信号,对应的处理方式就会立即响应

使用信号对僵尸进程回收

原理:当子进程退出后,会向父进程发送一个SIGCHLD的信号,只需要捕获该信号即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <myhead.h>

void handler(int signo)
{
if(signo == SIGCHLD)
{
while(waitpid(-1NULL, WNOHANG) > 0);
}
}

int main(int argc, const char *argv[])
{
// 将SIGCHLD信号和handler处理方式绑定
if(signal(SIGCHLD, handler) == SIG_ERR)
{
perror("signal error");
return -1;
}
for(int i = 0; i < 10; i++)
{
// 创建子进程并将子进程退出,使其成为僵尸进程
if(fork() == 0)
{
exit(EXIT_SUCCESS);// 发出SIGCHLD信号
}
}

while(1);
return 0;
}
  • 信号发送:kill、raise

向进程发送信号,进程收到信号后就执行相关处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <signal.h>

int kill(pid_t pid, int sig);
功能:向指定进程或进程组发送信号
参数1:进程号或进程组号
>0:表示向执行进程发送信号
=0:向当前进程所在的进程组中的所有进程发送信号
=-1:向所有进程发送信号
<-1:向指定进程组发送信号,进程组的ID号为给定pid的绝对值
参数2:要发送的信号
返回值:成功返回0,失败返回-1并置位错误码

#include <signal.h>

int raise(int sig);
功能:向自己发送信号 // 等价于:kill(getpid(), sig);
参数:要发送的信号
返回值:成功返回0,失败返回非0数组

四、system V提供的进程间通信

对于上面提到的管道通信,只能实现单向通信,而信号通信也只能起到通知的效果,

消息队列、共享内存、信号量(信号灯集)

1
2
3
4
5
6
相关指令:
ipcs 可以查看所有的信息(消息队列、共享内存、信号量)
ipcs -q:可以查看消息队列的信息
ipcs -m:可以查看共享内存的信息
ipcs -s:可以查看信号量的信息
ipcrm -q/m/s ID :可以删除指定ID的IPC对象

即使程序已经结束,但容器依然存在,除非手动删除

五、消息队列

原理:内核空间一个消息队列容器,每个元素包含消息类型消息正文,根据类型存取消息即可

  • 特征

消息取出来后就会从消息队列中移除

消息队列大小:16K

多个进程,使用相同的key值打开的是同一个消息队列

  • 相关api

    参数较为复杂,可自行前往api手册查看,这里仅作简单介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
1、创建key值
key_t ftok(const char *pathname, int proj_id); //ftok("/", 'k');

2、通过key值,创建消息队列
int msgget(key_t key, int msgflg);

3、向消息队列中存放数据
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

4、从消息队列中取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

5、销毁消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 经典使用模式

send.cpp(发送端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// 消息正文大小
#define MSGSIZE (sizeof(struct msgBuf) - sizeof(long))

// 消息结构体定义
struct msgBuf
{
long mtype; // 消息的类型
char mtext[1024]; // 消息正文
};

int main(int argc, const char *argv[])
{
// 创建key值,用于创建消息队列
key_t key = ftok("/"'k');
if(key == -1)
{
perror("ftok error");
return -1;
}
printf("key = %#x\n", key);

// 通过key值创建消息队列
int msqid = -1;
if((msqid = msgget(key, IPC_CREAT | 0664)) == -1)
{
perror("msgget error");
return -1;
}
printf("msqid = %d\n", msqid);

// 向消息队列中发送信息
struct msgBuf buf;
while(1)
{
printf("输入消息的类型:");
scanf("%ld", &buf.mtype);
getchar(); // 吞回车
printf("输入消息正文:");
fgets(buf.mtext, MSGSIZE, stdin); // 从终端读取数据
buf.mtext[strlen(buf.mtext) - 1] = '\0';

// 将上面的信息以阻塞的方式放入消息队列
msgsnd(msqid, &buf, MSGSIZE, 0);
printf("消息存放成功\n");

if(strcmp(buf.mtext, "quit") == 0)
{
break;
}
}
// 在接收端删除消息队列,就像最后走的人关门一样
return 0;
}

recv.cpp(接收端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// 消息正文大小
#define MSGSIZE (sizeof(struct msgBuf) - sizeof(long))

// 消息结构体定义
struct msgBuf
{
long mtype; // 消息的类型
char mtext[1024]; // 消息正文
};

int main(int argc, const char *argv[])
{
// 创建key值,用于创建消息队列
key_t key = ftok("/"'k');
if(key == -1)
{
perror("ftok error");
return -1;
}
printf("key = %#x\n", key);

// 通过key值创建消息队列
int msqid = -1;
if((msqid = msgget(key, IPC_CREAT | 0664)) == -1)
{
perror("msgget error");
return -1;
}
printf("msqid = %d\n", msqid);

// 向消息队列中发送信息
struct msgBuf buf;
while(1)
{
printf("输入要接收的消息类型:");
scanf("%ld", &buf.mtype);

// 将以阻塞的方式读取消息队列中的数据
if(msgrcv(msqid, &buf, MSGSIZE, buf.mtype, 0) == -1)
{
perror("msgrcv error");
break;
}
printf("消息读取成功:%s\n", buf.mtext);

if(strcmp(buf.mtext, "quit") == 0)
{
break;
}
}
// 删除消息队列
if(msgctl(msqid, IPC_RMID, NULL) == -1)
{
perror("msgctl IPC_RMID error");
return -1;
}
return 0;
}

六、共享内存

  • 像名称一样,多个进程共享一段内存,随时存取。

  • 共享内存保证时效性

  • 共享内存的读取不是一次性的,读取后,数据依然在共享内存中

  • 共享内存原理是指针,由于无需进行用户空间与内核空间的切换,所以共享内存是所有进程间通信方式中效率最高的一种通信方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

1、创建key值
key_t ftok(const char *pathname, int proj_id); //ftok("/", 'k');

2、通过key值创建共享内存段
int shmget(key_t key, size_t size, int shmflg);

3、将共享内存段的地址映射到用户空间
void *shmat(int shmid, const void *shmaddr, int shmflg);

4、释放共享内存的映射关系
int shmdt(const void *shmaddr);

5、共享内存的控制函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 经典使用模型

send.cpp(发送端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PAGE_SIZE 4096

int main(int argc, const char *argv[])
{
// 1、创建key值
key_t key = ftok("/"'k');
if(key == -1)
{
perror("ftok error");
return -1;
}

// 2、根据key值创建共享内存段(申请物理内存,映射到内核空间)
int shmid = -1;
if((shmid = shmget(key, PAGE_SIZE, IPC_CREAT | 0644)) == -1)
{
perror("shmget error");
return -1;
}
printf("shmid = %d\n", shmid);

// 3、将共享内存映射到用户空间
char *addr = (char *)shmat(shmid, NULL0);
if(addr == (void *)-1)
{
perror("shmat error");
return -1;
}
printf("addr = %p\n", addr);

// 4、对共享内存操作
while(1)
{
printf("请输入>>>");
fgets(addr, PAGE_SIZE, stdin);
addr[strlen(addr) - 1] = '\0';

if(strcmp(addr, "quit") == 0)
{
break;
}
}
// 5、释放共享内存与进程的映射关系
if(shmdt(addr) == -1)
{
perror("shmdt error");
return -1;
}

// 在接收端删除共享内存段
return 0;
}

recv.cpp(接收端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PAGE_SIZE 4096

int main(int argc, const char *argv[])
{
// 1、创建key值
key_t key = ftok("/"'k');
if(key == -1)
{
perror("ftok error");
return -1;
}

// 2、根据key值创建共享内存段(申请物理内存,映射到内核空间)
int shmid = -1;
if((shmid = shmget(key, PAGE_SIZE, IPC_CREAT | 0644)) == -1)
{
perror("shmget error");
return -1;
}

// 3、将共享内存段映射到用户空间
char *addr = (char *)shmat(shmid, NULL0);
if(addr == (void*)-1)
{
perror("shmat error");
return -1;
}
printf("addr = %p\n", addr);

// 4、对共享内存操作
while(1)
{
sleep(2);
printf("读取到消息为:%s\n", addr);

if(strcmp(addr, "quit") == 0)
{
break;
}
}
// 5、释放共享内存与进程的映射关系
if(shmdt(addr) == -1)
{
perror("shmdt error");
return -1;
}

// 在接收端删除共享内存段
if(shmctl(shmid, IPC_RMID, NULL) == -1)
{
perror("shmctl IPC_RMID error");
return -1;
}
printf("成功删除共享内存段\n");
return 0;
}

共享内存的竞态问题

  • 上面的示例可以看出,共享内存就是对一块可以共享的内存空间进行操作。如果只有两个进程还好,要是有多个进程同时使用共享内存,就会发生竞态问题,什么意思呢?
  • 竞态1:假设现在有三个进程ABC,A进程与B进程说悄悄话,但在这根本没有“悄悄”可言,因为C进程是可以随时听到悄悄话的,而A进程和B进程不想让C进程知道,C进程也不想知道。
  • 竞态2:A进程和B进程正在通信,但是C进程突然向共享内存中存放数据,那数据就变了呀,A进程和B进程之间的通信就被扰乱了
  • 以上两种情况供大家理解,说得不太准确,但相信大家也理解到意思了
  • 知道问题了,那如何解决呢?上面我加粗了两个词语,“随时听到悄悄话”和“突然向共享内存中存放数据”就是C进程在执行程序,那就很好解决了,保证顺序性即可,那就需要接下来的信号灯集辅助了

七、信号灯集

信号灯集是一个比较形象的名称,信号灯集可以给每个进程绑定信号量,执行进程需要消耗信号量资源,执行完后可以释放别的信号量,这样别的进程就有信号量来消耗了,可自定义顺序性。那么,如果信号量>0代表发光,那就是一个“信号灯”了。

简单来说,每一个进程都是:申请信号量资源–>执行程序–>释放别的信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>

1、创建key值
key_t ftok(const char *pathname, int proj_id); //ftok("/", 'k');

2、通过key值创建信号量集
int semget(key_t key, int nsems, int semflg);

3、关于信号量集的操作:P(申请资源)V(释放资源)
int semop(int semid, struct sembuf *sops, size_t nsops);

4、关于信号量集的控制函数
int semctl(int semid, int semnum, int cmd, ...);
  • 经典使用模型

信号灯集二次封装函数

sem.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _SEM_H_
#define _SEM_H_

// 创建信号灯集并初始化有semcount个信号灯
int create_sem(int semcount);

// 申请资源操作,semno表示要被申请资源的信号灯编号
int P(int semid, int semno);

// 释放资源操作,semno表示要被释放资源的信号灯编号
int V(int semid, int semno);

// 删除信号灯集
int delete_sem(int semid);

#endif

sem.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>

union semun
{
int val; // 信号量的值(当前我们只需要管这个)
struct semid_ds *buf; // 信号量集属性相关
unsigned short *array; // 对该信号量集所有信号量的操作
struct seminfo *__buf; // 缓冲(Linux特有的)
}

int init_sem(int semid, int semno)
{
int val = -1;
printf("请输入第%d个信号量的初始值:", semno);
scanf("%d", &val);
getchar(); // 吞回车

// 调用semctl函数
union semun us;
us.val = val;

if(semctl(semid, semno, SETVAL, us) == -1)
{
perror("semctl SETVAL error");
return -1;
}
return 0;
}

// 创建信号灯集并初始化有semcount个信号灯
int create_sem(int semcount)
{
// 创建key值
key_t key = ftok("/"'k');
if(key == -1)
{
perror("ftok error");
return -1;
}

// 通过key值创建信号量集
int semid = -1;
// IPC_EXCL参数:如果已经存在了就置位错误码为:EEXIST
if((semid = semget(key, semcount, IPC_CREAT | IPC_EXCL | 0664)) == -1)
{
if(errno == EEXIST) // 表示已经该信号量集存在了
{
semid = semget(key, semcount, IPC_CREAT | 0664);
return semid;
}
perror("semget error");
return -1;
}

// 循环初始化信号量集中的信号量
for(int i = 0; i < semcount; i++)
{
init_sem(semid, i);
}

return semid;
}

// 申请资源操作,semno表示要被申请资源的信号灯编号
int P(int semid, int semno)
{
// 信号量结构体
struct sembuf buf;
buf.sem_num = semno; // 操作的信号编号
buf.sem_op = -1; // -1表示申请资源
buf.sem_flg = 0; // 0表示阻塞形式

// 调用semop函数
// 第三个参数表示申请多少
if(semop(semid, &buf, 1) == -1)
{
perror("semop P error");
return -1;
}
return 0;
}

// 释放资源操作,semno表示要被释放资源的信号灯编号
int V(int semid, int semno)
{
// 信号量结构体
struct sembuf buf;
buf.sem_num = semno; // 操作的信号编号
buf.sem_op = 1; // 1表示释放资源
buf.sem_flg = 0; // 0表示阻塞形式

// 调用semop函数
// 第三个参数表示释放多少
if(semop(semid, &buf, 1) == -1)
{
perror("semop V error");
return -1;
}
return 0;
}

// 删除信号灯集
int delete_sem(int semid)
{
// 调用semctl函数
// 第三个参数为IPC_RMID时,第二个参数可忽略(但是要填),第四个参数可不填
if(semctl(semid, 0, IPC_RMID) == -1)
{
perror("semctl IPC_RMID error");
return -1;
}

return 0;
}

看代码长度就知道为什么要二次封装了,要是重复写这些代码,得累死

接下来就融合共享内存和信号量集

shmsnd.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include "sem.h"

#define PAGE_SIZE 4096

int main(int argc, const char *argv[])
{
// ------------创建并打卡信号量集
int semid = create_sem(2);

// 1、创建key值
key_t key = ftok("/"'k');
if(key == -1)
{
perror("ktok error");
return -1;
}

// 2、根据key值创建共享内存段(申请物理内存,映射到内核空间)
int shmid = -1;
if((shmid = shmget(key, PAGE_SIZE, IPC_CREAT | 0644)) == -1)
{
perror("shmget error");
return -1;
}
printf("shmid = %d\n", shmid);

// 3、将共享内存映射到用户空间
char *addr = (char *)shmat(shmid, NULL0);
if(addr == (void *)-1)
{
perror("shmat error");
return -1;
}
printf("addr = %p\n", addr);

// 4、对共享内存操作
while(1)
{
// ------------申请0号信号量的资源
P(semid, 0);

printf("请输入>>>");
fgets(addr, PAGE_SIZE, stdin);
addr[strlen(addr) - 1] = '\0';

// ------------释放1号信号量的资源
V(semid, 1);

if(strcmp(addr, "quit") == 0)
{
break;
}
}
// 5、释放共享内存与进程的映射关系
if(shmdt(addr) == -1)
{
perror("shmdt error");
return -1;
}

// 在接收端删除共享内存段

// ------------在接收端删除信号量级

return 0;
}

shmrcv.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PAGE_SIZE 4096

int main(int argc, const char *argv[])
{
// ------------打开已有的信号量集
int semid = create_sem(2);

// 1、创建key值
key_t key = ftok("/"'k');
if(key == -1)
{
perror("ftok error");
return -1;
}

// 2、根据key值创建共享内存段(申请物理内存,映射到内核空间)
int shmid = -1;
if((shmid = shmget(key, PAGE_SIZE, IPC_CREAT | 0644)) == -1)
{
perror("shmget error");
return -1;
}

// 3、将共享内存段映射到用户空间
char *addr = (char *)shmat(shmid, NULL0);
if(addr == (void*)-1)
{
perror("shmat error");
return -1;
}
printf("addr = %p\n", addr);

// 4、对共享内存操作
while(1)
{
// ------------申请1号信号量的资源
P(semid, 1);

printf("读取到消息为:%s\n", addr);

if(strcmp(addr, "quit") == 0)
{
break;
}

// ------------释放0号信号量的资源
V(semid, 0);
}
// 5、释放共享内存与进程的映射关系
if(shmdt(addr) == -1)
{
perror("shmdt error");
return -1;
}

// 在接收端删除共享内存段
if(shmctl(shmid, IPC_RMID, NULL) == -1)
{
perror("shmctl IPC_RMID error");
return -1;
}
printf("成功删除共享内存段\n");

// ------------删除信号量集
delete_sem(semid);
return 0;
}

注意:

  • 信号量集完成多个进程间的同步问题,一般不用于通信,用于辅助
  • 信号量集本质时维护了多个val值,根据val值来判断是否阻塞,控制进程的执行
  • 信号量为0,则不会再减少了,该进程会在此阻塞

三、总结

通信方式的选择

在使用不同的IPC(进程间通信)方式时,需要考虑它们的性能差异。

  • 管道(Pipe): 传输速度一般,但操作简单,适合小规模数据传输。
  • 消息队列(Message Queue): 传输速度中等,可靠性好,适合异步通信场景。
  • 共享内存(Shared Memory): 传输速度最快,但需要处理同步问题,适合大规模数据传输。
  • 信号量(Semaphore): 主要用于进程间同步,传输速度较慢。

一般来说,对于小规模数据传输,管道是最简单高效的选择 ; 对于大数据量或实时性要求高的场景,共享内存是最佳方案 ; 而消息队列则适用于异步通信的场景。根据特点选择适合的IPC方式

延申:实现并发还可以使用多线程,跨主机进程通信可以使用网络编程中的socket套接字

到这里,相信你已经大致了解了多进程是什么,以及简单的应用,希望大家喜欢 : )