目录
进程间通信介绍
进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信发展
1:管道
2:System V进程间通信
3:POSIX进程间通信
进程间通信分类
管道
1:匿名管道pipe
2:命名管道
System V IPC
1:System V 消息队列
2:System V 共享内存
3:System V 信号量
POSIX IPC
1:消息队列
2:共享内存
3:信号量
4:互斥量条件变量
5:读写锁
ipc命令:
-
ipcs
命令用于显示进程间通信设施的当前状态,包括消息队列、共享内存段和信号量。使用不同的选项可以查看不同类型的IPC资源:-m
:显示共享内存段的信息。-q
:显示消息队列的信息。-s
:显示信号量的信息。
-
ipcrm
命令用于删除或清除IPC资源。它通常与资源的ID一起使用,而不是用来查看信息:-m <id>
:删除共享内存段,其中<id>
是共享内存的ID。-q <id>
:删除消息队列,其中<id>
是消息队列的ID。-s <id>
:删除信号量,其中<id>
是信号量的ID
管道
什么是管道
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道。
匿名管道
#include<unistd.h>
int pipe(int fd[2]);
这里的 fd 是一个整型数组,至少包含两个整数,用于存储管道的文件描述符。
参数:
fd[2]:一个整型数组,其中 fd[0] 将被设置为管道的读取端文件描述符,fd[1] 将被设置为管道的写入端文件描述符。
返回值:
成功时,pipe 返回 0。
失败时,返回 -1,并设置 errno 以指示错误。
错误代码:
EMFILE:进程已达到文件描述符的最大限制。
ENFILE:系统已达到文件结构的最大限制。
EBADF:fd 数组中的某个文件描述符无效。
来段代码测试一下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int fds[2];
char buf[100];
int len;
if (pipe(fds) == -1)
perror("make pipe"), exit(1);
// read from stdin
while (fgets(buf, 100, stdin))
{
len = strlen(buf);
// write into pipe
if (write(fds[1], buf, len) != len)
{
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
// read from pipe
if ((len = read(fds[0], buf, 100)) == -1)
{
perror("read from pipe");
break;
}
// write to stdout
if (write(1, buf, len) != len)
{
perror("write to stdout");
break;
}
}
}
用fork来共享管道原理
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string>
// fork之后子进程是能拿到父进程的数据的,但是不能实时通信。
// 因为发生写时拷贝,(彼此的缓冲区里的改变,对方都看不到)对方都看不到
// char buffer[1024]; // 不行的
const int size = 1024; // 管道缓冲区大小
static int cnt = 0;
std::string getOtherMessage()
{
std::string messageid = std::to_string(cnt); // stoi -> string -> int
cnt++;
pid_t self_id = getpid();
std::string stringpid = std::to_string(self_id);
std::string message = "messageid: ";
message += messageid;
message += " my pid is : ";
message += stringpid;
return message;
}
void SubProcessWrite(int wfd)
{
int pipesize = 0;
std::string message = "father,I am your son process";
//子进程写10次
int count = 10;
while (count--)
{
std::string info = message + getOtherMessage(); // 这条消息,就是我们子进程发给父进程的消息
std::cerr << "Child: senting message! PID: " << getpid() << std::endl;
write(wfd, info.c_str(), info.size());
std::cout << std::endl;
sleep(1);
}
std::cout << "child quit ..." << std::endl;
sleep(1);
}
void FatherProcessRead(int rfd)
{
char inbuffer[size];//缓冲区
while (true)
{
//将管道里的信息读到缓冲区里
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1); // 注意:sizeof(inbuffer)->strlen(inbuffer);错的!!!
if (n > 0)
{
inbuffer[n] = 0; // == '\0' //当成字符串手动加'\0'
std::cout << "Father get message: " << inbuffer << std::endl;
}
else if (n == 0)
{
// 如果read的返回值是0,表示写端直接关闭了,我们读到了文件的结尾
std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;
break;
}
else if (n < 0)//读取错误
{
std::cerr << "read error" << std::endl;
break;
}
std::cout << std::endl;
}
}
int main()
{
// 1. 创建管道
int pipefd[2];
int n = pipe(pipefd);
if (n != 0)
{
std::cerr << "erro: " << errno << std::endl;
std::cerr << "errstring: " << strerror(errno) << std::endl;
return 1;
}
// 1.1 打印管道信息
std::cout << std::endl;
std::cout << "匿名管道创建成功!" << std::endl;
std::cout << "读端口 — pipefd[0]: " << pipefd[0] << std::endl;//读端口 — pipefd[0]
std::cout << "写端口 — pipefd[1]: " << pipefd[1] << std::endl;//写端口 — pipefd[1]
std::cout << std::endl;
sleep(1);
// 2. 创建子进程->子进程:write(写)
pid_t id = fork();
if (id == 0)
{
// 2.1 打印子进程信息
std::cout << std::endl;
pid_t myid = getpid();
std::cout << "I am child prosess,myid:" << myid << std::endl;
std::cout << "子进程关闭不需要的fd了, 准备发消息了" << std::endl;
std::cout << std::endl;
sleep(1);
// 3. 子进程关闭不需要的端口fd::关闭读端口,需要写端口
close(pipefd[0]);
//3.1 子进程向管道内写信息
SubProcessWrite(pipefd[1]);
// 子进程写工作结束,将写端关闭
std::cout << "5s, child close wfd" << std::endl;
close(pipefd[1]);
sleep(5);
exit(0);
}
// 父进程:read(读)::打印父进程信息
pid_t myid = getpid();
std::cout << "I am father prosess,myid:" << myid << std::endl;
std::cout << "父进程进程关闭不需要的fd了, 准备接收消息了" << std::endl;
sleep(1);
// 4. 父进程关闭不需要的端口fd
close(pipefd[1]);
//4.1 父进程向管道内读取信息
FatherProcessRead(pipefd[0]);
// 父进程读工作完毕,将读端关闭
std::cout << "5s, father close rfd" << std::endl;
close(pipefd[0]);
sleep(5);
// 等待子进程回收,避免产生僵尸进程
sleep(5);
// 查看退出状态
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
std::cout << "wait child process done, exit sig: " << (status & 0x7f) << std::endl;
std::cout << "wait child process done, exit code(ign): " << ((status >> 8) & 0xFF) << std::endl;
}
return 0;
}
在文件描述符角度深度理解管道
在内核角度理解管道本质
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,“Linux一切皆文件”。
管道读写规则
A:当没有数据可读时:
1:O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
2:O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
B:当管道满的时候:
1:O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
2:O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
C:如果所有管道写端对应的文件描述符被关闭,则read返回0。
D:如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
E:当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
管道特点
A:只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
B:管道提供流式服务
C:一般而言,进程退出,管道释放,所以管道的生命周期随进程
E:一般而言,内核会对管道操作进行同步与互斥
D:管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
命名管道
A:管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
B:如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
C:命名管道是一种特殊类型的文件
创建一个命名管道
命令行创建:
$ mkfifo filename
在程序里用函数创建:
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname:一个指向以 null 结尾的字符串的指针,指定了要创建的命名管道的路径和文件名。
mode:定义了命名管道的访问权限位。这个模式与 open 系统调用中的模式相同,但只影响文件的读取/写入/执行权限,并且会根据进程的 umask 值进行调整。
返回值:
成功时,mkfifo 返回 0。
失败时,返回 -1,并设置 errno 以指示错误。
错误代码:
EACCES:权限不足,无法在包含 pathname 的目录中创建文件。
EEXIST:pathname 指定的文件已存在。
ENOENT:指定的路径不存在。
ENOSPC:没有足够的磁盘空间。
EROFS:尝试在只读文件系统上创建命名管道。
关于这部分代码已经存在我的gitee里了,感兴趣的老铁可以看看:
XiangChao/Linux - 码云 - 开源中国 (gitee.com)https://gitee.com/RuofengMao/linux/tree/master/
匿名管道与命名管道的区别
A:匿名管道由pipe函数创建并打开。
B:命名管道由mkfifo函数创建,打开用open
C:FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
命名管道的打开规则
A:如果当前打开操作是为读而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
B:如果当前打开操作是为写而打开FIFO时:
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
system V 共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图
共享内存数据结构
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
共享内存函数
shmget函数
int shmget(key_t key, size_t size, int shmflg);
参数:
key:用于识别共享内存段的键。如果 key 是 IPC_PRIVATE,则创建一个新的共享内存段,而不是通过键查找现有的共享内存段。
size:共享内存段的大小,以字节为单位。
shmflg:一组标志位,用于设置共享内存段的权限和其他属性。这些标志可以包括:
IPC_CREAT:如果指定,并且 key 不是 IPC_PRIVATE,则在不存在具有该键的共享内存段的情况下创建一个新的共享内存段。
IPC_EXCL:与 IPC_CREAT 结合使用,如果指定,当尝试创建一个已经存在的共享内存段时,调用将失败并返回错误 EEXIST。
0666(八进制):默认权限,表示共享内存段的读写权限,将根据进程的 umask 值进行调整。
返回值:
成功时,shmget 返回共享内存段的标识符(一个非负整数)。
失败时,返回 -1,并设置 errno 以指示错误。
错误代码:
EACCES:没有足够的权限访问共享内存段。
EEXIST:IPC_CREAT 和 IPC_EXCL 标志被设置,但是键已经存在。
ENOENT:没有找到键对应的共享内存段,并且没有设置 IPC_CREAT。
ENOMEM:系统内存不足,无法创建共享内存段。
shmat函数
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存段的标识符,由 shmget 函数返回。
shmaddr:指定共享内存段附加到调用进程地址空间的地址。如果设置为 NULL,系统将选择一个合适的地址。
shmflg:控制共享内存段附加行为的标志位。常用的标志包括:
SHM_RDONLY:以只读方式附加共享内存段。
0:以读写方式附加共享内存段。
返回值:
成功时,shmat 返回共享内存段在调用进程地址空间中的实际地址。
失败时,返回 (void *)-1,并设置 errno 以指示错误。
错误代码:
EACCES:没有足够的权限访问共享内存段。
EINVAL:shmid 不合法或 shmaddr 不符合对齐要求。
ENOMEM:在指定的 shmaddr 地址无法附加共享内存段,或者系统内存不足。
说明:
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmctl函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:共享内存段的标识符,由 shmget 函数返回。
cmd:指定要执行的操作。可能的命令包括:
IPC_STAT:将 buf 指向的结构体填充为共享内存段的当前信息。
IPC_SET:设置共享内存段的属性,buf 指向要应用的新属性。
IPC_RMID:删除共享内存段,释放所有相关资源。
buf:指向 shmid_ds 结构体的指针,该结构体包含共享内存段的信息和/或设置。具体用法取决于 cmd 参数。
返回值:
成功时,返回 0。
失败时,返回 -1,并设置 errno 以指示错误。
错误代码:
EACCES:没有足够的权限执行指定的 cmd 操作。
EINVAL:shmid 不合法或 cmd 命令无效。
EPERM:试图删除不属于调用进程的共享内存段。
关于这部分代码已经存在我的gitee里了,感兴趣的老铁可以看看:
XiangChao/Linux - 码云 - 开源中国 (gitee.com)https://gitee.com/RuofengMao/linux/tree/master/
关于进程间通信的相关问题
1:进程间为什么要通信?
进程也是需要某种协同的,所以如何协同的前提条件:通信!
通信:数据也是有类别的,通知就绪的,单纯的要传递给当前进程的数据,控制相关信息......
事实:进程是具有独立性的!
2:进程间如何通信?
a:进程间通信成本相对较高!
b:进程间通信的前提,先让不同的进程,看到同一份(操作系统)资源("一段内存")。
一定是某一个进程先需要通信,让OS创建一个共享资源,所以OS必须要提供很多的系统调用,OS创建的共享资源不同,系统调用接口的不同,注定了进程间通信会有不同的种类。
3:管道的四种情况?
4:管道的五种特征?
信号量
进程互斥
A:由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
B:系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
C:在进程中涉及到互斥资源的程序段叫临界区特性方面
E:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核