零基础入门进程间通信:task 1(匿名管道与vscode使用)
目录
引言
VSCODE使用
进程间通信正题
基础背景
进程间通信分类
匿名管道
理解匿名管道
代码实现
匿名管道的特性
管道的四种情况
应用场景
引言
在当今的计算机技术领域,操作系统作为计算机系统的核心组件,承担着资源管理、任务调度和进程管理等重要职责。Linux作为一种开源、高性能、稳定的操作系统,广泛应用于服务器、嵌入式设备和个人电脑中。在Linux系统中,进程是资源分配和调度的基本单位,而进程间通信(Inter-Process Communication,IPC)则是确保多个进程能够协同工作、共享数据的关键技术。
本系列文章将通过task1-4来完成进程间通信的学习。
task 1将着重介绍:1.VScode远端连接linux云服务器 2.匿名管道通信
VSCODE使用
将VScode与linux远端的机器进行连接。
首先我们需要下载remote插件,F1找到remote添加远端主机指令。
输入用户名和远端主机的信息:ssh XXX@12345678
我们可以选择记住这个主机。
完成之后自己的用户内部有一个文件。
这个文件内部就添加了主机信息了(可以进行删除)
这样就提示连接上了远端。
需要注意的是,我们写完代码必须Ctrl + S保存才能保存文件。
从此我们就不需要用vim了,用vscode就可以进行代码编写
进程间通信正题
基础背景
进程通信的本质是:让不同的进程看到同一份资源。
这个资源一般是特定的内存空间,起这个资源一般是OS提供。
system V一般本机通信
posix一般网络通信
system V的通信标准给出了三个:消息队列、共享内存、信号量。
当然还有基于文件的通信:管道
进程间通信分类
匿名管道
理解匿名管道
父进程用wr的形式分别open打开一个文件
fork出子进程之后,子进程会继承父进程文件描述符表files_struct。指向同样的file结构。
父子关闭不需要的接口,就可以实现资源的互联。
进程就像是用户,关闭打开某个文件,对文件产生的影响都是通过OS进行的。file结构内部存在计数器,关闭文件会造成计数器--。
匿名管道只能用于亲戚进程通信:父子可以、兄弟可以、孙子爷爷可以(只要有一样的files_struct就可以)。
open打开的是磁盘的文件,内存的文件怎么办?
pipe用于打开在内存级别的一个文件,这个文件也存在缓冲区……
创建一个管道pipe(用读和写的形式打开一个文件两次)。
大部分系统调用,用于判断是否成功,成功一般返回0,否则-1.
传参提醒你,传入一个两个元素组成的int数组,这是一个输出型参数
pipefd[0]是读文件打开方式的fd
pipe[1]是写方式打开文件的fd
代码实现
实现一个子进程写,父进程读的逻辑代码
int main()
{
int pipefd[2] = {0};
int ret = pipe(pipefd); //创建内存管道文件,0是read端,1是write端。虽然fd不同,但是是同一个文件。
if (ret < 0)
{
cout << "Failed to create pipe" << endl;
return -1;
}
// cout << "pipefd[0] = " << pipefd[0] << ", pipefd[1] = " << pipefd[1] << endl;
pid_t id = fork();
if (id < 0)
{
cout << "Failed to fork" << endl;
return -1;
}
//子进程写,父进程读
if (id == 0)
{
// child process
close(pipefd[0]);
//ipc code
Writer(pipefd[1]); //不断写
close(pipefd[1]);
exit(0);
}
// parent process
close(pipefd[1]);
//ipc code
//虽然子进程将pipepid[0]关闭了,但是只会让file结构--,父进程还是可以对pipefd[0]进行读操作的。
Reader(pipefd[0]);
pid_t retid = waitpid(id, nullptr, 0);
if (retid < 0)
{
cout << "Failed to wait child process" << endl;
return 3;
}
close(pipefd[0]);
return 0;
}
写入缓冲区
//child process
void Writer(int Wfd)
{
string str = "Hello, im child process!";
pid_t pid = getpid();
int number = 0;
char buffer[1024] = {0};
while (true)
{
//构建发送字符串
buffer[0] = '\0'; //清空缓冲区,告诉读者,这是一个字符串
snprintf(buffer, sizeof(buffer), "%s pid = %d, number = %d\n", str.c_str(), pid, number++);
// cout << buffer << endl;
sleep(10);
//发送字符串给父进程(只要你是一个文件,存在fd,就可以用write向文件写入内容)
write(Wfd, buffer, strlen(buffer)); //write是写入到了文件缓冲区
}
}
snprintf可以将数据格式化的写道字符串中
从缓冲区读取
//parent process
void Reader(int Rfd)
{
char buffer[1024] = {0};
while (true)
{
buffer[0] = '\0'; //清空缓冲区,告诉读者,这是一个字符串
//从文件中读取字符串
ssize_t n = read(Rfd, buffer, sizeof(buffer)); //sizeof(buffer) != strlen(buffer)
if (n > 0) //n是读取的字节数
{
buffer[n] = 0; //字符串末尾加上'\0'
cout << "Received message: " << buffer << endl;
}
}
}
总结
核心逻辑就是父进程用pipe打开一个内存级别的文件两次
父进程创建子进程
子进程调用W函数,父进程调用R函数
W函数就是借助pipefd[1]向文件缓冲区写入(write)
R函数就是借助pipefd[0]从缓冲区读取(read)
父进程等待子进程
易错解析
每次打开一个文件都会产生一个file结构,同时获得一个file结构对应的fd。
读方式打开,获得读方式的fd
写方式打开,获得写方式的fd
一个进程可以用不同的方式打开一个文件多次。
我们不可以通过建立全局字符串的方式去进行通信,因为子进程在修改字符串时,会发生写时拷贝。
所有进行进程通信的时候,所占用的区域属于OS管理,而不是某个进程。
匿名管道的特性
1.只有亲戚之间可以通信
2.管道只能单向通信
3.父子进程协同、互斥(限定资源的抢占特性)、同步通信的特性
eg:子进程休眠10s才写入一次,那父进程也别着急读,会等待一下进程(并不会读取空的管道,因为没有打印Received message:,说明直接没有读)
4.管道面向字节流
不管你写的是什么,在r端认为都是一个个字节,只负责读。所谓的格式区分,不是r端该干的活,这种特性就是字节流。
5.管道基于文件,而文件被打开的生命周期基于进程,所以进程结束,管道关闭。
6.管道是有固定大小的,在不同的内核中,可能有差别
7.管道的原子性
原子性:小于pipe_buf,就是原子的。保证读写的连贯性 4kb
管道的四种情况
读写端正常,管道为空,读端阻塞
读写端正常,管道满了,写段阻塞
写端关闭,read会读取到EOF,返回0,不会阻塞
读端关闭,写端继续写入时,OS就要(通过信号)杀掉写进程
写端关闭特指的是:一定要现有写端被打开的现象,才能说是写端被关闭。如果写端都哦没有被打开,就不存在关闭一说
在命名管道(FIFO)的情况下,"写端关闭"这个说法确实指的是写端曾经被打开,并且随后被关闭。以下是详细解释:
打开写端:这意味着至少有一个进程通过 open 系统调用以写模式(O_WRONLY)打开了管道的写端,从而能够向管道写入数据。
关闭写端:这发生在进程完成写入操作后,通过 close 系统调用关闭了写端。关闭写端意味着该进程不再向管道写入数据,但如果有其他进程已经打开了写端,它们仍然可以写入。
写端未被打开:如果没有任何进程以写模式打开管道的写端,那么我们就不能说写端被关闭,因为关闭是一个状态改变,需要先有一个打开的状态。
因此,如果没有进程曾经打开过写端,那么说“写端被关闭”是不准确的。在这种情况下,更准确的说法是“写端尚未被打开”或“没有进程打开写端”。
当读端进程尝试从管道读取数据时,如果写端尚未被任何进程打开,那么读操作会阻塞,等待写端的打开和数据的写入。只有当至少一个进程打开了写端并写入数据后,读端进程的读操作才会解除阻塞并读取数据。如果所有写端都被关闭,并且没有数据留在管道中,那么读操作会返回0,表示到达了文件末尾。
应用场景
在bash中输入的|管道符号就是一种匿名管道。