【Linux的文件篇章 - 管道文件】

Linux学习笔记---013

  • Linux的管道文件
    • 1、进程间通信
      • 1.1、进程为什么要通信?
      • 1.2、进程如何通信?
      • 1.3、进程通信的方式?
    • 2、匿名管道
      • 2.1、理解一种现象
      • 2.2、基本概念和管道原理
    • 3、管道的使用
      • 3.1、代码样例
      • 3.2、如何使用管道通信呢?
      • 3.3、管道的4种情况
      • 3.4、管道的5种特征
      • 3.5、管道的应用场景
    • 4、命名管道
      • 4.1、原理
      • 4.2、创建命名管道函数的使用
    • 5、system V的共享内存
      • 5.1、原理
      • 5.2、代码理解
      • 5.3、共享内存的理解
      • 5.4、共享内存的相关接口
    • 6、消息队列
      • 6.1、基本概念
      • 6.2、涉及的常用接口
    • 7、信号量
      • 7.1、5个概念
      • 7.2、对于信号量的理论理解
      • 7.3、原子操作
      • 7.4、常用信号量的指令

Linux的管道文件

前言:
前篇开始进行了解学习Linux的磁盘文件等相关知识内容,接下来学习关于Linux的管道文件、共享内存、消息队列和信号量的基本知识,深入地了解这个强大的开源操作系统。
/知识点汇总/

1、进程间通信

1.1、进程为什么要通信?

进程间也是需要某种协同的,如何协同的前提条件就是通信。
数据是有类别的,通知就绪的,单纯的要传递的信息,以及控制信息。

事实:进程是具有独立性的。
进程 = 内核的数据结构 + 代码和数据

1.2、进程如何通信?

a、进程间通信,成本可能会稍微高一些。(因为进程是独立的)

比如:进程a把数据给进程b,进程具有独立性,所以数据无法直接传递的。(父子进程fork的方式,只是处于只读,传递信息和一直可以传递信息是有区别的,所以frok是处于可以传递信息,但不能一直传递,因为是基于写时拷贝的)

b、进程间通信的前提:先让不同的进程,看到同一份(操作系统的)资源(“一段内存”)

因为进程a和进程b,进程间具有独立性,相互之间的空间和数据等资源无法共享,就通过操纵系统实现,让它们能够在“一段内存中交换和访问数据”。

那么操作系统怎么知道什么时候创建共享区域呢?

1.一定是某一个进程先需要通信,让OS创建一个共享资源。
2.OS必须提供很多的系统调用。 – OS创建的共享资源不同,系统调用接口不同 ---- 进程间通信的方式就会存在不同。

1.3、进程通信的方式?

a、存在一定约定的标准(专利)
b、消息队列、共享内存、信号量

直接复用内核代码直接通信呢?
进程间独立,对于文件系统无关。引出管道

1.命名管道
2.匿名管道

2、匿名管道

2.1、理解一种现象

为什么父子进程会向同一个显示器终端打印数据。

因为父子进程中,子进程会继承父进程的文件描述符表,进而指向同一个显示器文件,用同一个进程inode,也就把数据写入同一个缓冲区里,所以系统刷新时,就刷新到同一个显示器中。

进程默认会打开三个标准输入/输出:0,1,2怎么做到的呢?

都属于bash的子进程,所以是bash打开了。
进程默认也就打开了,我们只要约定好即可。

close():为什么我们子进程主动close(0/1/2),不影响父进程继续使用显示器文件呢?

本质是由于,之前了解到的引用计数,通过引用计数能够知道有多少文件指针指向它,那么就通过引用计数的指针依次释放指定的次数。

2.2、基本概念和管道原理

那么在通过操作系统,基于文件系统上,在内存中建立的“一段共享内存”就称为管道资源。 – 管道文件

注意:

1.管道只允许单向通信 — 半双工通信
2.管道与文件的操作区别,就在于不用刷新到磁盘了。

既然父子进程会关闭不需要的fd,那么为什么在创建父子进程时,要默认打开呢?可以选择不关闭吗?

答:为了让子进程继承下去(父进程只有,那么子进程就只有读,父进程有读/写,那么子进程就继承读/写)。
可以不关闭,建议关闭,防止误读或误写,以及系统资源的浪费。

既然管道不用再刷新到磁盘中,那么需要重新设计通信接口吗?

答:创建管道的系统调用,底层实际就是open,只是不用了磁盘部分。
int pipe(int pipefd[2]);
不需要文件路径和文件名,其次也被称为匿名文件 – 匿名管道

管道只能实现单向通信。我实际就想要实现双向通信呢?

答:就创建两个管道。

为什么管道是单向通信的呢?

答:a.方便复用代码,减少开发成本。
b.数据易混淆,涉及数据的区分等复杂的操作,所以不采用双向,只需要满足传输数据。单向即可满足。

3、管道的使用

3.1、代码样例

测试代码: 子进程交给父进程的通信

//管道的使用

#include <iostream>
#include <unistd.h>
//c++版本的errno.h,和c++版本的string.h
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string>

using namespace std;

const int Size = 1024;

string getOtherMessage()
{
	static int cnt = 0;
	string messageid = to_string(cnt);
	cnt++;
	pid_t self_id getpid();
	string  stringpid = to_string(self_id);
	//拼接
	string message = "messageid: ";
	message += messageid;
	message += " my pid is : ";
	message += stringpid;

	return message;
}

//子进程写入
void SubProcessWrite(int wfd)
{
	string message = "father,I am your son process!";
	while (true)
	{
		string info = message + getOtherMessage();//拼接得到,子进程写入管道的信息
		write(wfd, info.c_str(), info.size());//写入管道时,用的是系统调用write,没有写入'\0',也没有必要不使用时一同写入‘\0’
		sleep(5);

		//情况2:管道满64kb,ubantu 20.02版本
		char c = 'A';
		write(wfd, &c, 1);
		cout << "pipesize" << ++pipesize << endl;
		break;
	}

	cout << "child quit ..." << endl;
}

//父进程读取
void FatherProcessRead(int rfd)
{
	char inbuffer[Size];//
	while (true)
	{
		//sleep(5);
		ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);//因为没有写入'\0',所以sizeof读取到时要减1
		if (n > 0)
		{
			inbuffer[n] = 0;//所以需要时,要手动添加'\0'
			cout << "father get message" << inbuffer << endl;
		}

		cout << "father get return val: " << n << endl;
	}
}

int main()
{
	//1.创建管道
	int pipefd[2];
	int n = pipe(pipefd);//pipe的参数属于输出型参数,rfd和wfd
	if (n != 0)
	{
		cerr << "errno:" << errno << ": " << "srrstring : " << strerror(errno) << endl;
		return 1;
	}

	//打印文件描述符,预测是3和4,因为文件描述符默认代开三个0,1,2.
	cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;
	sleep(1);
	//得到的是管道的读写端:
	//pipefd[0] --> 0 -->r
	//pipefd[1] --> 1 -->w

	//2.创建子进程
	//3.关闭不需要的文件描述符(以子进程写,父进程读为例)
	pid_t id = fork();
	if (id == 0)
	{
		cout << "子进程关闭不需要的fd,准备开始发消息" << endl;
		sleep(1);
		//子进程 -- write
		//....

		//关闭不需要的文件描述符
		close(pipefd[0]); // 关闭读
		//写入
		SubProcessWrite(pipefd[1]);
		close(pipefd[1]); // 写完后,关闭写
		exit(0);
	}

	cout << "父进程关闭不需要的fd,准备开始收消息" << endl;
	sleep(1);
	//父进程 -- read
	//....
	//关闭不需要的文件描述符
	close(pipefd[1]); // 关闭写
	//读取
	FatherProcessRead(pipefd[0]);
	close(pipefd[0]); // 读完后,关闭读

	//到此仍然没有进行通信,只是在建议一个共享的内存空间 --管道
	//即:让不同的进程看到同一块资源。
	//以上那么多操作,也就说明了进程间的通信是需要一定的成本的。(因为进程间具有独立性)

	//4.进程间通信
	//SubProcessWrite() 
	//FatherProcessRead()

	//5.防止僵尸进程
	pid_t rid = waitpid(id, nullptr, 0);
	if (rid > 0)
	{
		cout << "wait child process done" << endl;
	}

	return 0;
}

3.2、如何使用管道通信呢?

int pipe(int pipefd[2]); — 参数int pipefd[2],属于输出型参数,表示管道的输入或输出的端口
既然管道也是文件,那么文件的操作依然通用于管道文件。
read / write

根据之前的知识,知道的fork之后,子进程是拿到父进程的数据的,是属于通信吗?

严格意义上讲并不是属于通信,对于子进程来讲,它是只读的(无法修改,无法阻止接收通信,只能父进程交给子进程),完全是由父进程决定得到的资源,是单向的数据。
所以再结合写时拷贝,对方是看不见通信信息的。
所以简单的通过全局变量的缓冲区,使得双方获取对方数据是行不通的。

代码验证,测试代码:

#include <iostream>
#include <unistd.h>
//c++版本的errno.h,和c++版本的string.h
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string>

using namespace std;

const int Size = 1024;

string getOtherMessage()
{
	static int cnt = 0;
	string messageid = to_string(cnt);
	cnt++;
	pid_t self_id getpid();
	string  stringpid = to_string(self_id);
	//拼接
	string message = "messageid: ";
	message += messageid;
	message += " my pid is : ";
	message += stringpid;

	return message;
}

//子进程写入
void SubProcessWrite(int wfd)
{
	string message = "father,I am your son process!";
	char c = 'A';
	while (true)
	{
		//情况5:
		cerr << " ++++++++++++++++++++++ " << endl;
		string info = message + getOtherMessage();//拼接得到,子进程写入管道的信息
		write(wfd, info.c_str(), info.size());//写入管道时,用的是系统调用write,没有写入'\0',也没有必要不使用时一同写入‘\0’
		//sleep(5);
		cerr << info << endl;
		//情况2:管道满64kb,ubantu 20.02版本
		//	write(wfd, &c, 1);
		//	cout << "pipesize" << ++pipesize << "write charctor" << c << endl;
		//	c++;
		//	if (c == 'G') break;
		//	sleep(1);
	}

	cout << "child quit ..." << endl;
}

//父进程读取
void FatherProcessRead(int rfd)
{
	char inbuffer[Size];//
	while (true)
	{
		sleep(2);
		cout << " -------------- " << endl;
		ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);//因为没有写入'\0',所以sizeof读取到时要减1
		if (n > 0)
		{
			inbuffer[n] = 0;//所以需要时,要手动添加'\0'
			cout << "father get message" << inbuffer << endl;
		}
		else if (n == 0)
		{
			cout << "client quit,father get return vsl: " << n << " father quit too!" << endl;
			break;
		}
		else if (n < 0)
		{
			cerr << "read error" << endl;
			break;
		}
		//情况:5
		sleep(1);
		break;
	}
}

int main()
{
	//1.创建管道
	int pipefd[2];
	int n = pipe(pipefd);//pipe的参数属于输出型参数,rfd和wfd
	if (n != 0)
	{
		cerr << "errno:" << errno << ": " << "srrstring : " << strerror(errno) << endl;
		return 1;
	}

	//打印文件描述符,预测是3和4,因为文件描述符默认代开三个0,1,2.
	cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;
	sleep(1);
	//得到的是管道的读写端:
	//pipefd[0] --> 0 -->r
	//pipefd[1] --> 1 -->w

	//2.创建子进程
	//3.关闭不需要的文件描述符(以子进程写,父进程读为例)
	pid_t id = fork();
	if (id == 0)
	{
		cout << "子进程关闭不需要的fd,准备开始发消息" << endl;
		sleep(1);
		//子进程 -- write
		//....

		//关闭不需要的文件描述符
		close(pipefd[0]); // 关闭读
		//写入
		SubProcessWrite(pipefd[1]);
		close(pipefd[1]); // 写完后,关闭写
		exit(0);
	}

	cout << "父进程关闭不需要的fd,准备开始收消息" << endl;
	sleep(1);
	//父进程 -- read
	//....
	//关闭不需要的文件描述符
	close(pipefd[1]); // 关闭写
	//读取
	FatherProcessRead(pipefd[0]);
	cout << "5s, father close rfd" << endl;
	sleep(5);
	close(pipefd[0]); // 读完后,关闭读

	//到此仍然没有进行通信,只是在建议一个共享的内存空间 --管道
	//即:让不同的进程看到同一块资源。
	//以上那么多操作,也就说明了进程间的通信是需要一定的成本的。(因为进程间具有独立性)

	//4.进程间通信
	//SubProcessWrite() 
	//FatherProcessRead()
	
	//5.防止僵尸进程
	int status = 0;
	pid_t rid = waitpid(id, nullptr, 0);
	if (rid > 0)
	{
		cout << "wait child process done,exit sig: " << (status&0x7f) << endl;
		cout << "wait child process done,exit code(ign): " << ((status>>8)&0xFF) << endl;
	}

	return 0;
}

3.3、管道的4种情况

1.可能会存在被多个进程同时访问的情况(并发),数据都不一致问题。
2.如果管道内部是空的 && write fd 没有关闭,读取条件不具备,读进程会被阻塞 — wait — 读取条件具备再写入数据。
3.管道被写满了 && read fd 不读且没有关闭,管道被写满,写进程会被阻塞(管道被写满 – 写条件不具备) – wait — 写条件具备 --》读取数据,管道一直在读 && 写端关闭了wfd,读端read返回值读到了0,表示读到了文件结尾。
5.rfd直接关闭,写端wfd一直再进行写入?处于水管出口堵塞了,还一直灌水,属于无用功。对于操作系统不会做这种浪费时间浪费空间的事情,没有意义。操作系统会直接杀掉马,这种坏管道。
所以对于此类出异常的管道,操作系统会主动发送信号,kill SIGPIPE杀掉该管道。

3.4、管道的5种特征

1.匿名管道:只限于具有血缘关系的进程之间,进行通信,常用于父子进程之间的通信。(因为父子进程有一个“天生的”前提条件:都能看到同一份(操作系统的)资源(“一段内存”))
2.管道内部,自带进程之间同步的机制。(多执行流执行代码时,具有明显的顺序性)
3.管道文件的生命周期是随进程的。
4.管道文件在通信的时候,是面向字节流的(有些挑战)
面向字节流最典型的特点就是:
write的次数与读取的次数不是一一匹配的。
5.管道通信的模式,是一种特殊的半双工模式。

补充:PIPE_BUF

因为管道通信属于特殊的半双工,所以有关于管道大小的两点:
1.写入的字节大小小于PIPE_BUF的大小时,会被认为是原子的,也就是小于规定的范围的或者说属于一个单元的,即这种情况下是安全的,不会出现写到一半被读取走。
2.PIPE_BUF的大小通常是512byte,而Linux中是4096byte.

3.5、管道的应用场景

1.命令行中的|,就是匿名管道的应用。
2.进程池
比如提前创建fork一批子进程,有任务就通过每一个管道,对接每一个子进程;
从而父进程对接每一个管道的写端,每一个子进程对应与其对应的读端。
这种提前创建好进程的方式就是进程池,大大节约了成本,使其不用单独创建单独的进程了,直接通过各个管道派遣任务就行了。
并且管道里没有数据时,各个子进程(work进程)就处于阻塞等待,等待分配的任务;
所以父进程(master)向哪一个管道写入,就会唤醒哪一个进程来处理任务。(进程间+管道就处于的概念就是,进程的协同)
其中,父进程最好要将任务均衡的划分给每一个子进程,就称为负载均衡。

测试代码:

#include <iostream>
#include <sys/types.h>
#include <string>
#include <unistd.h>
#include <vector>
#include "Task.hpp"
#include <sys/wait.h>

using namespace std;

class Channel
{
public:
	Channel(int wfd, pid_t id, const string &name)
		:_wfd(wfd), _subprocessid(id),_name(name)
	{}
	int GetWfd()
	{
		return _wfd;
	}
	pid_t GetProcessId()
	{
		return _subprocessid;
	}
	string GetName()
	{
		return _name;
	}
	void CloseChannel()
	{
		close(_wfd);
	}
	void Wait()
	{
		pid_t rid = waitpid(_subprocessid,nullptr,0);
		if (rid > 0)
		{
			cout << "wait " << rid << " success" << endl;
		}
	}
	~Channel()
	{}
private:
	int _wfd;
	pid_t _subprocessid;
	string _name;
};

void work(int rfd)
{
	while (true)
	{
		int command = 0;
		int n = read(rfd, &command, sizeof(command));
		if (n == sizeof(int))
		{
			cout << "pid is: " << getpid() << " handler task" << endl;
			ExcuteTask(command);
		}
		else if (n == 0)
		{
			cout << "sub process: " << getpid() << " quit" << endl;
			break;
		}
	}
}


//创建信道和子进程
/**/
void test_pipepool(int argc, char* argv[])
{
	if (argc != 2)
	{
		cerr << "Usage: " << argv[0] << " processnum" << endl;
		return ;
	}
	int num = stoi(argv[1]);
	vector<Channel> channels;

	//创建信道和子进程
	for (int i = 0; i < num; i++)
	{
		//1.创建管道
		int pipefd[2] = { 0 };
		int n = pipe(pipefd);
		if (n < 0) exit(1);
		//3.创建子进程
		pid_t id = fork();
		if (id == 0)
		{
			//child --- read 处理任务
			close(pipefd[1]);//关闭写端
			work(pipefd[0]);
			close(pipefd[0]);//读完读端
			exit(0);
		}
		//3.构建一个channel1名称
		string channel_name = "Channel-" + to_string(i);
		//father --- write
		close(pipefd[0]);//关闭读端
		//a、子进程的Pid, b、父进程关心的管道的写端
		channels.push_back(Channel(pipefd[1], id, channel_name));
	}

	//for test
	for (auto& channel : channels)
	{
		cout << "==========================" << endl;
		cout << channel.GetName() << endl;
		cout << channel.GetProcessId() << endl;
		cout << channel.GetWfd() << endl;
	}
}

//优化
/**/
//形参类型和命名规范
//const   ---> 只读型参数
//const & --> 输出型参数
//& --->  输入输出型参数
//* ---> 输出型参数
void CreateChannelAndSub(int num, vector<Channel>* channels)
{
	//创建信道和子进程
	for (int i = 0; i < num; i++)
	{
		//1.创建管道
		int pipefd[2] = { 0 };
		int n = pipe(pipefd);
		if (n < 0) exit(1);
		//3.创建子进程
		pid_t id = fork();
		if (id == 0)
		{
			//child --- read 处理任务
			close(pipefd[1]);//关闭写端
			work(pipefd[0]);
			close(pipefd[0]);//读完读端
			exit(0);
		}
		//3.构建一个channel1名称
		string channel_name = "Channel-" + to_string(i);
		//father --- write
		close(pipefd[0]);//关闭读端
		//a、子进程的Pid, b、父进程关心的管道的写端
		channels->push_back(Channel(pipefd[1], id, channel_name));
	}

}

//轮询方案
int NextChannel(int channelnum)
{
	static int next = 0;
	int channel = next;
	next++;
	next %= channelnum;
	return channel;
}

void SendTaskCommand(Channel& channel, int taskcommand)
{
	write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}

//优化1
void test_pipepool2(int argc, char* argv[])
{
	if (argc != 2)
	{
		cerr << "Usage: " << argv[0] << " processnum" << endl;
		return ;
	}
	int num = stoi(argv[1]);
	LoadTask();//装载任务

	vector<Channel> channels;
	//1.创建信道和子进程
	CreateChannelAndSub(num, &channels);

	//2.通过channel控制子进程
	while (true)
	{
		sleep(1);
		//a、选择一个任务
		int taskcommand = SelectTask();
		//b、选择一个信道和进程
		int channel_index = NextChannel(channels.size());
		//c、发送任务
		SendTaskCommand(channels[channel_index], taskcommand);
		cout << "-----------------" << endl;
		cout << "taskcommand: " << taskcommand << " channel: " << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << endl;
	}

	//3.回收管道和子进程
}

//优化2
void ctrlProcessOnce(vector<Channel>& channels)
{
	sleep(1);
	//a、选择一个任务
	int taskcommand = SelectTask();
	//b、选择一个信道和进程
	int channel_index = NextChannel(channels.size());
	//c、发送任务
	SendTaskCommand(channels[channel_index], taskcommand);
	cout << "-----------------" << endl;
	cout << "taskcommand: " << taskcommand << " channel: " << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << endl;
}

void ctrlProcess(vector<Channel>& channels, int times = -1)
{
	if (times > 0)
	{
		while (times--)
		{
			ctrlProcessOnce(channels);
		}
	}
	else
	{
		while (true)
		{
			ctrlProcessOnce(channels);
		}
	}
}

void CleanUpChannel(vector<Channel>& channels)
{
	//关闭管道
	for (auto& channel : channels)
	{
		channel.CloseChannel();
	}
	//注意:防止僵尸进程
	//回收子进程
	for (auto& channel : channels)
	{
		channel.Wait();
	}
}

void test_pipepool3(int argc, char* argv[])
{
	if (argc != 2)
	{
		cerr << "Usage: " << argv[0] << " processnum" << endl;
		return;
	}
	int num = stoi(argv[1]);
	LoadTask();//装载任务

	vector<Channel> channels;
	//1.创建信道和子进程
	CreateChannelAndSub(num, &channels);

	//2.通过channel控制子进程
	ctrlProcess(channels);

	//3.回收管道和子进程
	//a、关闭所有的写端,返回值为0 --》子进程就自动退出,最后回收即可
	//b、回收子进程
	CleanUpChannel(channels);
}


//先描述,再组织
int main(int argc, char* argv[])
{
	//创建信道和子进程
	test_pipepool(argc, argv);
	//优化
	test_pipepool2(argc, argv);
	//优化
	test_pipepool3(argc, argv);
	return 0;
}

通过函数指针数组管理任务码,分配子进程完成任务功能.

可规定一个固定长度的4字节数组下标,写和读都以4字节为单位识别。 – 任务码

测试代码:

.hpp默认属于开源程序,因为声明和定义是写在一起的

//.hpp默认属于开源程序,因为声明和定义是写在一起的
#pragma once

#include <iostream>
#include <ctime>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;

#define TaskNum 3

typedef void (*task_t)();//task_t 函数指针

void Print()
{
	cout << "T am a print task" << endl;
}

void Douwnload()
{
	cout << "T am a download task" << endl;
}

void Flush()
{
	cout << "T am a flush task" << endl;
}

task_t tasks[TaskNum];

void LoadTask()
{
	srand(time(nullptr) ^ getpid());
	tasks[0] = Print;
	tasks[1] = Douwnload;
	tasks[2] = Flush;
}

void ExcuteTask(int number)
{
	if (number < 0 || number > 2)
		return;
	tasks[number]();
}

int SelectTask()
{
	return rand() % TaskNum;
}

//回调函数 --- work本质也是任务
void work()
{
	while (true)
	{
		int command = 0;
		int n = read(0, &command, sizeof(command));//重定向到标准输入去读取了
		if (n == sizeof(int))
		{
			cout << "pid is: " << getpid() << " handler task" << endl;
			ExcuteTask(command);
		}
		else if (n == 0)
		{
			cout << "sub process: " << getpid() << " quit" << endl;
			break;
		}
	}
}

4、命名管道

4.1、原理

两个进程毫无关系怎么建立通信呢?

通过命名管道,一方写另一方读

那么怎么保证两个不相关的进程,能够准确打开同一个文件呢?

答:每一个文件都有一个唯一路径(具有唯一性)

mkfifo命令

用于创建一个命名管道 mkfifo myfifo得到一个p管道文件

建立一次进程通信:

echo “hello named pipe” >myfifo
cat myfifo

循环通信:

while :;do sleep 1;echo “hello named pipe” >>myfifo; done
cat < myfifo

4.2、创建命名管道函数的使用

创建管道文件
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname.mode_t mode);
删除指定的管道文件
#include <unistd.h>
int unlink(const char* pathname);

测试代码:
client.cc

#include "namedPipe.hpp"

// write
int main()
{
    NamePiped fifo(comm_path, User);
    if (fifo.OpenForWrite())
    {
        std::cout << "client open namd pipe done" << std::endl;
        while (true)
        {
            std::cout << "Please Enter> ";
            std::string message;
            std::getline(std::cin, message);
            fifo.WriteNamedPipe(message);
        }
    }

    return 0;
}

server.cc

#include "namedPipe.hpp"

// server read: 管理命名管道的整个生命周期
int main()
{
    NamePiped fifo(comm_path, Creater);
    // 对于读端而言,如果我们打开文件,但是写还没来,我会阻塞在open调用中,直到对方打开
    // 进程同步
    if (fifo.OpenForRead())
    {
        std::cout << "server open named pipe done" << std::endl;

        sleep(3);
        while (true)
        {
            std::string message;
            int n = fifo.ReadNamedPipe(&message);
            if (n > 0)
            {
                std::cout << "Client Say> " << message << std::endl;
            }
            else if (n == 0)
            {
                std::cout << "Client quit, Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout << "fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }

    return 0;
}

namedPipe.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string comm_path = "./myfifo";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096

class NamePiped
{
private:
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

public:
    NamePiped(const std::string& path, int who)
        : _fifo_path(path), _id(who), _fd(DefaultFd)
    {
        if (_id == Creater)
        {
            int res = mkfifo(_fifo_path.c_str(), 0666);//创建命名管道,并配置权限
            if (res != 0)
            {
                perror("mkfifo");
            }
            std::cout << "creater create named pipe" << std::endl;
        }
    }
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    bool OpenForWrite()
    {
        return OpenNamedPipe(Write);
    }
    // const &: const std::string &XXX
    // *      : std::string * //输出型
    // &      : std::string & //输入输出型
    int ReadNamedPipe(std::string* out)
    {
        char buffer[BaseSize];
        int n = read(_fd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }
    int WriteNamedPipe(const std::string& in)
    {
        return write(_fd, in.c_str(), in.size());
    }
    ~NamePiped()
    {
        if (_id == Creater)
        {
            int res = unlink(_fifo_path.c_str());
            if (res != 0)
            {
                perror("unlink");
            }
            std::cout << "creater free named pipe" << std::endl;
        }
        if (_fd != DefaultFd) close(_fd);
    }

private:
    const std::string _fifo_path;
    int _id;
    int _fd;
};

5、system V的共享内存

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

5.1、原理

1.所有说到的操作都是由OS完成的
2.OS提供上面1,2步骤的系统调用,供用户进程A,B来进行调用 – 系统调用
3.AB,CD,EF…共享内存在系统中可以同时存在多份,每份可不同个数,不同对的进程同时进行通信。
4.OS注定了要对共享内存进行管理,–》先描述再组织 --》共享内存,不是简单的一段内存空间,也要有描述并管理共享内存的数据结构匹配的算法。
5.共享内存 = 内存空间(放数据) + 共享内存的属性

5.2、代码理解

测试代码:
shm目录 – 共享内存
Shm.hpp

#ifndef __SHM_HPP__
#define __SHM_HPP__

#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

const int gCreater = 1;
const int gUser = 2;
const std::string gpathname = "/home/whb/code/111/code/lesson22/4.shm";
const int gproj_id = 0x66;
const int gShmSize = 4097; // 4096*n

class Shm
{
private:
    key_t GetCommKey()
    {
        key_t k = ftok(_pathname.c_str(), _proj_id);
        if (k < 0)
        {
            perror("ftok");
        }
        return k;
    }
    int GetShmHelper(key_t key, int size, int flag)
    {
        int shmid = shmget(key, size, flag);
        if (shmid < 0)
        {
            perror("shmget");
        }

        return shmid;
    }
    std::string RoleToString(int who)
    {
        if (who == gCreater)
            return "Creater";
        else if (who == gUser)
            return "gUser";
        else
            return "None";
    }
    void* AttachShm()//挂接
    {
        if (_addrshm != nullptr)
            DetachShm(_addrshm);
        void* shmaddr = shmat(_shmid, nullptr, 0);
        if (shmaddr == nullptr)
        {
            perror("shmat");
        }
        std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;
        return shmaddr;
    }
    void DetachShm(void* shmaddr)
    {
        if (shmaddr == nullptr)
            return;
        shmdt(shmaddr);
        std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;
    }

public:
    Shm(const std::string& pathname, int proj_id, int who)
        : _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr)
    {
        _key = GetCommKey();
        if (_who == gCreater)
            GetShmUseCreate();
        else if (_who == gUser)
            GetShmForUse();
        _addrshm = AttachShm();

        std::cout << "shmid: " << _shmid << std::endl;
        std::cout << "_key: " << ToHex(_key) << std::endl;
    }
    ~Shm()
    {
        if (_who == gCreater)
        {
            int res = shmctl(_shmid, IPC_RMID, nullptr);
        }
        std::cout << "shm remove done..." << std::endl;
    }

    std::string ToHex(key_t key)
    {
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "0x%x", key);
        return buffer;
    }
    bool GetShmUseCreate()
    {
        if (_who == gCreater)
        {
            _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
            if (_shmid >= 0)
                return true;
            std::cout << "shm create done..." << std::endl;
        }
        return false;
    }
    bool GetShmForUse()
    {
        if (_who == gUser)
        {
            _shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);
            if (_shmid >= 0)
                return true;
            std::cout << "shm get done..." << std::endl;
        }
        return false;
    }
    void Zero()
    {
        if (_addrshm)
        {
            memset(_addrshm, 0, gShmSize);
        }
    }

    void* Addr()
    {
        return _addrshm;
    }

    void DebugShm()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        if (n < 0) return;
        std::cout << "ds.shm_perm.__key : " << ToHex(ds.shm_perm.__key) << std::endl;
        std::cout << "ds.shm_nattch: " << ds.shm_nattch << std::endl;
    }

private:
    key_t _key;
    int _shmid;

    std::string _pathname;
    int _proj_id;

    int _who;
    void* _addrshm;
};

#endif

shmnamedPipe.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string comm_path = "./myfifo";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096

class NamePiped
{
private:
    bool OpenNamedPipe(int mode)
    {
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

public:
    NamePiped(const std::string& path, int who)
        : _fifo_path(path), _id(who), _fd(DefaultFd)
    {
        if (_id == Creater)
        {
            int res = mkfifo(_fifo_path.c_str(), 0666);
            if (res != 0)
            {
                perror("mkfifo");
            }
            std::cout << "creater create named pipe" << std::endl;
        }
    }
    bool OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    bool OpenForWrite()
    {
        return OpenNamedPipe(Write);
    }
    // const &: const std::string &XXX
    // *      : std::string *
    // &      : std::string & 
    int ReadNamedPipe(std::string* out)
    {
        char buffer[BaseSize];
        int n = read(_fd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }
    int WriteNamedPipe(const std::string& in)
    {
        return write(_fd, in.c_str(), in.size());
    }
    ~NamePiped()
    {
        if (_id == Creater)
        {
            int res = unlink(_fifo_path.c_str());
            if (res != 0)
            {
                perror("unlink");
            }
            std::cout << "creater free named pipe" << std::endl;
        }
        if (_fd != DefaultFd) close(_fd);
    }

private:
    const std::string _fifo_path;
    int _id;
    int _fd;
};

shmclient.cc

#include "Shm.hpp"
#include "shmnamedPipe.hpp"

int main()
{
    // 1. 创建共享内存

    Shm shm(gpathname, gproj_id, gUser);
    shm.Zero();
    char* shmaddr = (char*)shm.Addr();
    sleep(3);

    // 2. 打开管道
    NamePiped fifo(comm_path, User);
    fifo.OpenForWrite();

    // 当成string
    char ch = 'A';
    while (ch <= 'Z')
    {
        shmaddr[ch - 'A'] = ch;

        std::string temp = "wakeup";
        std::cout << "add " << ch << " into Shm, " << "wakeup reader" << std::endl;
        fifo.WriteNamedPipe(temp);
        sleep(2);
        ch++;
    }
    return 0;
}

shmserver.cc

include "Shm.hpp"
#include "shmnamedPipe.hpp"

int main()
{
    // 1. 创建共享内存
    Shm shm(gpathname, gproj_id, gCreater);
    char* shmaddr = (char*)shm.Addr();

    shm.DebugShm();

    // // 2. 创建管道
    // NamePiped fifo(comm_path, Creater);
    // fifo.OpenForRead();

    // while(true)
    // {
    //     // std::string temp;
    //     // fifo.ReadNamedPipe(&temp);

    //     std::cout << "shm memory content: " << shmaddr << std::endl;
    // }

    sleep(5);

    return 0;
}

5.3、共享内存的理解

申请一个systeam V版本的动态内存

int shmget(key_t key,size_t size,int shmflg);
参数:
1.size_t size — 创建的共享内存大小
2.int shmflg — 标志位(常用IPC_CREAT 和 IPC_EXEL) – 可以位图的形式传参
IPC_CREAT:如果你要创建的共享内存不存在,就创建,如果存在,获取该共享内存并返回。(总能获取到)
IPC_EXEL:单独使用没有意义,只有和IPC_CREAT组合使用才有意义。
IPC_CREAT | IPC_EXEL:如果你要创建的共享内存不存在,就创建,否则出错返回。(获取的是全新的shm)
3.key_t key — 由用户自定义的key值,设置为唯一标识符,只要具备唯一性即可
a、key_t key是什么?是由用户自定义的key值,用于设置为唯一标识符
b、为什么?因为让不同的进程看到同一个共享内存
c、怎么办?利用ftok随机设置key值,便于用户使用
返回值:
返回唯一的标识符
#include <sys/typse.h>
#include <sys/ipc.h>
key_t ftok(const char* pathname, int proj_id);

我们怎么确定,OS内的共享内存是否存在了呢?

答:struct Shm中会有一个标识共享内存的唯一性标识符

能让OS自动生成,标识符呢?

答:不能, 共享内存,不随着进程的结束而自动释放,需要手动释放。(或系统调用释放)

共享内存的生命周期: 共享内存生命周期随内核,文件生命周期随进程

查看共享内存的信息:

ipcs -m
删除/释放指定的共享内存:
ipcrm -m shmid(返回给用户的标识符)

补充:IPC的知识

key VS shmid
key:属于用户形成的,属于内核使用的一个特定字段,具有唯一性,用户不能使用key来对shm进行管理,内核进行区分shm的唯一性(struct file*)
shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的id值(fd).

5.4、共享内存的相关接口

a、shmctl

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmod_ds *buf);
功能:
共享内存的控制,增删改查…
参数:
int shmid:内核给用户返回的id值
int cmd:对共享内存要执行的操作(常用IPC_RMID,删除/释放当前共享内存)
struct shmod_ds *buf:共享内存结构体的属性成员

b、shmat

#include <sys/typse.h>
#include <sys/shm.h>
void* shmat(int shmid, const void* shmaddr, int shmflg);
功能:将对应的地址空间挂接到共享内存中。
返回值:
地址空间中,共享内存的起始地址

c、shmdt

int shmdt(const void* shmaddr);
功能:取消挂接关联

6、消息队列

6.1、基本概念

一个进程,向另一个进程发送有类型的数据块的方式。结合之前的理解,msg_queue自带属性信息。
消息队列的生命周期也是随内核的,不随进程。

6.2、涉及的常用接口

消息队列常用接口
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
key_t ftok(const char* pathname,int proj_id);
int msgget(key_t key, int msgflg);
int msgctl(int msgid,int cmd,struct msgid_ds* buf);
int msgsnd(int msgid, const void* msgp, size_t msgsz, int msgflg);
int mshrcv(int msgid, void* msgp,size_t msgsz, long msgtyp,int msgflg);

7、信号量

7.1、5个概念

1.多个执行流(进程)都能看到的一份资源,称为共享资源
2.被保护起来的资源 – 称为临界资源 – 同步和互斥
3.互斥:任何时刻只能有一个进程访问共享资源。
4.资源 — 要被程序员访问 – 资源被访问,简单理解就是就是通过代码访问,代码 = 访问共享资源的代码(临界区) + 不访问共享资源的代码(非临界区)
5.所谓的对共享资源进行保护 – 临界资源 – 本质是对访问共享资源的代码进行代码。

7.2、对于信号量的理论理解

临界区 <=加锁/解锁=> 非临界区

1.用于保护临界资源,本质是一个计数器。

信号量的计数数量,标志对共享资源的预定机制。 担心超出资源量的个数,管理属性资源总数,限制资源不被多余预定。
类比:电影院系统
电影院:共享资源(临界资源)
买票:申请信号量
票数:信号量的初始值

申请信号量的本质:就是对公共资源的一种预定机制

申请信号量
访问共享资源
释放信号量

对共享资源整体的使用,其实不就是资源只有一个么?

1/0,二元信号量,互斥

既然信号量是一个计数器,可以使用一个全局的变量(如:gcongt)来充当对共享资源的保护吗?

答:不能,
1.因为全局变量不能被所有进程能够看到。
2.并且gcount++,不是原子的。

7.3、原子操作

所以IPC信号量:

1.与共享内存一样,使不同的进程之间都能看到同一个信号量(计数器),控制不同进程的同步或互斥
2.意味着信号量本身也属于共享资源
3.既然本身也属于临界资源,却要保护别的临界资源安全,前提是不是需要自己肯定是安全的呢?—提出原子操作
4.允许用户一次性申请多个信号量集 – 用数组来维护的。

步骤:

1.申请信号量
2.访问公共资源(共享内存)
3.释放信号量

对信号量(计数器)的操作,就被设置为原子操作:

P和V操作 – 安全的 – 原子性
– —》本身是安全的 P
++ —》本身是安全的 V

7.4、常用信号量的指令

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key,int nsems, int semflg);
int semctl(int semid,int semnum,int cmd,…);
int semop(int semid,struct sembuf* sops,size_t nsops);
查看信号量指令:
incs -s
删除指定信号量:
ipcrn -s semid

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/602543.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

4000定制网站,因为没有案例,客户走了

接到一个要做企业站点的客户&#xff0c;属于定制开发&#xff0c;预算4000看起来是不是还行的一个订单&#xff1f; 接单第一步&#xff1a;筛客户 从客户询盘的那一刻开始就要围绕核心要素&#xff1a;预算和工期&#xff0c;凡是不符合预期的一律放掉就好了&#xff0c;没必…

基于springboot的校园食堂订餐系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…

基于参数化建模的3D产品组态实现

我们最近为荷兰设计师家具制造商 KILO 发布了基于网络的 3D 配置器的第一个生产版本。我们使用了 Salsita 3D 配置器&#xff0c;这是一个内部 SDK&#xff0c;使新的 3D 配置器的实施变得轻而易举。虽然它给我们带来了巨大帮助&#xff0c;但我们仍然面临一些有趣的挑战。 NSD…

新一代智慧音视频平台,企业必备新基建

随着5G、云计算、实时音视频、多模态、大模型、数字人等前沿技术的发展&#xff0c;企业与客户的交互方式正加速趋于移动化、视频化。 国家有关部门也相继出台系列政策法规&#xff0c;确保线上业务安全合规&#xff0c;以保障消费者权益。如&#xff0c;针对保险、银行、证券…

思维导图怎么画?一文掌握绘制技巧

思维导图怎么画&#xff1f;你是不是还在为不知道怎么绘制思维导图而困惑&#xff1f;别担心&#xff0c;看完这篇文章就可以掌握绘制思维导图的基础步骤了。一起来看看吧&#xff01; 一、思维导图的基本结构 思维导图通常由中心节点、分支节点和子节点组成。中心节点是思维导…

【基于 PyTorch 的 Python深度学习】5 机器学习基础(1)

前言 文章性质&#xff1a;学习笔记 &#x1f4d6; 学习资料&#xff1a;吴茂贵《 Python 深度学习基于 PyTorch ( 第 2 版 ) 》【ISBN】978-7-111-71880-2 主要内容&#xff1a;根据学习资料撰写的学习笔记&#xff0c;该篇主要介绍了机器学习的基本任务、机器学习的一般流程&…

活动预约小程序源码系统 自定义预约表单+收费项目 带完整的安装代码包以及系统部署教程

数字化时代的快速发展&#xff0c;活动预约管理已经成为许多企业和个人不可或缺的一部分。为满足这一需求&#xff0c;我们特别开发了一款活动预约小程序源码系统&#xff0c;该系统不仅具备自定义预约表单的功能&#xff0c;还支持收费项目&#xff0c;旨在为用户提供更加便捷…

Garden Planner for Mac/win:打造您专属的绿意空间

随着城市化进程的加速&#xff0c;绿色空间对于现代人来说愈发珍贵。为满足人们对美好生活的追求&#xff0c;我们特推出了一款功能强大的园林绿化设计软件——Garden Planner for Mac/win。这款软件将帮助您轻松规划和设计您的花园、菜园或庭院&#xff0c;让绿意成为您生活的…

刷代码随想录有感(59):二叉搜索树的最近公共祖先

题干&#xff1a; 代码&#xff1a; class Solution {递归实现 public:TreeNode* traversal(TreeNode* root, TreeNode* p, TreeNode* q){if(root NULL)return NULL;if(root->val > p->val && root->val > q->val){TreeNode* left traversal(root…

高速开箱机价格与性能解析:如何挑选适合您的开箱解决方案?

随着电商和物流行业的迅猛发展&#xff0c;高效、自动化的包装设备成为了提升工作效率、减少人工成本的必备利器。高速开箱机作为其中的重要一环&#xff0c;其性能与价格成为了许多企业和个人关注的焦点。星派将深入探讨高速开箱机的价格与性能之间的关系&#xff0c;帮助您在…

视频封面一键提取:从指定时长中轻松获取您想要的帧图片

在数字媒体时代&#xff0c;视频已成为人们获取信息、娱乐和沟通的主要形式之一。而一个好的视频封面&#xff0c;往往能够吸引观众的眼球&#xff0c;增加视频的点击率和观看量。然而&#xff0c;对于很多视频创作者和编辑者来说&#xff0c;如何从视频中快速、准确地提取出合…

代码随想录算法训练营第二十天:二叉树成长

代码随想录算法训练营第二十天&#xff1a;二叉树成长 110.平衡二叉树 力扣题目链接(opens new window) 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡二叉树定义为&#xff1a;一个二叉树每个节点 的左右两个子树的高度差的绝…

俄罗斯副总理暗示欧佩克+或增加原油产量,亚洲早盘油价小幅下跌

在俄罗斯副总理亚历山大诺瓦克暗示欧佩克可能采取行动增加原油产量后&#xff0c;亚洲早盘的油价出现小幅下跌。这一消息引起了市场对原油供给增加的担忧&#xff0c;导致油价走低。 City Index和FOREX.com的市场分析师Fawad Razaqzada表示&#xff0c;虽然原油价格在技术上尚…

C语言例题37、输入三组数字,按照从小到大的顺序排列输出

#include<stdio.h>int main() {int num[3];printf("请输入3组数字&#xff1a;");for (int i 0; i < 3; i)scanf("%d", &num[i]);for (int i 0; i < 2; i) { //每次选出最小值放在数组前面for (int j i 1; j < 3; j) {if (num[j] …

day_21

很简单&#xff0c;两个指针&#xff0c;指向1和n依次输出&#xff0c;然后自加自减即可。这样可以保证任意非两边的数同时大于或小于左邻和右邻的数。 看代码 #include <iostream> using namespace std;int main() {int n;cin >> n;int i 1, j n;while(i <…

带你快速掌握Spring Task

Spring Task ⭐Spring Task 是Spirng框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑 &#x1f4cc;一款定时任务框架 应用场景 信用卡信息银行贷款信息火车票信息 只要是需要定时处理的场景都可以使用Spring Task 只要有定时&#xff0c;就会有…

QT7_视频知识点笔记_2_对话框,布局,按钮,控件(查看帮助文档找功能函数)

第二天&#xff1a; 对话框&#xff0c;布局&#xff0c;按钮 QMainWindow&#xff1a;菜单下拉框添加之后可通过ui->actionXXX&#xff08;自定义的选项名&#xff09;访问&#xff0c;用信号triggered发出信号&#xff0c;槽函数可以使用lambda表达式进行 //菜单栏&am…

一文搞懂MySQL索引的数据结构

一、引言 在数据库管理系统中&#xff0c;索引是提高查询性能的关键所在。对于MySQL这类关系型数据库来说&#xff0c;索引更是其优化查询不可或缺的一部分。索引能够大大加快数据的检索速度&#xff0c;减少数据库的I/O操作&#xff0c;提高数据库的整体性能。本文将从索引的…

U盘管控软件,禁止员工用U盘拷贝机密数据,防止信息通过U盘泄露

随着信息技术的不断发展&#xff0c;U盘等便携式存储设备已成为我们日常工作中不可或缺的工具。然而&#xff0c;随着U盘的普及&#xff0c;企业面临的信息泄露风险也在不断增加。为了确保企业的信息安全&#xff0c;许多企业开始采用U盘管控软件&#xff0c;禁止员工使用U盘拷…

Gen-2颠覆AI生成视频!一句话秒出4K高清大片,网友:彻底改变游戏规则

这&#xff0c;绝对称得上是生成式AI进程中的里程碑。 就在深夜&#xff0c;Runway家标志性的AI视频生成工具Gen-2&#xff0c;迎来了“iPhone时刻”般的史诗级更新—— 依旧是简单一句话输入&#xff0c;不过这一次&#xff0c;视频效果一口气拉到了4K超逼真的高度&#xff…
最新文章