您的位置:首页 > 游戏 > 手游 > 比较开放的浏览器网址_长沙房地产交易中心_成人短期就业培训班_黄冈网站推广厂家

比较开放的浏览器网址_长沙房地产交易中心_成人短期就业培训班_黄冈网站推广厂家

2025/3/13 2:55:33 来源:https://blog.csdn.net/2301_80194476/article/details/146094923  浏览:    关键词:比较开放的浏览器网址_长沙房地产交易中心_成人短期就业培训班_黄冈网站推广厂家
比较开放的浏览器网址_长沙房地产交易中心_成人短期就业培训班_黄冈网站推广厂家

博客封面

✨✨所属专栏:Linux✨✨

✨✨作者主页:嶔某✨✨

Linux:进程间通信

介绍:

目的:

  • 数据传输,一个进程需要将它的数据发送给另一个进程
  • 资源共享,多个进程间共享同样的资源
  • 通知事件,一个进程需要向另一个或一组进程发送消息,通知它(们)发生了某种事件(如进程终止通知父进程)
  • 进程控制,有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

通信方式及发展:

  • 管道

    匿名管道

    命名管道

  • System V IPC

System V 消息队列

System V 共享内存

System V信号量

  • POSIX IPC

    消息队列

    共享内存

    信号量

    互斥量

    条件变量

    读写锁

管道

管道是Unix比较古老的一种进程间通信的形式,我们把一个进程连接到另一个数据流成为一个“管道”

image-20250304150431645

匿名管道

#include <unistd.h>
//功能:创建一匿名管道
//函数原型:
int pipe(int fd[2]);//参数:
//fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
//返回值:成功返回0,失败返回错误代码

例子:可以使用管道从键盘stdin读取数据,写到屏幕stdout,具体代码如下

#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 stdinwhile (fgets(buf, 100, stdin)){len = strlen(buf);// write into pipeif (write(fds[1], buf, len) != len){perror("write to pipe");break;memset(buf, 0x00, sizeof(buf));// read from pipeif ((len = read(fds[0], buf, 100)) == -1){perror("read from pipe");break;}// write to stdoutif (write(1, buf, len) != len){perror("write to stdout");break;}}}
}

另外pipe可以用于父子之间通信,fork后,子进程会继承父进程的文件描述符,父子进程分别关闭对应的读或写端就可以实现单向通信

image-20250304210312914

我们在操作管道的时候,操作的是文件描述符。那么,匿名管道是文件吗?

首先匿名管道在磁盘中并没有对应的空间,只是在内核层面维护着一段缓冲区(内存上),但是它又继承了一些文件的操作,比如可以使用系统调用readwrite对其进行读写操作。总之匿名管道不是实体文件,但是在行为上和真正的文件相似,这也迎合了Linux下一切皆文件的思想。

管道的读写规则
  • 当没有数据可读时:

    O_NONBLOCK disable(禁用非阻塞模式):read调用时阻塞,即进程暂停执行,一直等到有数据来为止。

    O_NONBLOCK enable(使用非阻塞模式):read调用返回-1,error值为EAGAIN

  • 当管道满的时候:

    O_NONBLOCK disable(禁用非阻塞模式):write调用时阻塞,直到有进程来读走数据。

    O_NONBLOCK enable(使用非阻塞模式):write调用返回-1,error值为EAGAIN

  • 如果所有管道写端对应的文件描述符被关闭,read返回0。

  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。

  • 当要写如的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。

  • 当要写如的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

管道特点
  • 只能用于具有共同祖先的进程(或者是具有血缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
  • 管道提供流式服务
  • 管道的生命周期是随着进程的,进程退出,管道就释放了。
  • 内核会对管道进行同步和互斥
  • 管道通信时半双工的,数据只能向一个方向流通;需要双方通信时,需建立两个管道。
基于匿名管道的进程池

这个小项目,就是先创建一个父进程,之后fork出多个子进程,并分别创建管道。当父进程收到任务时,可以通过朝对应管道发送任务码,从而控制对应的子进程去完成任务,实现了任务分配。

任务分配时该选哪一个子进程呢?轮询,随机,还是给每一个进程tag一个任务量,不同的方法有不同的好处。

另外要注意,之前父进程开的管道的读端会继承给下一个子进程,这样第一个子进程对应管道的读端就会有多个读端,引用计数随着进程的增多不断增多。这是一个藏的比较深的bug当时如果不是蛋哥说出来我一定不知道。解决方法是在fork新的子进程时删除上一个子进程对应管道的写端。这样每一个子进程对应的管道的读端引用计数都是1。在删除的时候本该是关掉对应的写端,读端读到0然后退出之后父进程waitpid回收资源拿到退出码。如果读端的引用计数不是1,就会出现关闭父进程的写端后,子进程不会退出,一直在read那里阻塞。

具体信息可以参考源代码:25/Process_Pool · 钦某/Code - 码云 - 开源中国 (gitee.com)

命名管道

匿名管道的限制就是只能在具有共同祖先(亲缘关系)的进程间通信。如果我们想在不相关的进程间交换数据,就可以使用FIFO文件来进行,它被叫做命名管道。本质上也是文件,有文件名,所以叫做命名管道

命名管道可以在命令行上创建:

$ mkfifo filename

命名管道可以代码里创建,相关函数:

int mkfifo(const char* filename, mode_t mode); // 这个函数在手册3中,不属于系统调用int main()
{mkfifo("p2", 0644);return 0;
}
匿名管道与命名管道的区别
特性匿名管道命名管道
通信范围亲缘关系进程任意进程(可跨网络)
通信方向半双工全双工
存在形式内存缓冲区,无文件实体文件系统路径(如FIFO文件)
创建方式pipe()mkfifo()或系统API
生命周期随进程结束销毁需显式删除文件
典型应用场景父子进程快速通信多进程协作、服务端-客户端
命名管道的打开规则

如果当前打开操作是为读而打开FIFO时

O_NONBLOCK disable(禁用非阻塞模式):阻塞直到有相应进程为写而打开该FIFO

O_NONBLOCK enable(使用非阻塞模式):立刻返回成功

如果当前打开操作是为写而打开FIFO时

O_NONBLOCK disable(禁用非阻塞模式):阻塞直到有相应进程为读而打开该FIFO

O_NONBLOCK enable(使用非阻塞模式):立刻返回失败,错误码为ENXIO

基于命名管道的进程间通信封装

这个项目里只有一个类,这个类里面只有三个开放的成员函数:

  1. 打开管道文件
  2. 对管道文件进行操作
  3. 获取管道文件的文件描述符fd

进程在初始化类时会传入一个参数,这个参数由宏定义,SERVERCLIENT,初始化后,系统会自动调用构造函数,并把SERVERCLIENT赋值给成员变量,方便后续进行条件编译。在构造函数里面条件调用了一个创建命名管道文件的私有函数。在server端初始化时会调用这个函数。而在client端则不会调用该函数。

在Operate函数中也使用了条件编译,如果成员变量为SERVER则调用Read函数从管道里面读取数据,如果成员变量为CLIENT则调用Write对管道进行写入。在该类中,并没有对ReadWrite函数做高耦合。这也许会是后续复用代码时需要改进的方向之一。

system V 共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说,进程不在通过调用进入内核的系统调用来传递数据

image-20250306211429284

共享内存数据结构struct

在操作系统中不仅仅只有两个进程通过共享内存存相互通信,那么操作系统就必须要将这么多的共享内存管理起来。那么又是这个老生常谈的问题,**先描述再组织。**所以内核中组织共享内存的结构体就应运而生了。

结构体里面记录了权限、大小、最后一次关联时间、最后一次改变时间、创建进程的pid、最后一个操作进程的pid等信息。

/* Obsolete, used only for backwards compatibility and libc5 compiles */
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:这个共享内存段名字,由ftok函数规定获取

size:共享内存大小

shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的,取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。取值为IPC_CREAT | IPC_EXCL共享内存不存在,创建并返回;共享内存已存在,出错返回。

返回值:成功则返回一个非负整数,即该共享内存段的标识码;失败返回-1。

  • shmat函数

功能:将共享内存段连接到进程地址空间

原型:

void *shmget(int shm_id, const void *shmaddr, int shmflg);

参数:

shm_id:共享内存标识

shmaddr:指定连接的地址

shmflg:它的两个可能取值是SHM_RNDSHM_RDONLY

说明:

shmaddr为NULL,核心自动选择一个地址

shmaddr不为NULL切shmflg无SHM_RND标记,则以shmaddr为连接地址

shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址回自动向下调整为SHMLBA的整数倍

公式:shmaddr - (shmaddr % SHMLBA)

shmflg = SHM_RDONLY,表示连接操作用来只读共享内存

返回值:成功返回一个指针,指向共享内存的第一个节;失败返回-1

  • shmdt函数

功能:将共享内存段与当前进程脱离

原型:

int shmdt(const void *shmaddr);

参数:shmaddr是由shmat返回的指针

返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

  • shmctl函数

功能:用于控制共享内存

原型:

int shmctl(int shmid, int cmd, struct shm_ds *buf);

参数:

shmid:由shmget返回的共相内存标识码

cmd:将要采取的动作(三个可取值)

命令说明
IPC_STAT把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET在进程由足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
IPC_EMID删除共享内存段

buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值:成功返回0;失败返回-1

基于system V共享内存的进程间通信

在这个项目中只有一个类Shm且只有一个方法就是获取共享内存的地址,也就是调用了函数shmat并返回。其他的方法都被封装到了构造函数和析构函数中。用户只需要在初始化阶段传入一个参数用于指明serverclient即可。

另外,在此项目中,还使用了命名管道(被修改过的命名管道类)进行进程间通信的第二信道,client进程结束前,会通过第二信道发送一条指令,server在收到指令后会跳出循环,调用类的析构函数,结束进程。

详情可参考代码:25/Share_memery · 钦某/Code - 码云 - 开源中国 (gitee.com)

需要注意的是:共享内存不像管道那样,由同步和互斥机制,这也会导致缺乏控制,会带来并发问题,但是有缺点就有优点,它快啊!
装到了构造函数和析构函数中。用户只需要在初始化阶段传入一个参数用于指明serverclient即可。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com