Linux——进程间通信
目录
1. 进程间通信的介绍
1.1 概念
1.2 目的
1.3 进程间通信的本质
1.4 进程间通信的分类
2. 管道
2.1 概念
2.2 匿名管道
2.2.1 原理
2.2.2 pipe函数
2.2.3 匿名管道使用步骤
2.2.4 管道读写规则
2.2.5 管道的特点
2.2.6 管道的四种特殊情况
2.2.7 管道的大小
2.3 命名管道
2.3.1 原理
2.3.2 使用命令创建命名管道
2.3.3 使用函数创建命名管道
2.3.4 命名管道的打开规则
2.3.5 用命名管道实现server&client通信
2.3.6 用命名管道实现派发计算任务
2.3.7 用命名管道实现进程遥控
2.3.8 用命名管道实现文件拷贝
2.3.9 命名管道和匿名管道的区别
3. system V共享内存
3.1 共享内存的基本原理
3.2 共享内存数据结构
3.3 共享内存的建立与释放
3.4 共享内存的创建
3.5 共享内存的释放
3.6 共享内存的关联
3.7 共享内存的去关联
3.8 用共享内存实现server&client通信
3.9 共享内存与管道进行对比
1. 进程间通信的介绍
1.1 概念
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
1.2 目的
1、数据传输: 一个进程需要将它的数据发送给另一个进程。
2、资源共享: 多个进程之间共享同样的资源。
3、通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
4、进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.3 进程间通信的本质
进程间通信的本质就是,让不同的进程看到同一份资源。
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
1.4 进程间通信的分类
2. 管道
2.1 概念
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个“管道”。
例如,统计我们当前使用云服务器上的登录用户个数。
其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
tip: who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数。
2.2 匿名管道
2.2.1 原理
匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
注意:
1、这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
2、管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。
2.2.2 pipe函数
pipe函数用于创建匿名管道,pip函数的函数原型如下:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
pipe函数调用成功时返回0,调用失败时返回-1。
2.2.3 匿名管道使用步骤
在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
step1:父进程调用pipe函数创建管道。
step2:父进程创建子进程。
step3:父进程关闭写端,子进程关闭读端。
注意:
1、管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
2、从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。
我们可以站在文件描述符的角度再来看看这三个步骤:
step1:父进程调用pipe函数创建管道。
step2:父进程创建子进程。
这里其实就是发生了浅拷贝,子进程将父进程的文件描述符表拷贝了一份儿,但是指针的指向并没有改变,这样就能保证父子进程打开的是同一个文件。
step3:父进程关闭写端,子进程关闭读端。
例如,在以下代码当中,子进程向匿名管道当中写入10行数据,父进程从匿名管道当中将数据读出。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{int fd[2]={0};//创建匿名管道if(pipe(fd)<0){perror("pipe");return 1;}pid_t id=fork();if(id==0){//子进程:关闭读端close(fd[0]);//子进程向管道写入const char* str="Hello Father,I am child";int count=10;while(count--){write(fd[1],str,strlen(str));sleep(1);}//子进程写入完毕,关闭文件close(fd[1]);exit(0);}//父进程close(fd[1]);//父进程关闭写端char buff[64];while(1){ssize_t s=read(fd[0],buff,sizeof(buff));if(s>0){buff[s]='\0';printf("child send to father:%s\n",buff);}else if(s==0){printf("end of file\n");break;}else{printf("read fail\n");break;}}close(fd[0]);waitpid(id,NULL,0);return 0;
}
2.2.4 管道读写规则
pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe2函数的第二个参数用于设置选项。
1、当没有数据可读时:
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
2、当管道满的时候:
O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
2.2.5 管道的特点
1、管道内部自带同步与互斥机制。
我们将一次只允许一个进程使用的资源,称为临界资源。
管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。
临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。
为了避免这些问题,内核会对管道操作进行同步与互斥:
同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
2、管道的生命周期随进程。
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
这里我们可以了联想到我们之前学过的0\1\2文件描述符,我们在程序中一般不会主动去关闭它们,其实在进程退出的时候,系统自动将它们关闭了。
3、管道提供的是流式服务。
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
流式服务: 数据没有明确的分割,不分一定的报文段。
数据报服务: 数据有明确的分割,拿数据按报文段拿。
4、管道是半双工通信的。
单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。简单来说:任何一个时刻,一个发,一个收。
全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。简单来说:任何一个时刻,可以同时收发。
管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。
2.2.6 管道的四种特殊情况
1、写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
2、读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
3、写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后(read会读到文件末尾),就会继续执行该进程之后的代码逻辑,而不会被挂起。
4、读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
相关解释:
1、其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
2、第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
3、第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,OS不会做没有意义的事,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
我们可以通过代码查看第四种情况子进程会收到几号信号:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int fd[2] = {0};// 创建匿名管道if (pipe(fd) < 0){perror("pipe");return 1;}pid_t id = fork();if (id == 0){// 子进程:关闭读端close(fd[0]);// 子进程向管道写入const char *str = "Hello Father,I am child";int count = 10;while (count--){write(fd[1], str, strlen(str));sleep(1);}// 子进程写入完毕,关闭文件close(fd[1]);exit(0);}// 父进程close(fd[1]); // 父进程关闭写端close(fd[0]); // 父进程直接关闭读端(子进程会被系统杀掉)int status = 0;waitpid(id, &status, 0);printf("子进程收到信号:%d\n", status & 0x7F);return 0;
}
2.2.7 管道的大小
管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?
方法一:使用man手册
根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。
然后我们可以使用uname -r命令,查看自己使用的Linux版本。
方法二:使用ulimit命令
我们还可以使用ulimit -a命令,查看当前资源限制的设定。
根据上图,管道最大容量是512*8=4096bytes,这与我们上面man查出来的不一致,那么下面我们自己利用代码测试一下。
方法三:自行测试
前面说到,若是读端进程一直不读取管道当中的数据,写端进程一直向管道写入数据,当管道被写满后,写端进程就会被挂起。据此,我们可以写出以下代码来测试管道的最大容量。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{int fd[2] = {0};if (pipe(fd) < 0){ // 使用pipe创建匿名管道perror("pipe");return 1;}pid_t id = fork(); // 使用fork创建子进程if (id == 0){// childclose(fd[0]); // 子进程关闭读端char c = 'a';int count = 0;// 子进程一直进行写入,一次写入一个字节while (1){write(fd[1], &c, 1);count++;printf("%d\n", count); // 打印当前写入的字节数}close(fd[1]);exit(0);}// fatherclose(fd[1]); // 父进程关闭写端// 父进程不进行读取waitpid(id, NULL, 0);close(fd[0]);return 0;
}
大家可以看到,进程阻塞了,卡在65536这里,说明管道最大容量是65536bytes(64KB),这是在ubuntu22.04下管道的大小,不同的系统可能不一样。
2.3 命名管道
2.3.1 原理
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。
命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
注意:
1、普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
2、命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
2.3.2 使用命令创建命名管道
使用这个命名管道文件,就能实现两个进程之间的通信了。
我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
cp@hcss-ecs-348a:~/test1$ while :; do echo "Hello fifo";sleep 1; done > fifo
这时我们如果直接关闭读端,那么写端进程将被系统杀掉。
这里大家可以明显地看到系统报错,“Connection closed”,说明通信中断了。
2.3.3 使用函数创建命名管道
在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:
int mkfifo(const char *pathname, mode_t mode);
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
- 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
- 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。这里和我们之前学习的文件创建类似(管道本来也就是文件)。
mkfifo函数的返回值。
- 命名管道创建成功,返回0。
- 命名管道创建失败,返回-1。
示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>#define FILE_NAME "Myfifo"int main()
{umask(0); //将文件默认掩码设置为0if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件perror("mkfifo");return 1;}//创建成功return 0;
}
2.3.4 命名管道的打开规则
1、如果当前打开操作是为读而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功。
2、如果当前打开操作是为写而打开FIFO时。
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。
2.3.5 用命名管道实现server&client通信
共用头文件的代码如下:
//comm.h
#pragma once#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
对于如何让客户端和服务端使用同一个命名管道文件,这里我们可以让客户端和服务端包含同一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。
服务端代码(server.c)如下:
#include "comm.h"
int main()
{umask(0); // 设置权限掩码// 使用mkfifo创建命名管道if (mkfifo(FILE_NAME, 0666) < 0){perror("mkfifo");return 1;}int fd = open(FILE_NAME, O_RDONLY); // 以读的方式打开管道文件if (fd < 0){perror("open");return 2;}char msg[128];while (1){// 每次读之前清空msgmsg[0] = '\0';// 从命名管道中读取信息ssize_t s = read(fd, msg, sizeof(msg) - 1);//减1给\0留位置if (s > 0){msg[s] = '\0';printf("client:%s\n", msg);}else if (s == 0){printf("client quit\n");break;}else{printf("read error\n");break;}}close(fd);//通信完成,关闭管道文件return 0;
}
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。
客户端代码(client.c)如下:
#include"comm.h"
int main()
{int fd=open(FILE_NAME,O_WRONLY);//以写的方式打开管道文件if(fd<0){perror("open");return 1;}char msg[128];while(1){msg[0]='\0';//每次读清空msgprintf("请输入信息:");fflush(stdout);ssize_t s=read(0,msg,sizeof(msg)-1);if(s>0){msg[s-1]='\0';//把\n的位置改成0//将信息写入命名管道write(fd,msg,strlen(msg));}}close(fd);return 0;
}
而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。
代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。
下面我们在运行客户端,在客户端输入消息,服务端将看见用户端发来的消息;
大家观察上面的图,这样我们就实现了进程间的通信,当我们客户端终止进程,服务端也会跟着退出。
当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。
也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的。
服务端和客户端之间的退出关系
当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。
当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。
通信是在内存当中进行的
若是我们只让客户端向管道写入数据,而服务端不从管道读取数据,那么这个管道文件的大小会不会发生变化呢?
//server.c
#include "comm.h"int main()
{umask(0); //将文件默认掩码设置为0if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件perror("mkfifo");return 1;}int fd = open(FILE_NAME, O_RDONLY); //以读的方式打开命名管道文件if (fd < 0){perror("open");return 2;}while (1){//服务端不读取管道信息}close(fd); //通信完毕,关闭命名管道文件return 0;
}
这里大家发现,管道文件的大小为0,尽管服务端不读取管道当中的数据,但是管道当中的数据并没有被刷新到磁盘,也就说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的,这也就与我们前面的结论逻辑自洽了。
上面我们是分开实现了客户端和服务端,这里其实我们可以封装一个类,即“命名管道”类,这样代码的可读性就更强,也更加系统化。
comm.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define PATH "."
#define FILENAME "fifo"
#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)class Namedfifo
{
public:Namedfifo(const std::string &path, const std::string &name): _path(path), _name(name){_fifoname = path + "/" + name;umask(0);// 创建管道int n = mkfifo(_fifoname.c_str(), 0666);if (n < 0){ERR_EXIT("mkfifo");}else{std::cout << "mkfifo success" << std::endl;}}~Namedfifo(){// 删除管道文件int n = unlink(_fifoname.c_str());if (n < 0){ERR_EXIT("unlink");}else{std::cout << "remove success" << std::endl;}}private:std::string _path;std::string _name;std::string _fifoname;
};
class Fileoper
{
public:Fileoper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_fifoname = path + "/" + name;}void OpenforRead(){_fd=open(_fifoname.c_str(),O_RDONLY);if(_fd<0){ERR_EXIT("open");}std::cout<<"open fifo success"<<std::endl;}void OpenforWrite(){_fd=open(_fifoname.c_str(),O_WRONLY);if(_fd<0){ERR_EXIT("open");}std::cout<<"open fifo success"<<std::endl;}void Write(){//写入操作std::string messsage;int cnt=1;pid_t id=getpid();while(true){std::cout<<"请输入";std::getline(std::cin,messsage);messsage+=(", message number:"+std::to_string(cnt++)+",["+std::to_string(id)+"]");write(_fd,messsage.c_str(),messsage.size());}}void Read(){//读取操作while(true){char buffer[1024];int number=read(_fd,buffer,sizeof(buffer)-1);if(number>0){buffer[number]=0;std::cout<<"Client say:"<<buffer<<std::endl; }else if(number==0){std::cout<<"client quit"<<std::endl;break; }else{std::cout<<"read error"<<std::endl;break;}}}void Close(){if(_fd>0) close(_fd);}~Fileoper(){}private:std::string _path;std::string _name;std::string _fifoname;int _fd;
};
这里需要说明一下,.hpp为后缀的文件表示头源文件混编的,我们可以直接在这里实现对应的方法。
server.cc
#include"comm.hpp"
int main()
{//创建管道文件Namedfifo fifo(PATH,FILENAME);//文件操作Fileoper readerfile(PATH,FILENAME);readerfile.OpenforRead();readerfile.Read();readerfile.Close();return 0;
}
client.cc
#include"comm.hpp"
int main()
{Fileoper writerfile(PATH,FILENAME);writerfile.OpenforWrite();writerfile.Write();writerfile.Close();return 0;
}
Makefile
.PHONY:ALL
ALL:client server
client:client.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f client server
大家可以发现,我们封装以后,客户端和服务端的代码都变得非常简洁,我们可以很高效地完成通信操作(直接调用类中的接口就即可)。
2.3.6 用命名管道实现派发计算任务
需要注意的是两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行某些处理的。
这里我们以客户端向服务端派发计算任务为例,客户端通过管道向服务端发送双操作数的计算请求,服务端接收到客户端的信息后需要计算出相应的结果。
这里我们无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。
#include "comm.h"
int main()
{umask(0);if (mkfifo(FILE_NAME, 0666) < 0) // 使用mkfifo创建命名管道{perror("mkfifo");return 1;}int fd = open(FILE_NAME, O_RDONLY);if (fd < 0){perror("open");return 2;}char msg[128];while (1){msg[0] = '\0'; // 每次读之前清空msg// 从命名管道中读取信息ssize_t s = read(fd, msg, sizeof(msg) - 1);if (s > 0){msg[s] = '\0'; // 设置'\0',便于输出printf("client:%s\n", msg);// 服务端处理计算任务char *lable = "+-*/%";char *p = msg;int flag = 0;while (*p){switch (*p){case '+':flag = 0;break;case '-':flag = 1;break;case '*':flag = 2;case '/':flag = 3;break;case '%':flag = 4;break;}p++;}char *data1 = strtok(msg, "+-*/%");char *data2 = strtok(NULL, "+-*/%");int num1 = atoi(data1);int num2 = atoi(data2);int ret = 0;switch (flag){case 0:ret = num1 + num2;break;case 1:ret = num1 - num2;break;case 2:ret = num1 * num2;break;case 3:ret = num1 / num2;break;case 4:ret = num1 % num2;break;}printf("%d %c %d = %d\n",num1,lable[flag],num2,ret);}else if(s==0){printf("client quit\n");break;}else{printf("rean error\n");break;}}close(fd);return 0;
}
此时服务端接收到客户端的信息后,需要进行的处理动作就不是将其打印到显示器了,而是需要将信息经过进一步的处理,从而得到相应的结果。
2.3.7 用命名管道实现进程遥控
们可以通过一个进程来控制另一个进程的行为,比如我们从客户端输入命令到管道当中,再让服务端将管道当中的命令读取出来并执行。
下面我们只实现了让服务端执行不带选项的命令,若是想让服务端执行带选项的命令,可以对管道当中获取的命令进行解析处理。
这里的实现非常简单,只需让服务端从管道当中读取命令后创建子进程,然后再进行进程程序替换即可。
这里也无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。
#include "comm.h"
int main()
{umask(0);if (mkfifo(FILE_NAME, 0666) < 0){perror("mkfifo");return 1;}int fd = open(FILE_NAME, O_RDONLY);if (fd < 0){perror("open");return 2;}char msg[128];while (1){msg[0] = '\0';// 从命名管道读取数据ssize_t s = read(fd, msg, sizeof(msg) - 1);if (s > 0){msg[s] = '\0';printf("client:%s\n", msg);if (fork() == 0){// 子进程execlp(msg, msg, NULL); // 进行进程替换exit(1);}waitpid(-1, NULL, 0); // 等待子进程}else if (s == 0){printf("client quit\n");break;}else{printf("read error\n");break;}}return 0;
}
2.3.8 用命名管道实现文件拷贝
需要拷贝的文件是text.txt,该文件当中的内容如下:
我们要做的是,在客户端将text.txt文件发送给服务端,在服务端创建一个text_bat.txt文件,并将从管道获取到的数据写入text_bat.txt文件当中,至此便实现了text.txt文件的拷贝。
下面我们来实现具体代码:
其中服务端需要做的就是,创建命名管道并以读的方式打开该命名管道,再创建一个名为text_bat.txt的文件,之后需要做的就是将从管道当中读取到的数据写入到text_bat.txt文件当中即可。
服务端代码如下:
#include"comm.h"
int main()
{umask(0);if(mkfifo(FILE_NAME,0666)<0){perror("mkfifo");return 1;}int fd=open(FILE_NAME,O_RDONLY);if(fd<0){perror("open");return 2;}//创建文件text_bat.txt,并以写的方式打开该文件int fout=open("text_bat.txt",O_WRONLY,0666);if(fout<0){perror("write");return 3;}char msg[128];while(1){msg[0]='\0';//每次读之前将msg清空//从命名管道中读取信息ssize_t s=read(fd,msg,sizeof(msg)-1);if(s>0){write(fout,msg,s);//将读到的信息写入text_bat.txt文件中}else if(s==0){printf("client quit\n");break;}else{printf("read error\n");break;}}close(fd);//通信完毕,关闭管道文件close(fout);//数据写入完毕,关闭text_bat.txt文件return 0;
}
而客户端需要做的就是,以写的方式打开这个已经存在的命名管道文件,再以读的方式打开text.txt文件,之后需要做的就是将text.txt文件当中的数据读取出来并写入管道当中即可。
客户端的代码如下:
#include "comm.h"
int main()
{int fd = open(FILE_NAME, O_WRONLY); // 以写的方式打开管道文件if (fd < 0){perror("open");return 1;}int fin = open("text.txt", O_RDONLY);if (fin < 0){perror("open");return 2;}char msg[128];while (1){// 从text.txt中读取数据ssize_t s = read(fin, msg, sizeof(msg));if (s > 0){write(fd, msg, s); // 将数据写到命名管道中}else if (s == 0){printf("read end of file\n");break;}else{printf("read error\n");break;}}close(fd); // 通信完毕,关闭管道文件close(fin); // 数据读取完毕,关闭text.txt文件return 0;
}
编写完代码后,先运行服务端,再运行客户端,一瞬间这两个进程就相继运行结束了。
程序运行完毕后,我们查看text_bat.txt文件,发现其中的内容就是我们一开始创建的文件内容,至此我们就完成了拷贝工作。
使用管道实现文件的拷贝有什么意义?
因为这里是使用管道在本地进行的文件拷贝,所以看似没什么意义,但我们若是将这里的管道想象成“网络”,将客户端想象成“Windows Xshell”,再将服务端想象成“centos服务器”。
那我们此时实现的就是文件上传的功能,若是将方向反过来,那么实现的就是文件下载的功能。
2.3.9 命名管道和匿名管道的区别
1、匿名管道由pipe函数创建并打开。
2、命名管道由mkfifo函数创建,由open函数打开。
3、FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。
3. system V共享内存
3.1 共享内存的基本原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
注意:
这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成。
3.2 共享内存数据结构
在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理(先描述,再组织),所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。
共享内存的数据结构如下:
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 */
};
当我们申请了一块共享内存后,为了让要实现通信的进程能够看到同一个共享内存,因此每一个共享内存被申请时都有一个key值,这个key值用于标识系统中共享内存的唯一性。
可以看到上面共享内存数据结构的第一个成员是shm_perm
,shm_perm
是一个ipc_perm
类型的结构体变量,每个共享内存的key值存储在shm_perm
这个结构体变量当中,其中ipc_perm
结构体的定义如下:
struct ipc_perm{__kernel_key_t key;__kernel_uid_t uid;__kernel_gid_t gid;__kernel_uid_t cuid;__kernel_gid_t cgid;__kernel_mode_t mode;unsigned short seq;
};
3.3 共享内存的建立与释放
共享内存的建立大致包括以下两个过程:
- 在物理内存当中申请共享内存空间。
- 将申请到的共享内存挂接到地址空间,即建立映射关系。
共享内存的释放大致包括以下两个过程:
- 将共享内存与地址空间去关联,即取消映射关系。
- 释放共享内存空间,即将物理内存归还给系统
3.4 共享内存的创建
创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
int shmget(key_t key, size_t size, int shmflg);
shmget函数的参数说明:
第一个参数key,表示待创建共享内存在系统当中的唯一标识。
第二个参数size,表示待创建共享内存的大小。
第三个参数shmflg,表示创建共享内存的方式。
shmget函数的返回值说明:
shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
shmget调用失败,返回-1。
注意: 我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取;
key_t ftok(const char *pathname, int proj_id);
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。
需要注意的是,pathname所指定的文件必须存在且可存取。
注意:
1、使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
2、需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。
传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:
使用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。
至此我们就可以使用ftok和shmget函数创建一块共享内存了,创建后我们可以将共享内存的key值和句柄进行打印,以便观察,代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096
int main()
{key_t key=ftok(PATHNAME,PROJ_ID);//获取key值if(key<0){perror("ftok");return 1;}int shm = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);if(shm<0){perror("shmget");return 2;}printf("key:%x\n",key);printf("shm:%d\n",shm);return 0;
}
该代码编写完毕运行后,我们可以看到输出的key值和句柄值:
Linux当中,我们可以使用ipcs
命令查看有关进程间通信设施的信息。
单独使用ipcs
命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
- -q:列出消息队列相关信息。
- -m:列出共享内存相关信息。
- -s:列出信号量相关信息。
例如,携带-m选项查看共享内存相关信息:
此时,根据ipcs
命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了。
ipcs
命令输出的每列信息的含义如下:
注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系。
注意:
共享内存的大小是4KB(4096字节)的整数倍,如果我们申请4097,那么系统会向上取整,为我们申请4096*2,但是我们只能用4097个字节。
3.5 共享内存的释放
通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。
实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
使用命令释放共享内存资源
我们可以使用ipcrm -m shmid
命令释放指定id的共享内存资源。
注意: 指定删除时使用的是共享内存的用户层id,即列表当中的shmid。
使用程序释放共享内存资源
控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl函数的参数说明:
第一个参数shmid,表示所控制共享内存的用户级标识符。
第二个参数cmd,表示具体的控制动作。
第三个参数buf,用于获取或设置所控制共享内存的数据结构。
shmctl函数的返回值说明:
shmctl调用成功,返回0。
shmctl调用失败,返回-1。
其中,作为shmctl函数的第二个参数传入的常用的选项有以下三个:
例如,在以下代码当中,共享内存被创建,3秒后程序自动移除共享内存,再过3秒程序就会自动退出。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096
int main()
{key_t key=ftok(PATHNAME,PROJ_ID);//获取key值if(key<0){perror("ftok");return 1;}int shm = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);if(shm<0){perror("shmget");return 2;}printf("key:%x\n",key);//打印key值printf("shm:%d\n",shm);//打印句柄sleep(3);shmctl(shm,IPC_RMID,NULL);//释放共享内存sleep(3);return 0;
}
我们可以打开监控脚本,来观察共享内存的变化。
cp@hcss-ecs-348a:~/test1$ while :; do ipcs -m;echo "###################################";sleep 1;done
3.6 共享内存的关联
将共享内存连接到进程地址空间我们需要用shmat函数,shmat函数的函数原型如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmat函数的参数说明:
第一个参数shmid,表示待关联共享内存的用户级标识符。
第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
第三个参数shmflg,表示关联共享内存时设置的某些属性。
shmat函数的返回值说明:
shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
shmat调用失败,返回(void*)-1。
其中,作为shmat函数的第三个参数传入的常用的选项有以下三个:
其实这个函数就类似于我们之前学的malloc函数,申请空间,然后返回起始地址。
这时我们可以尝试使用shmat函数对共享内存进行关联:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096
int main()
{key_t key = ftok(PATHNAME, PROJ_ID); // 获取key值if (key < 0){perror("ftok");return 1;}int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);if (shm < 0){perror("shmget");return 2;}printf("key:%x\n", key); // 打印key值printf("shm:%d\n", shm); // 打印句柄sleep(2);char* mem=shmat(shm,NULL,0);//关联共享内存if(mem==(void*)-1){perror("shmat");return 3;}printf("attend end\n");sleep(2);shmctl(shm,IPC_RMID,NULL);//释放共享内存return 0;
}
代码运行后发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存。
我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096
int main()
{umask(0);key_t key = ftok(PATHNAME, PROJ_ID); // 获取key值if (key < 0){perror("ftok");return 1;}int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);if (shm < 0){perror("shmget");return 2;}printf("key:%x\n", key); // 打印key值printf("shm:%d\n", shm); // 打印句柄sleep(2);char *mem = shmat(shm, NULL, 0); // 关联共享内存if (mem == (void *)-1){perror("shmat");return 3;}printf("attend end\n");sleep(2);shmctl(shm, IPC_RMID, NULL); // 释放共享内存return 0;
}
我们只需要在创建共享内存时带上权限即可,这里需要先将系统的权限掩码置为0。
此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限。
3.7 共享内存的去关联
取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:
int shmdt(const void *shmaddr);
shmdt函数的参数说明:
待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。
shmdt函数的返回值说明:
shmdt调用成功,返回0。
shmdt调用失败,返回-1。
现在我们就能够取消共享内存与进程之间的关联了。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cp/test1/test.c"
#define PROJ_ID 0x6666 // 整数标识符
#define SIZE 4096
int main()
{umask(0);key_t key = ftok(PATHNAME, PROJ_ID); // 获取key值if (key < 0){perror("ftok");return 1;}int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);if (shm < 0){perror("shmget");return 2;}printf("key:%x\n", key); // 打印key值printf("shm:%d\n", shm); // 打印句柄sleep(2);char *mem = shmat(shm, NULL, 0); // 关联共享内存if (mem == (void *)-1){perror("shmat");return 3;}printf("attend end\n");sleep(2);printf("detach begin\n");sleep(2);shmdt(mem);//共享内存去关联printf("detach end\n");sleep(2);shmctl(shm, IPC_RMID, NULL); // 释放共享内存return 0;
}
运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联。
注意: 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系,比如上面的代码,我们最后还需要调用系统调用主动释放共享内存。
3.8 用共享内存实现server&client通信
在知道了共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了。在让两个进程进行通信之前,我们可以先测试一下这两个进程能否成功挂接到同一个共享内存上。
为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。
这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。
共用头文件的代码如下:
//comm.h
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>#define PATHNAME "/home/cp/test1/shm/server.c" //路径名#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后进入死循环,便于观察服务端是否挂接成功。
服务端代码如下:
//server.c
#include"comm.h"
int main()
{umask(0);key_t key=ftok(PATHNAME,PROJ_ID);//获取key值if(key<0){perror("ftok");return 1;}int shm=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//创建共享内存if(shm<0){perror("shmget");return 2;}printf("key:%x\n",key);//打印key值printf("shm:%d\n",shm);//打印用户层idchar* mem=shmat(shm,NULL,0);//关联共享内存while(1){}shmdt(mem);//共享内存去关联shmctl(shm,IPC_RMID,NULL);//释放共享内存return 0;
}
客户端只需要直接和服务端创建的共享内存进行关联即可,之后也进入死循环,便于观察客户端是否挂接成功。
客户端代码如下:
//client.c
#include"comm.h"
int main()
{key_t key=ftok(PATHNAME,PROJ_ID);//获取与server端相同的key值if(key<0){perror("ftok");return 1;}int shm=shmget(key,SIZE,IPC_CREAT);//获取与server端创建的共享内存的用户层idif(shm<0){perror("shmget");return 2;}printf("key:%x\n",key);printf("shm:%d\n",shm);char* mem=shmat(shm,NULL,0);//关联共享内存while(1){}shmdt(mem);//共享内存去关联return 0;
}
先后运行服务端和客户端后,通过监控脚本可以看到服务端和客户端所关联的是同一个共享内存,共享内存关联的进程数也是2,表示服务端和客户端挂接共享内存成功。
此时我们就可以让服务端和客户端进行通信了,这里以简单的发送字符串为例。
客户端不断向共享内存写入数据:
//客户端不断向共享内存写入数据
int i = 0;
while (1){mem[i] = 'A' + i;i++;mem[i] = '\0';sleep(1);
}
服务端不断读取共享内存当中的数据并输出:
//服务端不断读取共享内存当中的数据并输出
while (1){printf("client# %s\n", mem);sleep(1);
}
此时先运行服务端创建共享内存,当我们运行客户端时服务端就开始不断输出数据,说明服务端和客户端是能够正常通信的。
谈到这里,大家有没有发现一个问题,我们在对共享内存做读写操作时,并没有调用系统调用,反观上面命名管道,我们读写时直接使用了read和write等系统调用的接口,那么这是什么原因?
实际上答案与共享内存的映射位置有关,共享内存经过页表映射到进程地址空间的共享区,而共享区是属于用户的,可以让用户直接使用。
共享内存的优缺点
优点:
1、映射之后,读写直接被对方看到。
2、不需要进行系统调用来获取或者写入内容。
3、是速度最快的进程间通信方式。
缺点:
1、通信双方没有“同步机制”。
2、导致数据不一致问题,没有保护机制。(两个进程各跑各的,不存在谁等谁,如果写端正在写入,而读端没等写端写完就直接读了,那么可能读上来的数据不全,导致数据不一致问题)。
3.9 共享内存与管道进行对比
当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。
实际上,共享内存是所有进程间通信方式中最快的一种通信方式。
从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
1、服务端将信息从输入文件复制到服务端的临时缓冲区中。
2、将服务端临时缓冲区的信息复制到管道中。
3、客户端将信息从管道复制到客户端的缓冲区中。
4、将客户端临时缓冲区的信息复制到输出文件中。
我们再来看看共享内存通信:
从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:
1、从输入文件到共享内存。
2、从共享内存到输出文件。
所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少,并且不需要调用系统调用来进行读写操作。
相关文章:
Linux——进程间通信
目录 1. 进程间通信的介绍 1.1 概念 1.2 目的 1.3 进程间通信的本质 1.4 进程间通信的分类 2. 管道 2.1 概念 2.2 匿名管道 2.2.1 原理 2.2.2 pipe函数 2.2.3 匿名管道使用步骤 2.2.4 管道读写规则 2.2.5 管道的特点 2.2.6 管道的四种特殊情况 2.2.7 管道的…...
深入详解人工智能数学基础——微积分中拉格朗日乘数法在GAN训练中的应用
🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C++, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用…...
精益数据分析(26/126):依据商业模式确定关键指标
精益数据分析(26/126):依据商业模式确定关键指标 在创业与数据分析的探索之路上,每一次的学习都像是为前行点亮一盏灯。今天,我们依旧怀揣着共同进步的期望,深入解读《精益数据分析》的相关内容࿰…...
前端面试宝典---vue原理
vue的Observer简化版 class Observer {constructor(value) {if (!value || typeof value ! object) returnthis.walk(value) // 对对象的所有属性进行遍历并定义响应式}walk (obj) {Object.keys(obj).forEach(key > defineReactive(obj, key, obj[key]))} } // 定义核心方法…...
Cribl 上传lookup 表,传入数据进event
cribl 插入lookup 表,来数据有针对性的插入字段,对event 的数据进行字段插入。灵活性强。 The Lookup At long last, were ready to configure the lookup. First, lets create the Lookup table wed like to use. Getting the goods 先下载一个lookup 表,然后上传到cri…...
使用 binlog2sql 闪回 MySQL8 数据
【说明】 MySQL服务器版本 8.0.26 mysql> SELECT version(); ----------- | version() | ----------- | 8.0.26 | -----------Python 版本 Python 3.8.10 [infuq ~]# python -V Python 3.8.10【安装】 binlog2sql 官方地址 1.安装 binlog2sql [infuq ~]# git clone …...
蓝桥杯赛场反思:技术与心态的双重修炼
蓝桥杯赛场反思:技术与心态的双重修炼 在刚刚结束的第十六届蓝桥杯大赛软件赛省赛第二场中,我经历了一场充满挑战与自我审视的旅程。走出赛场,内心既有些许成就感,也夹杂着对自身不足的深刻反思。这次比赛不仅是一次技术的较量&a…...
介绍常用的退烧与消炎药
每年春夏交替之季,是感冒发烧、咳嗽、咽喉肿痛、支气管炎、扁桃体炎的高发期。在家里或公司,常备几种预防感冒发烧、咳嗽、流鼻涕、咽喉发炎的药品,是非常必要的。下面介绍几款效果非常明显的中成药、西药,具体如下。 1 莲芝消炎…...
C++篇——继承
目录 引言 1.继承的概念及定义 1_1,继承的概念 1_2, 继承定义 1_2_1,继承关系和访问限定符 1_2_2,继承基类成员访问方式的变化 2.基类和派生类对象赋值转换 3.继承中的作用域 4.派生类的默认成员函数 构造函数 拷贝构造…...
C++ 基础综合练习案例01:联系人管理系统(Part01)
通讯录是一个可以记录亲人、好友信息的工具。 本教程主要利用C来实现一个通讯录管理系统 系统中需要实现的功能如下: * 添加联系人:向通讯录中添加新人,信息包括(姓名、性别、年龄、联系电话、家庭住址)最多记录1000人…...
Trae 宝藏功能实测:从 Mcp 搭建天气系统,到 AI 重塑 Excel 数据处理
本文 利用trae以及第三方MCP Server搭建一个天气系统网页前言链接高德地图MCP链接quickchart-server MCP Server链接EdgeOne Pages Deploy MCP智能体的创建天气系统效果展示 利用trae做一个Excel格式化工具前言使用trae完成代码的实现总结 我正在参加Trae「超级体验官」创意实践…...
MCP与Sequential Thinking:系统问题的分解与解决之道
MCP与Sequential Thinking:系统问题的分解与解决之道 引言:复杂问题背后的逻辑思维 在面对复杂问题时,我们常常感到手足无措,尤其是在需要将任务分解为多个步骤时。这是对个人思维能力的极大挑战,而掌握有效的思维工具则可以让事情事半功倍。今天我们讨论的两个工具:MC…...
Scrapy爬取动态网页:简洁高效的实战指南
引言 动态网页依赖JavaScript加载,传统爬虫望而却步。Scrapy搭配scrapy-splash却能轻松破局!本文通过一个原创案例,带你用Scrapy和Splash高效爬取动态网页,代码简洁、可运行,从零基础到进阶开发者都能快速上手。无论是数据采集还是自动化任务,这篇指南让你一学即会,开启…...
在 Linux 上安装 PNPM 的教程
在 Linux 上安装 PNPM 的教程 PNPM(Performant NPM)是一个非常快速的包管理器,作为 npm 的替代品,PNPM 在安装速度和磁盘占用方面都具有显著优势。PNPM 通过“硬链接”共享依赖来节省磁盘空间,并且比 npm 更加高效。本…...
Vue3 组件通信与插槽
Vue3 组件通信方式全解(10种方案) 一、组件通信方式概览 通信方式适用场景数据流向复杂度Props/自定义事件父子组件简单通信父 ↔ 子⭐v-model 双向绑定父子表单组件父 ↔ 子⭐⭐Provide/Inject跨层级组件通信祖先 → 后代⭐⭐事件总线任意组件间通信任…...
php一些命名规范 和 css命名规范
一 php命名规范 $myName bill gates;$yourFamilyName ggbone; 1.1 变量命名 变量以美元符号 $ 开头, 第一个字符不可以是数字 ,除了下划线_ 不能有任何符号 $name bill;$age 33; 当用2个或2个以上的单词命名变量时,可以使用驼峰法规则(…...
【TypeScript】速通篇
目录 0 前言 1 准备工作 1.1 安装typescript包 1.2 简化运行TS 2 类型注解 2.1 常用类型 2.1.1 原始类型 2.1.2 数组类型 2.1.3 联合类型 2.1.3.1 类型别名 2.1.4 函数类型 2.1.4.1 void类型 2.1.4.2 可选参数 2.1.5 对象类型 2.1.5.1 可选属性 2.1.5.2 接口 2.…...
flutter 引擎初始化
在 Flutter 混合开发中,iOS 端的 Flutter 引擎初始化时机 取决于集成方式(纯 Flutter 或混合开发)。以下是详细分析: 1. 纯 Flutter 应用(默认 Flutter App) 初始化时机 启动…...
Spring Boot 连接 Microsoft SQL Server 实现登录验证
Spring Boot 连接 Microsoft SQL Server 实现登录验证 这篇文章将非常系统地讲解如何使用 Spring Boot 结合 Microsoft SQL Server 2019 完成一个完整的登录验证系统,包括数据库连接问题、SSL证书错误处理、CORS跨域详细解释和解决方案。 适合需要前后端联调、单独…...
腾讯云智三道算法题
import java.math.BigDecimal; import java.math.BigInteger; import java.util.*;public class MyMain {//第一题:一个水果切成n块public static void getRes(int n, int l, int r){int min -1;int max -1;for (int il;i<r;i){if (i%n0){min i/n;break;}}for…...
语音合成之七语音克隆技术突破:从VALL-E到SparkTTS,如何解决音色保真与清晰度的矛盾?
从VALL-E到SparkTTS,如何解决音色保真与清晰度的矛盾? 引言语音克隆技术发展史YourTTS:深入剖析架构与技术VALL-E:揭秘神经编解码语言模型MaskGCTSparkTTS:利用 LLM 实现高效且可控的语音合成特征解耦生成式模型特征解…...
【Pandas】pandas DataFrame rdiv
Pandas2.2 DataFrame Binary operator functions 方法描述DataFrame.add(other)用于执行 DataFrame 与另一个对象(如 DataFrame、Series 或标量)的逐元素加法操作DataFrame.add(other[, axis, level, fill_value])用于执行 DataFrame 与另一个对象&…...
maven打包时配置多环境参数
1. pom配置 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.…...
【Linux】基本指令(下)
目录 一、详解指令补充知识1:什么是压缩 19. zip 指令(1)-r 选项(2)zip 和 unzip 的安装 20. unzip 指令(1)-d 选项补充知识2:本地机器与云服务器补充知识3:本地机器与云…...
NVLink、UALink 崛起,PCIe Gen6 如何用 PAM4 迎战未来?
现在数字经济发展地相当快速,像Cloud、现在火红的AI、大数据这些新技术都需要在数据中心里运行更多运算,伴随而来的是更快的数据传输速度的需求。 在数据中心,有很多条数据传输路径,举例 : Server 和Storage之间&…...
23种设计模式-行为型模式之迭代器模式(Java版本)
Java 迭代器模式(Iterator Pattern)详解 🧠 什么是迭代器模式? 迭代器模式是一种行为型设计模式,它提供一种方法顺序访问一个聚合对象中的各个元素,而不暴露该对象的内部表示。 🎯 使用场景 …...
指标监控:Prometheus 结合 Grafana,监控redis、mysql、springboot程序等等
软件作用说明 Prometheus:采集各种指标数据(如CPU、内存、请求数),并存储到时序数据库中。Grafana:数据可视化,生成监控仪表盘。 架构说明 被监控服务(如Redis/MySQL/SpringBoot&a…...
微信小程序,基于uni-app的轮播图制作,调用文件中图片
以便捷为目的,想使用文件中的图片制作轮播图 但网上找到的都是轮播图彼此分割,没有使用数组存储在一起,不便于管理,代码不美观简洁 作者使用文件中的图片,并使用数组制作轮播图的具体操作如下:࿰…...
未来医院已来:AI如何实现无死角安全监控
AI智慧医院如何用算法守护安全与效率 ## 背景:医疗场景的智能化转型需求 现代医院作为人员密集、场景复杂的公共场所,面临诸多管理痛点:患者跌倒可能延误救治、医闹事件威胁安全、医疗垃圾处置不当引发感染风险、重点区域(如药…...
搭建动态SQL取数
日常取数的时候可能会存在动态SQL的问题,比如取数动态或者条件动态等情况,下面针对动态SQL做一个完整的处理。包括SELECT 、FROM、WHERE 以及 最后table的动态。 首先 数据定义,这里全按照表来append处理 TYPES:BEGIN OF ty_data,edpline T…...
Python函数基础:简介,函数的定义,函数的调用和传入参数,函数的返回值
目录 函数简介 函数定义,调用,传入参数,返回值 函数的定义 函数的调用和传入参数 函数的返回值 函数简介 函数简介:函数是组织好,可重复使用,用来实现特定功能(特定需求)的代码…...
下垂控制属于构网型控制技术
下垂控制属于构网型控制,而非跟网型控制。 一、构网型与跟网型控制的本质区别 控制策略差异 构网型控制(Grid-Forming Control, GFM): 通过模拟同步发电机的特性(如转子运动方程),自主构建电压幅…...
主流 LLM 部署框架
主流 LLM 部署框架 框架主要特点适用场景vLLM- 超快推理(高吞吐) - 动态批处理 - 支持 HuggingFace Transformer - 支持 PagedAttention高并发、低延迟在线推理TGI (Text Generation Inference)- Huggingface官方出品 - 多模型管理 - 支持动态量化 - 支持…...
数据库系统概论(四)关系操作,关系完整性与关系代数
数据库系统概论(四)详细讲解关系操作,关系完整性与关系代数 前言一、什么是关系操作1.1 基本的关系操作1.2 关系数据语言的分类有哪些 二、关系的完整性2.1 实体完整性2.2 参照完整性2.3 用户的定义完整性 三、关系代数是什么3.1 传统的集合运…...
C#里使用libxl来加载网络传送过来的EXCEL文件
从服务器传送过来的数据,是一个EXCEL文件, 那么怎么样获取里面的数据比较合适呢? 是不是把数据先保存到文件,再使用传统的方式打开它呢? 其实这样做,也是可以的,对于比较大的文件来说。 如果文件比较小,就不必要这样做了,可以直接保存在内存,然后使用函数LoadRaw…...
Make + OpenOCD 完成STM32构建+烧录
目录 前言 准备工作 开始操作 后记 前言 前两篇通过VSCodeSTM32CubeMx跑通了用EIDE构建烧录。为今天的工作打下了非常棒的基础!今天来尝试手动构建烧录。 准备工作 安装Make,我这次用的是Win10,所以需要安装一个新朋友 msys2 ࿰…...
Linux:进程间通信->命名管道
1. 命名管道 概念 是一种进程间通信(IPC)机制,能允许没有关联的两个进程进行数据交换。 由于匿名管道只能在有亲缘关系的父子进程间通信所以具有局限性,所以就要通过命名管道来对两个没有关系的进程进行通信。 命名管道是通过路径和文件名来使两个进…...
CS001-50-depth
目录 深度图 如何写入深度图 长什么样子 copy depth pass z反转 如何读取深度图&还原世界坐标 深度图 深度图,是记录离物体离摄像机最近的图。 如何写入深度图 深度图,在urp中,如果相机开启了需要深度图的话,会自动在…...
开源AI视频FramePack发布:6GB显卡本地运行
您现在可以在自己的笔记本电脑上免费生成完整的离线AI视频。 只有GPU和纯粹的创造力。 这到底是什么? 一个名为FramePack的新型离线AI视频生成器几天前在GitHub上发布 — 几乎没人在谈论它。这很奇怪,因为这个工具真的很厉害。 它允许您从静态图像和提示词在自己的机器上…...
P3309 [SDOI2014] 向量集 Solution
Description 有一个向量列表,初始为空,有 n n n 个操作分两种: add ( x , y ) \operatorname{add}(x,y) add(x,y):将向量 ( x , y ) (x,y) (x,y) 添加到列表末尾. query ( x 0 , y 0 , l , r ) \operatorname{query}(x_0…...
深入探究 MySQL 架构:从查询到硬件
了解数据库的底层工作原理对于开发人员和系统架构师来说至关重要。在本指南中,我们将探索 MySQL 查询的奇妙旅程,从它离开应用程序的那一刻起,直到到达物理存储层——每个步骤都配有真实的示例。 旅程开始:应用层 当您的应用程序执行 SQL 查询时,它会启动一系列复杂的事件…...
matlab实现稀疏低秩去噪
稀疏低秩去噪的matlab代码,包括OMP算法与KSVD算法 IGARSS2013/cal_ssim.m , 6372 IGARSS2013/Compute_NLM_Matrix.m , 2004 IGARSS2013/FeatureSIM.m , 18790 IGARSS2013/KSVD_Matlab_ToolBox2/demo1.m , 1907 IGARSS2013/KSVD_Matlab_ToolBox2/demo2.m , 3679 IGA…...
泽润新能IPO隐忧:募资缩水2亿元,毛利率两连降,内控存瑕疵?
撰稿|行星 来源|贝多财经 又一家光伏企业,即将登陆资本市场。 近日,江苏泽润新能科技股份有限公司(SZ: 301636,下称“泽润新能”)对外发布了首次公开发行股票并在创业板上市的招股意向书,并于4月25日启动…...
20250426在ubuntu20.04.2系统上打包NanoPi NEO开发板的FriendlyCore系统刷机eMMC的固件
20250426在ubuntu20.04.2系统上打包NanoPi NEO开发板的FriendlyCore系统刷机eMMC的固件 2025/4/26 21:30 缘起:使用NanoPi NEO开发板,编译FriendlyCore系统,打包eMMC固件的时候报错。 1、在ubuntu14.04下git clone异常该如何处理呢ÿ…...
商用车与农用车电气/电子架构 --- 赋能智能车队管理与远程信息处理
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...
Medical Image Nnalysis发表对抗多实例学习框架,基于病理切片进行生存分析
小罗碎碎念 在医学AI领域,全切片图像(WSI)的生存分析对疾病预后评估至关重要。 现有基于WSI的生存分析方法存在局限性,经典生存分析规则使模型只能给出事件发生时间的点估计,缺乏预测稳健性和可解释性;且全…...
Ubuntu20.04部署Dify(Docker方式)
Ubuntu20.04部署Dify(Docker方式) Ubuntu20.04 DifyInstall DockerInstall Docker ComposeRun DifyRunning Ollama 由于写这篇博客的时候电脑还没装输入法,所以先用半吊子英文顶着了…关于最后运行ollama的部分可以无视,因为我修改…...
常见的六种大语言模型微调框架
六大主流微调框架详细解析 框架简介优势劣势Hugging Face PEFT专注于「参数高效微调」(LoRA、Prefix、Prompt-tuning等)的小型库,直接挂在Transformers上用。简单稳定,兼容性好,文档丰富,适配各种小模型到中…...
高精度3D圆弧拟合 (C++)
本文的目的是实现高精度的3D圆弧拟合,若对精度要求不高,可使用PCL的圆拟合接口,参见 PCL拟合空间3D圆周 fit3DCircle-CSDN博客 ---------------------------------------------------------------------------------------------------------…...
WPF定义扩展属性和依赖属性
WPF扩展属性与依赖属性详解 一、依赖属性(Dependency Property)详解 1. 什么是依赖属性? 依赖属性是WPF框架的核心特性之一,它允许属性值依赖于: 父元素的属性值(继承)样式和模板动画数据绑定资源查找2. 依赖属性的特点 属性值继承:子元素可以继承父元素的属性…...