最新资讯

  • Linux的进程间通信

Linux的进程间通信

2025-05-16 08:37:38 1 阅读

目录

进程间通信介绍

进程间通信的概念

主要IPC方式

进程间通信的目的

进程间通信的本质

进程间通信的分类

管道

什么是管道

匿名管道

匿名管道的原理

pipe函数

匿名管道使用步骤

管道读写规则

匿名管道的特点

管道的四种特殊情况

 管道的大小

命名管道

命名管道的原理

使用命令创建命名管道

在程序里创建一个命名管道

命名管道的打开规则

使用案例:

如何保证两个不相关的进程打开同一个命名管道通信

 重点!不刷盘

具有inode

命名管道的应用

数据在内核缓冲区上,不刷盘

 命名管道与匿名管道的区别

 命令行当中的管道

system V进程间通信

System V共享内存

共享内存的基本原理

共享内存数据结构

共享内存的建立与释放

共享内存的创建

共享内存的释放

最后简单给出一个使用案例

共享内存的关联

共享内存的去关联

共享内存与管道进行对比 

System V消息队列

消息队列的基本原理

消息队列数据结构

消息队列的创建

消息队列的释放

从消息队列获取数据

System V信号量

信号量相关概念

信号量数据结构

信号量凭什么是进程通信一种

本质

 二元信号量

原子?

信号量的设计

system V IPC联系


进程间通信介绍

进程间通信的概念

进程间通信(Inter-Process Communication, IPC)是指在不同进程之间传递数据或信号的机制。由于进程拥有独立的地址空间,无法直接访问彼此的内存,因此需要特定的IPC方法来实现数据交换和协调。

主要IPC方式

  1. 管道(Pipe)

    • 匿名管道:用于父子进程或兄弟进程间的单向通信。

    • 命名管道(FIFO):允许无亲缘关系的进程通过文件系统进行通信。

  2. 消息队列(Message Queue)

    • 进程通过消息队列发送和接收消息,消息按类型存储,支持异步通信。

  3. 共享内存(Shared Memory)

    • 多个进程共享同一块内存区域,速度最快,但需要同步机制(如信号量)来避免冲突。

  4. 信号量(Semaphore)

    • 用于进程同步,控制对共享资源的访问,防止竞态条件。

  5. 信号(Signal)

    • 用于通知进程发生了特定事件,如中断或错误。

  6. 套接字(Socket)

    • 适用于网络通信,也可用于同一台机器上的进程间通信。

  7. 文件(File)

    • 通过读写文件进行数据交换,适用于持久化存储。

进程间通信的目的

进程间通信(IPC)的主要目的是使不同进程能够共享数据、协调任务和同步操作

  1. 数据共享:多个进程可能需要访问或修改同一数据,IPC提供了一种安全的方式来实现数据共享。

  2. 任务协调:多个进程可能需要协同完成一项任务,IPC帮助它们协调工作。

  3. 资源管理:多个进程可能需要共享有限的系统资源(如CPU、内存、文件等),IPC帮助它们合理分配和调度资源。

  4. 进程同步:多个进程的执行顺序可能需要协调,IPC提供同步机制(如信号量、锁等)来确保进程按正确顺序执行。

  5. 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
  6. 事件通知:一个进程可能需要通知其他进程发生了某些事件,IPC提供了一种机制来发送和接收通知。

进程间通信的本质

让不同的进程看到同一份”资源“

 资源是特定的内存空间。其中资源一般是由操作系统提供的。

我们进程访问这个空间,进行通信,本质就是访问操作系统。


由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的吗,是由成本的。

如果说各个进程间如果想要进行通信,一定要向操作系统申请第三方资源。使得进程间可以通过这个资源进行写入与读取,进而实现进程之间的通信。

因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。但由于操作系统不同和板块提供的第三方资源不同,所以通信又可以分很多的类。

进程间通信的分类

管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

管道

什么是管道

管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个“管道”。

在本质上管道就是基于文件级别的通信方式。

管道又分为匿名管道与命名管道。

管道的命名就是由我们日常生活中的管道命名而来,比如水龙头管道,其有一个特点就是水只能单向流动,所以管道有一个特点就是单向通信。

实际上,Linux就为类Unix操作系统,

例如:统计我们当前使用云服务器上的登录用户个数。

其中,根据我们以前学的,但我们执行一个指令时,其实就是创建一个who命令和wc命令都是两个程序, 当两个程序运行起来后就会变为进程,其中who进程通过标准输出将数据打到“管道”当中,然后wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。

解释:who是用于查看当前云服务器的登录用户(一行显示一个用户) ,wc -l用于统计当前的行数。

匿名管道

匿名管道的原理

匿名管道(Anonymous Pipe)是一种单向的进程间通信(IPC)机制,主要用于具有亲缘关系的进程(如父子进程或兄弟进程)之间的数据传输。它是操作系统提供的一种简单且高效的通信方式。

 进程间通信的本质就是,让不同的进程看到同一份资源,其中匿名管道就是为了实现父子进程之间的通信,其原理就是让父子进程先看到同一份被打开的文件资源,又因为管道具有单向通信的特点,所以父子进程就要对该文件进行写入或是读取操作,进而实现父子进程间通信。

注意:

  • 一开始我们也说了要实现通信要借助第三方资源,其中这个第三方资源就是由操作系统提供的。那么这里父子进程看到的同一份文件资源就是由操作系统给予的,维护的。 这意味着父子进程看到的文件资源是相同的。
  • 但是写时拷贝是一种优化技术,通常用于内存管理。所以对于文件缓冲区,写时拷贝不适用。父子进程对文件的写入操作会直接修改共享的缓冲区,不会创建副本(不会在创建一个文件缓冲区,单独为子进程使用,原本的文件缓冲区变为单独为父进程使用)。
  • 管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。
pipe函数
#include 
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:

数组元素含义
pipefd[0]管道读端的文件描述符
pipefd[1]管道写端的文件描述符

 对于返回值:pipe函数调用成功时返回0,调用失败时返回-1。

匿名管道使用步骤

在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:

1、父进程调用pipe函数创建管道。

2、父进程调用fork函数创建子进程。 3、父进程关闭写端,子进程关闭读端。

注意:

  1.  因为管道只能够进行单向通信,因此当父进程创建完子进程后,需要确定父子进程谁读谁写,然后关闭对应的读写端。
  2. 从管道写端写入的数据会被存储在内核缓冲,如果缓冲区未满,写入操作会立即完成;如果缓冲区已满,写入操作会阻塞,直到有空间可用。
  3. 当进程从管道的读端读取数据时,数据会从内核缓冲区中取出。如果缓冲区为空,读取操作会阻塞,直到有数据可读。
  4. 在管道(Pipe)中,文件缓冲区是不发挥作用的。因为数据不需要持久化。
  5. 在规定上没有明确规定父子进程谁做读端好,谁做写端好。这一点设定可以根据自己要求。

同样,我们可以站在文件描述符的角度再来看看这三个步骤:

1、父进程调用pipe函数创建管道。 

2、父进程调用fork函数创建子进程。 

3、父进程关闭写端,子进程关闭读端。

 例如,在以下代码当中,子进程向匿名管道当中写入10次数据,父进程从匿名管道当中将数据读出。

//child->write, father->read
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int fd[2]; // 0->读  1->写
    if(pipe(fd) < 0) // 使用pipe函数创建匿名管道
    {
        perror("pipe");
		return 1;
    }
    pid_t id = fork(); //使用fork创建子进程
    if(id == 0)
    {
        // child
        close(fd[0]); // 子进程关闭读端
        //子进程向管道写入数据
        const char* msg = "hello father, I am child...";
        int cnt = 10;
        while(cnt--)
        {
            write(fd[1], msg, strlen(msg));
            sleep(1);
        }
        close(fd[1]);
        exit(0);
    }
    // father
    close(fd[1]); //父进程关闭写端
    char buff[64];
    while (1)
    {
		ssize_t s = read(fd[0], buff, sizeof(buff));
		if (s > 0)
        {
			buff[s] = '';
			printf("child send to father:%s
", buff);
		}
		else if (s == 0)
        {
			printf("read file end
");
			break;
		}
		else
        {
			printf("read error
");
			break;
		}
	}
    close(fd[0]);
    waitpid(id, NULL, 0);
    return 0;
}

运行效果如下:

管道读写规则

pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:

int pipe2(int pipefd[2], int flags);

与pipe不同的是多了一个参数flags。该参数用于设置选项。

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将不再保证写入的原子性。

匿名管道的特点

下面我有的使用匿名管道有的使用的管道。

如果用管道,那么同样命名管道也具有该特点。

如果用的是匿名管道,那么仅匿名管道有该特点。

1、只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个匿名管道由一个进程创建,然后该进程调用fork,次后父、子进程之间就可以应用该匿名管道。

这一点没有需要特别解释的,但是要注意的是如果看到有人说匿名管道是用于父子进程之间,那么你要知道这句话是错误的,匿名管道是只能用于具有共同祖先的进程(具有亲缘关系的进程)之间。并不是仅仅在父子之间。但同样别人说父子进程使用匿名管道进行通信你也要知道别人说的是正确的,只是不准确而已。

2、管道提供流式服务。

对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:

  • 流式服务: 数据没有明确的分割,不分一定的报文段。
  • 数据报服务: 数据有明确的分割,拿数据按报文段拿。

 3、一般而言,进程退出,匿名管道释放,所以匿名管道的生命周期随进程。

匿名管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说匿名管道的生命周期随进程。

4、一般而言,内核会对管道操作进行同步与互斥。 

 临界资源是指在并发环境中,一次只能被一个进程或线程访问的资源。而管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。

临界资源是需要被保护的,若我们不进行任何保护,多个进程同时访问临界资源可能导致数据不一致或系统错误。比如说进程A向其写入了hello linux,但是进程B也向其写入hello world。但当进程A刚写入hello,进程B就向其写入hello world那么就会导致错误。

为了避免这些问题,内核会对管道操作进行同步与互斥:

  • 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
  • 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作,并且这种顺序在正常情况下是不变的。

也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。

5、管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。

这一点也不难理解,因为管道只允许单向通信。如果二者要进行双方通信时,就需要建立起两个管道。

扩展:

在数据通信中,数据在线路上的传送方式可以分为以下三种:

  • 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  • 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  • 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。
管道的四种特殊情况

在使用管道时,可能出现以下四种特殊情况:

  1. 如果管道的写端正常但不写入数据,而读端一直在读取数据,最终管道会变空。此时,读端进程会被阻塞,直到写端写入新数据后,读端进程才会被唤醒。
  2. 如果管道的读端正常但不读取数据,而写端一直在写入数据,最终管道会变满。此时,写端进程会被放入等待队列,进入睡眠状态。当读端读取数据后,内核会唤醒等待队列中的写端进程。
  3. 如果管道的写端关闭,而读端一直在读取数据,最终管道会变空。此时,读端进程读取会返回0,表示EOF(文件结束符)。表示没有更多数据可读。读端进程不会被阻塞,而是会正常结束读取操作。
  4. 如果管道的读端关闭,而写端一直在写入数据,最终管道会变满。此时,写端进程会收到一个SIGPIPE信号,如果写端进程没有处理该信号,进程会终止,当然通常下写端进程终止。如果写端进程忽略或捕获了SIGPIPE信号,写操作会返回-1,并设置errno为EPIPE。

其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。这样的设定就很好的保护了管道文件的数据安全。

第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时操作系统就会认为,读端进程也就没必要浪费资源继续阻塞等待。可以执行该进程的其他逻辑了,但一般来说还是会被结束。

第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。

我们可以通过以下代码看看情况四中,子进程退出时究竟是收到了什么信号。

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
	int fd[2] = { 0 };
	if (pipe(fd) < 0) //使用pipe创建匿名管道
    { 
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); //使用fork创建子进程
	if (id == 0)
    {
		//child
		close(fd[0]); //子进程关闭读端
		//子进程向管道写入数据
		const char* msg = "hello father, I am child...";
		int count = 10;
		while (count--)
        {
			write(fd[1], msg, strlen(msg));
			sleep(1);
		}
		close(fd[1]); //子进程写入完毕,关闭文件
		exit(0);
	}
	//father
	close(fd[1]); //父进程关闭写端
	close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
	int status = 0;
	waitpid(id, &status, 0);
	printf("child get signal:%d
", status & 0x7F); //打印子进程收到的信号
    // 0x7f->1111111 低七位
	return 0;
}

运行结果显示,子进程退出时收到的是13号信号。

通过kill -l命令可以查看13对应的具体信号。

由此可知,当发生情况四时,操作系统向子进程发送的是SIGPIPE信号将子进程终止的。


 管道的大小

管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?

方法一:借助man手册

指令:man 7 pipe 

根据man手册,看其官方的介绍得知,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。

然后我们可以使用uname -r命令,查看自己使用的Linux版本。

可以看到我的时2.6.11往后的。因此我的管道的最大容量是65536字节。 

方法二:使用ulimit命令

其次,我们还可以使用ulimit -a命令,查看当前资源限制的设定。 

根据显示,管道的最大容量是512512 字节的块(512字节的块是单位,实际就为512字节):512 × 512 = 262,144字节。

这个值比65536大。其实这个大小,是用户层面的限制,用于控制单个进程可以使用的管道缓冲区大小。

而65536是默认容量:这是内核层面的限制,表示管道的实际最大容量。

方法三:自行测试

代码如下

#include 
#include 
#include 
#include 
int main()
{
	int fd[2] = { 0 };
	if (pipe(fd) < 0)
    { //使用pipe创建匿名管道
		perror("pipe");
		return 1;
	}
	pid_t id = fork(); // 使用fork创建子进程
	if (id == 0)
    {
		//child 
		close(fd[0]);// 子进程关闭读端
		char c = 'a';
		int count = 0;
		// 子进程一直进行写入,一次写入一个字节
		while (1)
        {
			write(fd[1], &c, 1);
			count++;
			printf("%d
", count); //打印当前写入的字节数
		}
		close(fd[1]);
		exit(0);
	}
	// father
	close(fd[1]); //父进程关闭写端

	// 父进程不进行读取

	waitpid(id, NULL, 0);
	close(fd[0]);
	return 0;
}

 可以看到,在读端进程不进行读取的情况下,写端进程最多写65536字节的数据就被阻塞了,也就是说,我当前Linux版本中管道的最大容量是65536字节。

命名管道

命名管道的原理

匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。其中命名管道是一种特殊类型的文件。

注意:

  1. 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
  2. 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。

管道的应用的一个限制就是只能在具有共同祖先的进程间通信?

首先明确的说,这句话是错误的。应该改为匿名管道管道的应用的一个限制就是只能在具有共同祖先的进程间通信。因为管道包括匿名管道和命名管道。也只有匿名管道会限制在有共同祖先的进程间通信。所以看到这句话的时候要知道是错误的!!!

使用命令创建命名管道

命名管道可以在命令行上创建,命令行方法是使用下面这个命令:

mkfifo filename

可以看到,创建出来的文件的类型是p,代表该文件是命名管道文件。

当然我的是Ubuntu,其中后面文件名还带了一个 | 也在代表着其为命名管道文件。 

现在我们简单的设计一个使用场景,使用命名管道进行两个不相关的进程间通信。 我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。

对于进程A的指令:

while :; do echo "hello linux"; sleep 1; done > fifo

对于进程B的指令:

cat fifo

 现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。

之前我们在匿名管道中说过,当管道的读端进程退出后, 写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。(后面会解释为什么是命令行解释器bash执行的我们写的shell脚本)

在程序里创建一个命名管道

我们不仅可以在命令行上创建命名管道,我们还可以在程序中调用mkfifo函数接口。mkfifo函数原型如下:

#include 
#include 
int mkfifo(const char *pathname, mode_t mode);

mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。

注意:

一般来说我们都是使用第二种方式,因为打个比方,如果我们传一个文件名,我们只需要这样,

这会在当前工作目录下创建一个名为 my_fifo 的命名管道文件。

mkfifo("my_fifo", 0666);

 但我们要使用第一种方法,我们就需要这样使用该函数

mkfifo("/tmp/my_fifo", 0666);

这会在 /tmp 目录下创建一个名为 my_fifo 的命名管道文件。而且,如果你只传递了一个目录路径(例如 /tmp/),而没有指定文件名,则会导致错误。除此之外我们还要传完整的路径,可以穿绝对路径,也可以传相对路径。

mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。 

这个就不解释了,不清楚的可以看一下文件权限。

写文章-CSDN创作中心

mkfifo函数的返回值。

  • 命名管道创建成功,返回0。
  • 命名管道创建失败,返回-1。

 创建命名管道示例:

#include 
#include 
#include 

#define FILE_NAME "myfifo"

int main()
{
    umask(0); // 将文件默认掩码设置为0
    if(mkfifo(FILE_NAME, 0666) < 0) // 调用mkfifo创建命名管道
    {
        // 失败
        perror("mkfifo");
        return 1;
    }
    printf("mkfifo success
");
    return 0;
}

 运行代码后,命名管道myfifo就在当前路径下被创建了。

命名管道的打开规则

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

  •   O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
  • O_NONBLOCK enable:立刻返回成功。

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

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。、
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

调用函数:unlink;这个函数比较简单,可以自己查看man手册

man 2 unlink

调用失败返回-1。成功返回一个非负整数。 

使用案例:

案例场景:创建出来的命名管道可以供两个进程通信进程A 向管道当中写 “i am process A”进程B 从管道当中读 并且打印到标准输出

对于进程A:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define FILE_NAME "myfifo"

int main()
{
    umask(0); // 将文件默认掩码设置为0
    if(mkfifo(FILE_NAME, 0666) < 0) // 调用mkfifo创建命名管道
    {
        // 失败
        perror("mkfifo");
        return 1;
    }
    // 创建成功
    printf("pipe created sucessfully
");
    // A
    int fd = open(FILE_NAME, O_WRONLY);
    if (fd == -1)
    {
        perror("open");
        return 2;
    }
    // 写
    const char* msg = "i am process A";
    write(fd, msg, strlen(msg));
    close(fd);
    return 0;
}

对于进程B:

#include 
#include 
#include 
#include 
#include 

#define FILE_NAME "myfifo"

int main() 
{
    int fd;
    char buff[1024];
    
    // 打开命名管道以读取
    fd = open(FILE_NAME, O_RDONLY);
    if (fd == -1) 
    {
        perror("open");
        return 1;
    }
    
    // 读取消息并打印
    read(fd, buff, sizeof(buff));
    printf("process B read: %s
", buff);
    close(fd);
    
    // 删除命名管道
    unlink(FILE_NAME);
    
    return 0;
}

如何保证两个不相关的进程打开同一个命名管道通信

对于两个毫不相关的进程,如果说它俩在同一目录下,还好处理,但是如果二者路径上都没有关系,那么我们如何保证二者打开同一个命名管道进行通信,怎么保证二者正常的通信呢?

其实这一点我们可以用路径+文件名来确保唯一性。这样就可以让不同或者说毫不相关的进程之间通信。

当然路径可以使用绝对路径,也可以使用相对路径。一般来说相对路径使用的多。

 重点!不刷盘

这一点与匿名管道一样,因为命名管道也是单行通信。也不会刷盘,也就跟我前面的解释一下。

具有该性质。

具有inode

命名管道是内存级文件,它具有inode。

可以使用下面的指令查看:

ls -l -i

可以看到确实具有inode 

扩展:(十分重要)

如果说,如果我创建了一个命名管道,如果在通信的过程中我删除了管道文件,哪有问题吗?

实际上通信上是没有问题的,管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除后,之前已经打开管道的进程依然可以通信。

已经打开的管道:

  • 已打开的文件描述符仍然有效:如果进程已经打开了管道(无论是读端还是写端),删除管道文件不会影响这些已经打开的文件描述符。

  • 通信可以继续:通过这些已打开的文件描述符,进程仍然可以正常读写数据。

未打开的管道:

  • 新进程无法打开该管道:删除后,其他尝试打开该管道的进程会失败(返回ENOENT错误)。

  • 但不会影响现有连接:已经建立连接的进程可以继续通信。

命名管道的应用

这段使用内容比较多。后面会单开一篇文章展示使用。后面会补上文章链接。

数据在内核缓冲区上,不刷盘

其命名管道的数据是存储在内核缓冲区上。 同样匿名管道也是如此。不会刷入磁盘上

 命名管道与匿名管道的区别

特性匿名管道(Anonymous Pipe)命名管道(Named Pipe,FIFO)
创建方式通过 pipe() 系统调用创建通过 mkfifo() 系统调用创建,也可以用命令行
文件类型FIFO 文件无文件类型
文件系统路径有(例如 /tmp/my_fifo
进程关系限制只能用于具有共同祖先的进程可用于任意进程
生命周期随进程结束而销毁持久化,直到显式删除
使用场景父子进程或兄弟进程间通信任意进程间通信
写端通过 write() 向匿名管道写入数据通过 write() 向命名管道写入数据
读端

读端进程通过 read() 从匿名管道读取数据

读端进程通过 read() 从命名管道读取数据

有读端没写端操作系统会关闭读端写端进程会被阻塞
有写端没读端操作系统会关闭写端读端进程会被阻塞

 命令行当中的管道

现创建test.txt文件,文件当中的内容如下:

我们可以利用管道(“|”)同时使用cat命令和grep命令,进而实现文本过滤。

cat data.txt | grep linux

那么此时就会有问题了。此时命令行当中的管道(“|”)到底是匿名管道还是命名管道呢? 

 由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。

我们现在使用sleep创建三个进程。

指令:

sleep 100 | sleep 200 | sleep 300

使用指令ps查看PPID其相关关系。 

ps axj | head -1 && ps axj | grep sleep | grep -v grep

下面通过管道(“|”)连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。 

 而它们的父进程实际上就是命令行解释器,这里为bash

也就是说,由管道(“|”)连接起来的各个进程是有亲缘关系的,它们之间互为兄弟进程。

现在我们已经知道了,若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。

system V进程间通信

管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。

system V IPC提供的通信方式有以下三种:

  • system V共享内存:允许两个或多个进程共享一段内存区域,是进程间通信中最快的方式,因为数据不需要在进程间复制。

  • system V消息队列:允许一个或多个进程写入或读取消息,可以看作是一个消息链表,每个消息都有一个类型和一个优先级

  • system V信号量:用于同步进程,控制多个进程对共享资源的访问。System V信号量分为二进制信号量和计数信号量。

其中system V共享内system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然它并不能直接让进程间通信,但还是属于通信范畴,服务于进程间通信。

他们三者的区别就类似于:system V共享内system V消息队列是用于通信的手机,而system V信号量是用于下棋比赛时用的棋钟,用于保证两个棋手之间的同步与互斥。

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 */
};

当我们申请了一块共享内存后,为了要实现进程间通信的进程能够看见同一个共享内存,因此还要设定一个唯一标识符,而这个标识符就在共享内存创建的时候就有了,其就是key值,这个key值用于标识系统中共享内存的唯一性,并且是用户提供的,用于标识共享内存段的唯一性。

所以其key也是要被操作系统维护再共享内存的数据结构内,其可以看到上面共享内存数据结构的第一个成员是shm_permshm_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;
};
共享内存的建立与释放

共享内存的建立,从我们用户来看,我们只需要执行下面几步就可以

  1. 生成 key 值。

  2. 调用 shmget 系统调用创建共享内存段。
  3. 调用 shmat 将共享内存段附加到自己的虚拟地址空间。

从用户的角度来看确实很简单,但是从操作系统来说就比较麻烦了,简单来说分两个过程

  1. 在物理内存当中申请共享内存空间。
  2. 将申请到的共享内存挂接到地址空间,即建立映射关系。

同样在用户来看,我们释放只需要

  1. 调用 shmdt 将其从进程的地址空间中分离。
  2. 调用 shmctl 删除共享内存段。

从操作系统来说,共享内存的释放大致分两个过程:

  1. 将共享内存与地址空间去关联,即取消映射关系。
  2. 释放共享内存空间,即将物理内存归还给系统。

下面我们分别介绍建立与释放,我们先说明用户需要做什么,需要注意什么,然后再同操作系统的角度来解释细节。

共享内存的创建

创建共享内存我们需要用shmget函数:

头文件:
#include 
#include 

功能: 用来创建共享内存(于物理内存上创建)

原型
int shmget(key_t key, size_t size, int shmflg);

参数
    key:这个共享内存段名字
    size:共享内存大小
    shmflg:由九个权限标志构成,他们的用法和创建文件时使用的mode模式标志是一样的。
返回值:成功返回一个非负整数,技改共享内存段的标识码;失败返回 -1

注意: 我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。

传入shmget函数的第一个参数key,实际上需要我们使用ftok函数进行获取。

  • 但是key虽然是一个 key_t  类型,但是它其实为一个数字,但它是几并不重要,就算最后设计为100012,也没关系,关键在于它的设计要能再内核中具有唯一性,能够让不同的进程进行唯一性标识。
  • 同样我们第一个进程可以通过key的创建共享内存,第二个及以后的进程只需要拿着同一个key就可以和第一个进程看到同一个共享内存。
  • 对于一个已经创建好的共享内存,key就在共享内存的描述对象中。
  • 所以第一次创建共享内存时,就需要一个key值,调用ftok函数来创建。

ftok函数的函数原型如下:

头文件
#include 
#include 

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值,然后才能找到同一个共享资源。

注意:第二个参数一般建立设置为4096的整数倍。比如说如果传的4097,但实际上为4096*2 

传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:

组合方式作用
IPC_CREAT如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄
IPC_CREAT | IPC_EXCL如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回

 换句话说:

  • 使用组合IPC_CREAT,一定会获得一个共享内存的句柄,但无法确认该共享内存是否是新建的共享内存。
  • 使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存的句柄,并且该共享内存一定是新建的共享内存。
  • IPC_EXCL不单独使用!!!

现在在用户层面我们已经了解了共享内存的创建,也大致知道怎么创建了,那么我们从用户角度来谈一下细节。

  1. 用户调用 shmget,触发系统调用进入内核。

  2. 内核检查参数合法性。

  3. 内核在全局共享内存表中查找是否已存在对应的共享内存段。

  4. 如果不存在且允许创建,则:

    • 分配共享内存段描述符。

    • 分配物理内存页。

    • 初始化共享内存段并添加到全局表。

  5. 返回共享内存标识符(shmid)给用户空间。

至此我们就可以使用ftok和shmget函数创建一块共享内存了,创建后我们可以将共享内存的key值和句柄进行打印,以便观察,代码如下:

#include 
#include 
#include 
#include 
#include 
#include 

#define PATHNAME "/home/haha" // 路径名

#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
", key); //打印key值
	printf("shm: %d
", shm); //打印句柄
    return 0;
}

该代码编写完毕运行后,我们可以看到输出的key值和句柄值: 

Linux当中,我们可以使用ipcs命令查看有关进程间通信设施的信息。 

图中有三部分打印,分别是默认列出消息队列、共享内存以及信号量相关的信息。如果我们想看单独一行的信息,可以加上选项

  • -q:列出消息队列相关信息。
  • -m:列出共享内存相关信息。
  • -s:列出信号量相关信息。

 例如,携带-m选项查看共享内存相关信息:

此时我们通过ipc查看,我们确实正确使用了ftok与shmget函数创建了共享内存。

 ipcs命令输出的每列信息的含义如下:

标题含义
key系统区别各个共享内存的唯一标识
shmid共享内存的用户层id(句柄)
owner共享内存的拥有者
perms共享内存的权限
bytes共享内存的大小
nattch关联共享内存的进程数
status共享内存的状态

注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,是操作系统内部使用的,用于唯一标识共享内存段,二者关系就类似与key是名字(但这个名字特殊,具有唯一性),shmid是句柄。 

共享内存的释放

通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。

这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。

此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。

使用命令释放共享内存资源

我们可以使用ipcrm -m shmid命令释放指定id的共享内存资源。

ipcrm -m shmid

注意:

指定删除时使用的是共享内存的用户层id,即列表当中的shmid。

其在删除时并不会删除已存在的key值,但它不再与任何共享内存段关联。但会通过shmid找到共享内存段表,查找对应的共享内存段描述符,会释放物理内存,删除共享内存段描述符,更新内核数据结构。

除此之外共享内存段占用的物理内存页也会被释放,共享内存的数据结构也会被删除。

需要注意的是以上是建立在引用计数为 0 的情况下。

调用释放共享内存资源 

 控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:

头文件
#include 
#include 

功能:用于控制共享内存

原型
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
    shmid:由shmget返回的共享内存标识码
    cmd:将要采取的动作(有三个可取值)
    buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0,失败返回-1

其中,作为shmctl函数的第二个参数传入的常用的选项有以下三个:

选项作用
IPC_STAT获取共享内存的当前关联值,此时参数buf作为输出型参数
IPC_SET在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值
IPC_RMID删除共享内存段

同样调用shmctl与命令行的ipcrm -m shmid在操作系统的角度做着同样的操作。 

例如,对上面的代码进行修改,当共享内存被创建,两秒后程序自动移除共享内存,再过两秒程序就会自动退出。

#include 
#include 
#include 
#include 
#include 
#include 

#define PATHNAME "/home/haha" // 路径名

#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
", key); //打印key值
	printf("shm: %d
", shm); //打印句柄

    sleep(2);
    shmctl(shm, IPC_RMID, NULL); //释放共享内存
    sleep(2);
    return 0;
}

我们可以调用监控脚本查看一下

while :; do ipcs -m;echo "###################################";sleep 1;done

通过监控脚本可以确定共享内存确实创建并且成功释放了。

最后简单给出一个使用案例

细节就不处理了。

要求:使用代码创建一个共享内存, 支持两个进程进行通信进程A 向共享内存当中写 “i am process A”进程B 从共享内存当中读出内容,并且打印到标准输出。

对于进程A代码

#include 
#include 
#include 
#include 

const int size = 4096; 
const char* pathname = "/home/haha";
const int proj_id = 0x6666;
key_t GetKey()
{
    key_t k = ftok(pathname, proj_id);//返回一个指向 C 风格字符串的指针(即以 '' 结尾的字符数组)
    return k;
}

int main()
{
    key_t key = GetKey();
    int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
    char* shm = (char*)shmat(shmid, NULL, 0);

    strcpy(shm, "i am process A");
    printf("process A send: %s
", shm);

    while(1);

    // 分离共享内存
    shmdt(shm);

    // 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    
    return 0;
}

对于进程B

#include 
#include 
#include 
#include 
const int size = 4096; 
const char* pathname = "/home/haha";
const int proj_id = 0x6666;
key_t GetKey()
{
    key_t k = ftok(pathname, proj_id);//返回一个指向 C 风格字符串的指针(即以 '' 结尾的字符数组)
    return k;
}
int main() 
{
    key_t key = GetKey();
    int shmid = shmget(key, size, 0666);
    char* shm = shmat(shmid, NULL, 0);

    printf("Process B read: %s
", shm);

    // 分离共享内存
    shmdt(shm);
    
    return 0;
}

 运行效果:

共享内存的关联

将共享内存连接到进程地址空间我们需要用shmat函数:

功能:将共享内存段连接到进程地址空间
原型
    void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
    shmid:共享内存标识
    shmaddr:指定连接的地址   通常设置为null,让操作系统连接一个合适的地址
    shmflg:他的三个可能取值是SHM_RND和SHM_RDONLY和0
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1

其中,作为shmat函数的第三个参数传入的常用的选项:

选项作用
SHM_RDONLY关联共享内存后只进行读取操作
SHM_RND若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)
0默认为读写权限
  • shmaddr为NULL,核心自动选择一个地址
  • shmaddr不为NULL且shmflg无SHM_RND标识,则以shmaddr为连接地址 
  • shmaddr不为NULL且shmflg设置了SHM_RND标识,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
  • shmflg = SHM_RDONKY,表示连接操作用来只读共享内存

这时我们可以尝试使用shmat函数对共享内存进行关联。

#include 
#include 
#include 
#include 
#include 
#include 

#define PATHNAME "/home/haha" // 路径名

#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
", key); //打印key值
	printf("shm: %d
", shm); //打印句柄

    printf("attach begin!
");
    char* mem = shmat(shm, NULL, 0); // 关联共享内存
    if (mem == (void*)-1)
    {
		perror("shmat");
		return 1;
	}
    printf("attach end!
");
    sleep(2);
    
    shmctl(shm, IPC_RMID, NULL); //释放共享内存
    sleep(2);
    return 0;
}

代码运行后发现关联失败,主要原因是我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存。我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。

int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存

此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限。

共享内存的去关联

取消共享内存与进程地址空间之间的关联我们需要用shmdt函数:

功能:将共享内存段与当前进程脱离
原型
    int shmdt(const void *shmaddr);
参数
    shmaddr:由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存

现在我们就能够取消共享内存与进程之间的关联了

#include 
#include 
#include 
#include 
#include 
#include 

#define PATHNAME "/home/haha" // 路径名

#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 | 0666); //创建权限为0666的共享内存
    if (shm < 0)
    {
		perror("shmget");
		return 2;
	}
	printf("key: %x
", key); //打印key值
	printf("shm: %d
", shm); //打印句柄

    printf("attach begin!
");
    char* mem = shmat(shm, NULL, 0); // 关联共享内存
    if (mem == (void*)-1)
    {
		perror("shmat");
		return 1;
	}
    printf("attach end!
");
    sleep(2);


    printf("detach begin!
");
    sleep(2);
    shmdt(mem); //共享内存去关联
    printf("detach end!
");
    sleep(2);

    shmctl(shm, IPC_RMID, NULL); //释放共享内存
    sleep(2);
    return 0;
}

运行程序,通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联。

共享内存与管道进行对比 

当我们共享内存创建好后,就不需要再调用系统接口来进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。

使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:

  1. 服务端将信息从输入文件复制到服务端的临时缓冲区中。
  2. 将服务端临时缓冲区的信息复制到管道中。
  3. 客户端将信息从管道复制到客户端的缓冲区中。
  4. 将客户端临时缓冲区的信息复制到输出文件中。

而共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:

  1. 从输入文件到共享内存。
  2. 从共享内存到输出文件。

所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。

但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。但是我们可以通过特殊的设定,从而有同步互斥的效果。

System V消息队列

消息队列的基本原理

消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。

其中消息队列当中的某一个数据块是由谁发送给谁的,取决于数据块的类型。

总结一下:

  1. 消息队列提供了一个从一个进程向另一个进程发送数据块的方法。
  2. 每个数据块都被认为是有一个类型的,接收者进程接收的数据块可以有不同的类型值。
  3. 和共享内存一样,消息队列的资源也必须自行删除,否则不会自动清除,因为system V IPC资源的生命周期是随内核的。

虽然这一部分内容为了解内容,但还是简单的介绍一下。

消息队列数据结构

当然,系统当中也可能会存在大量的消息队列,系统一定也要为消息队列维护相关的内核数据结构。

 消息队列的数据结构如下:

struct msqid_ds {
	struct ipc_perm msg_perm;
	struct msg *msg_first;      /* first message on queue,unused  */
	struct msg *msg_last;       /* last message in queue,unused */
	__kernel_time_t msg_stime;  /* last msgsnd time */
	__kernel_time_t msg_rtime;  /* last msgrcv time */
	__kernel_time_t msg_ctime;  /* last change time */
	unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes; /* ditto */
	unsigned short msg_cbytes;  /* current number of bytes on queue */
	unsigned short msg_qnum;    /* number of messages in queue */
	unsigned short msg_qbytes;  /* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;   /* last receive pid */
};

可以看到消息队列数据结构的第一个成员是msg_perm,它和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;
};
消息队列的创建

创建消息队列我们需要用msgget函数,msgget函数的函数原型如下:

int msgget(key_t key, int msgflg);

说明一下:

  1. 创建消息队列也需要使用ftok函数生成一个key值,这个key值作为msgget函数的第一个参数。
  2. msgget函数的第二个参数,与创建共享内存时使用的shmget函数的第三个参数相同。
  3. 消息队列创建成功时,msgget函数返回的一个有效的消息队列标识符(用户层标识符)。
消息队列的释放

释放消息队列我们需要用msgctl函数,msgctl函数的函数原型如下:

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

说明一下:
msgctl函数的参数与释放共享内存时使用的shmctl函数的三个参数相同,只不过msgctl函数的第三个参数传入的是消息队列的相关数据结构。

向消息队列发送数据
向消息队列发送数据我们需要用msgsnd函数,msgsnd函数的函数原型如下:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msgsnd函数的参数说明:

  • 第一个参数msqid,表示消息队列的用户级标识符。
  • 第二个参数msgp,表示待发送的数据块。
  • 第三个参数msgsz,表示所发送数据块的大小
  • 第四个参数msgflg,表示发送数据块的方式,一般默认为0即可。

msgsnd函数的返回值说明:

  • msgsnd调用成功,返回0。
  • msgsnd调用失败,返回-1。

其中msgsnd函数的第二个参数必须为以下结构:

struct msgbuf{
	long mtype;       /* message type, must be > 0 */
	char mtext[1];    /* message data */
};

注意: 该结构当中的第二个成员mtext即为待发送的信息,当我们定义该结构时,mtext的大小可以自己指定。

从消息队列获取数据

从消息队列获取数据我们需要用msgrcv函数,msgrcv函数的函数原型如下:

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgrcv函数的参数说明:

  • 第一个参数msqid,表示消息队列的用户级标识符。
  • 第二个参数msgp,表示获取到的数据块,是一个输出型参数。
  • 第三个参数msgsz,表示要获取数据块的大小
  • 第四个参数msgtyp,表示要接收数据块的类型。

msgrcv函数的返回值说明:

  • msgsnd调用成功,返回实际获取到mtext数组中的字节数。
  • msgsnd调用失败,返回-1。

我们可以通过指令查看我们创建的消息队列。

指令:

ipcs -q

下面就给一个简易的使用代码

#include 
#include 
#include 
#include   // POSIX 消息队列头文件
#include    // 定义 O_* 常量
#include  // 定义 mode 常量

#define QUEUE_NAME "/my_message_queue"  // 消息队列名称
#define MAX_MSG_SIZE 1024               // 消息的最大大小
#define MSG_BUFFER_SIZE (MAX_MSG_SIZE + 10) // 缓冲区大小

int main() {
    mqd_t mq;  // 消息队列描述符
    struct mq_attr attr;
    char buffer[MSG_BUFFER_SIZE];
    int msg_priority = 0;

    // 设置消息队列属性
    attr.mq_flags = 0;          // 阻塞标志
    attr.mq_maxmsg = 10;        // 队列中最大消息数
    attr.mq_msgsize = MAX_MSG_SIZE; // 每条消息的最大大小
    attr.mq_curmsgs = 0;        // 当前队列中的消息数

    // 创建消息队列
    mq = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0666, &attr);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        exit(EXIT_FAILURE);
    }
    printf("Message queue created: %s
", QUEUE_NAME);

    // 发送消息到队列
    // 调用msgsnd函数
    // .......


    // 从队列接收消息
    // 调用msgrcv函数
    // ......

    // 关闭消息队列
    mq_close(mq);

    // 删除消息队列
    mq_unlink(QUEUE_NAME);
    printf("Message queue deleted: %s
", QUEUE_NAME);

    return 0;
}

System V信号量

信号量相关概念
  • 由于进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系叫做进程互斥。
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
  • 在进程中涉及到临界资源的程序段叫临界区。
  • 在特性方面:IPC资源必须删除,否则不会自动删除,因为system V IPC的生命周期随内核。
信号量数据结构

在系统当中也为信号量维护了相关的内核数据结构。

信号量的数据结构如下:

struct semid_ds {
	struct ipc_perm sem_perm;       /* permissions .. see ipc.h */
	__kernel_time_t sem_otime;      /* last semop time */
	__kernel_time_t sem_ctime;      /* last change time */
	struct sem  *sem_base;      /* ptr to first semaphore in array */
	struct sem_queue *sem_pending;      /* pending operations to be processed */
	struct sem_queue **sem_pending_last;    /* last pending operation */
	struct sem_undo *undo;          /* undo requests on this array */
	unsigned short  sem_nsems;      /* no. of semaphores in array */
};

信号量数据结构的第一个成员也是ipc_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;
};

然而上面的数据结构,一般来说是加深理解,但始终他为了解知识,那么下面我们深入了解一下其本质

信号量凭什么是进程通信一种

信号量虽然不像前两个system V通信可以实用性的通信,但它还是作为其一,这是因为如下:

  1. 通信不仅仅是通信数据,互相协同也是也是通信。
  2. 协同本质也是通信,信号量做到的就是首先被所有的进程看到!!!
本质

信号量本质上是一个受保护的计数器,其核心功能是:

  • 记录可用资源数量(当信号量 > 0时)

  • 同步多线程/进程对共享资源的访问(当信号量 ≤ 0时,阻塞等待)

光看着句话还是不明所以然。下面我们就给一个情景模拟一下,就明白了。

就比方所我们在网上订票,电影院的老板在此次的电影放映中仅仅放出了100张票,作为顾客的我们,上网订票成功,那么总票数就会减减,这就相当于我们申请成功,计数器减减。但是我们申请成功了,但是我们并没有真正的要到了资源或者来说我们现在并没有看。所以在订票成功只是对这个电影进行了预订机制。票数的计数器保证了进入电影院的人数要保证小于等于100。所以对于每一个人,想要看这场电影,就需要申请成功,保证买到票,使得票的计数器减减。

在上面的这个过程中,我们网上买票成功就类似于

  1. 权限获取阶段:当线程成功申请信号量时(计数器值 > 0),表示该线程获得了访问共享资源的准入资格,此时信号量计数器会减少1。这类似于网上订票成功(获得观影资格),但此时还未实际使用资源(尚未入场观影)。

  2. 控制机制特性:信号量本质上是一种资源访问的准入控制系统,它通过计数器限制可同时访问资源的执行流数量,但并不直接管理资源的具体使用过程。

  3. 访问流程规范:任何执行流必须严格遵守"先申请信号量,后访问资源"的流程。就像观众必须先成功购票才能入场一样,执行流必须确保信号量申请成功后才能操作共享资源。

下面给出图形补充描述

 二元信号量

首先解释一下什么是二元信号量。

  • 定义:信号量计数器最大值被限制为1(只有0和1两种状态)。

  • 行为

    • 1:表示资源空闲,允许一个线程获取

    • 0:表示资源被占用,其他线程必须等待

其本质就是为一把锁。

因为它的行为和互斥锁(Mutex)几乎一致

操作二元信号量互斥锁(Mutex)
初始化初始值=1初始状态=未锁定
加锁sem_wait()(P操作)pthread_mutex_lock()
解锁sem_post()(V操作)pthread_mutex_unlock()
阻塞行为值为0时新线程阻塞已锁定时新线程阻塞

其就是信号量的一种特殊形式,这里简单提一下,了解便可。

原子?

对于一些操作,我们一般来说原子的是更为安全的,但这里的原子又是什么意思呢?

在物理,化学上规定,原子不可分,所以简单来说就是原子性操作就是不可再分的,也就是一步走完的。

就比方说下面的一个简单代码

int cnt = 10;
cnt--;

 其中cnt--,这一步在汇编语言中其实要分为很多步的,

  1. 他是先从内存中获取cnt变量的内容然后用cpu寄存器存起来。
  2. 然后再在cpu中进行--操作。
  3. 然后再将计算的结果写回cnt变量的存储位置。

虽然说在汇编语言上仅仅三步,但实际上并非如此,他还要在内核态,用户态来回转换,这又有几步操作。

所以我们说cnt--,它就不是原子的操作,它可以分好几步进行。就相对于原子操作不安全,可以你在执行第二步时突然时间片到了要执行别的了,别的操作可能恰好是cnt=50。这就会出现安全隐患。

信号量的设计

刚才我们也说了,申请信号量,本质就是对计数器--,专业术语就是P操作。

释放资源,释放信号量,那就是对计数器++,专业术语就是V操作。

其中P,V操作都是原子的,都是安全的。

system V IPC联系

通过对system V系列进程间通信的学习,可以发现共享内存、消息队列以及信号量,虽然它们内部的属性差别很大,但是维护它们的数据结构的第一个成员确实一样的,都是ipc_perm类型的成员变量。

这样设计的好处就是,在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC或system V资源,就在该数组当中开辟一个这样的结构。这也貌似体现了继承。所以说语言还是在一定程度上继承了操作系统。

本文地址:https://www.vps345.com/10852.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP docker 容器 运维 java-rabbitmq java 服务器安全 网络安全策略 防御服务器攻击 安全威胁和解决方案 程序员博客保护 数据保护 安全最佳实践 服务器 linux 游戏 云计算 网络工程师 网络管理 软考 2024 2024年上半年 下午真题 答案 Deepseek Deepseek-R1 大模型 私有化部署 推理模型 deepseek DeepSeek-R1 API接口 神经网络 人工智能 深度学习 计算机视觉 卷积神经网络 redis harmonyos 华为 javascript 前端 chrome edge 物联网 ubuntu 数据库 centos oracle 关系型 安全 分布式 python 机器学习 ddos android 网络安全 web安全 gcc centos 7 网络 tcp/ip 网络协议 ip协议 c语言 英语 Ollama Qwen2.5-coder 离线部署 YOLO conda pytorch vscode yolov5 macos windows DNS 进程 操作系统 进程控制 Ubuntu RTSP xop RTP RTSPServer 推流 视频 vue.js spring boot nginx https CH340 单片机 嵌入式硬件 串口驱动 CH341 uart 485 fstab ssh 经验分享 自动化 unix 模型联网 API CherryStudio mysql adb Flask FastAPI Waitress Gunicorn uWSGI Uvicorn 笔记 C 环境变量 进程地址空间 阿里云 llama 算法 opencv 自然语言处理 语言模型 pycharm ide django 游戏引擎 学习 ai nlp 数据库系统 php 开发语言 数据分析 开发环境 agi AIGC 科技 个人开发 架构 部署 像素流送api 像素流送UE4 像素流送卡顿 像素流送并发支持 typescript 计算机网络 MCP 云原生 ip spring cloud compose 持续部署 arm开发 人工智能生成内容 Dify MacMini Mac 迷你主机 mini Apple Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 springsecurity6 oauth2 授权服务器 前后端分离 jvm 虚拟机 网络结构图 程序 编程 内存 性能分析 学习方法 开源 milvus ROS 自动驾驶 centos-root /dev/mapper yum clean all df -h / du -sh 负载均衡 tomcat gnu kubernetes prometheus grafana qt arm ipython oracle fusion oracle中间件 apache 交互 计算机外设 bug dify 知识库 本地化部署 react.js 前端面试题 node.js c# audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 智能路由器 dell服务器 指令 UEFI Legacy MBR GPT U盘安装操作系统 教程 环境搭建 Java Maven 后端 eureka c++ burpsuite 安全工具 mac安全工具 burp安装教程 渗透工具 面试 AI-native gpt maxkb ARG 实时音视频 音视频 rpc Ubuntu20.04 GLIBC 2.35 JDK LInux Windows 外网访问 内网穿透 端口映射 ffmpeg 视频编解码 pip 远程工作 rust腐蚀 git npm jellyfin nas http asp.net大文件上传 asp.net大文件上传下载 asp.net大文件上传源码 ASP.NET断点续传 k8s ACL 流量控制 基本ACL 规则配置 数据结构 并查集 leetcode asm kafka AI大模型 大模型技术 本地部署大模型 ubuntu 18.04 安装教程 udp 本地环回 bind intellij-idea devops live555 rtsp rtp mcu 目标检测 鸿蒙 ui 快捷键 旋转屏幕 自动操作 fastapi mcp mcp-proxy mcp-inspector fastapi-mcp agent sse golang flutter Google pay Apple pay VMware C语言 虚拟现实 Linux xrdp 远程桌面 远程连接 word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 Alexnet flask 小程序 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 YOLOv8 NPU Atlas800 A300I pro asi_bench 目标跟踪 OpenVINO 推理应用 大数据 spark hive cron crontab日志 ArkUI 鸿蒙系统 ArkTS deepseek-r1 大模型本地部署 elasticsearch 性能优化 debian PVE 多线程服务器 Linux网络编程 sql 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 高级IO epoll websocket 机器人 virtualenv grub 版本升级 扩容 策略模式 USB转串口 缓存 vue3 excel 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 svn jenkins gitee vnc outlook 错误代码2603 无网络连接 2603 maven 进程优先级 调度队列 进程切换 华为认证 github yum docker-compose docker compose stm32项目 stm32 Linux 维护模式 matplotlib fonts-noto-cjk protobuf 序列化和反序列化 安装 虚拟局域网 AI opengl zotero WebDAV 同步失败 代理模式 反向代理 智能体开发 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 LDAP kylin windows 服务器安装 华为云 华为od filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 无人机 PX4 AutoDL 程序员创富 list AI编程 p2p 集成学习 集成测试 EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 虚拟显示器 远程控制 zabbix mariadb burp suite 抓包 axure 富文本编辑器 python2 ubuntu24.04 电子信息 通信工程 信息与通信 毕业 交换机 硬件 设备 GPU PCI-Express openvpn server openvpn配置教程 centos安装openvpn VMware Tools vmware tools安装 vmwaretools安装步骤 vmwaretools安装失败 vmware tool安装步骤 vm tools安装步骤 vm tools安装后不能拖 vmware tools安装步骤 ollama llm DeepSeek Chatbox 孤岛惊魂4 HarmonyOS Next 程序人生 Linux PID 监控k8s集群 集群内prometheus 安卓模拟器 mac adobe dubbo IMM 系统安全 bash 聚类 json rsyslog IM即时通讯 QQ 微信 企业微信 剪切板对通 HTML FORMAT nvm 移动端开发 Hyper-V WinRM TrustedHosts CPU 使用率 系统监控工具 linux 命令 ubuntu20.04 开机黑屏 压力测试 大模型压力测试 EvalScope JAVA 1024程序员节 ssh漏洞 ssh9.9p2 CVE-2025-23419 vite 私有化 本地部署 WSL2 IP 地址 命令 环境迁移 链表 自定义客户端 SAS cpu 实时 使用 zip unzip 嵌入式 pillow perf xcode C# MQTTS 双向认证 emqx 前端框架 ue4 着色器 ue5 虚幻 uni-app 上传视频文件到服务器 uniApp本地上传视频并预览 uniapp移动端h5网页 uniapp微信小程序上传视频 uniapp app端视频上传 uniapp uview组件库 图像处理 课程设计 ssl 思科 gitlab unity cursor transformer 数据挖掘 FTP 服务器 EtherCAT转Modbus EtherCAT转485网关 ECT转485串口服务器 ECT转Modbus485协议 ECT转Modbus串口网关 ECT转Modbus串口服务器 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 mysql离线安装 ubuntu22.04 mysql8.0 ros ros1 Noetic 20.04 apt 安装 实时互动 媒体 Doris搭建 docker搭建Doris Doris搭建过程 linux搭建Doris Doris搭建详细步骤 Doris部署 firewall rag ragflow 大模型部署 rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 TCP WebServer 运维开发 桥接模式 vmware windows虚拟机 虚拟机联网 spring sublime text 编辑器 qps 高并发 vim visualstudio 统信 NFS IIS .net core Hosting Bundle .NET Framework vs2022 react next.js 部署next.js frp ip命令 新增网卡 新增IP 启动网卡 多线程 C++ 高效I/O postman 测试工具 ansible iftop 网络流量监控 腾讯云 镜像源 QT 5.12.12 QT开发环境 Ubuntu18.04 pygame Mac内存不够用怎么办 webrtc WebUI DeepSeek V3 云电竞 云电脑 todesk 安全威胁分析 rabbitmq vm MCP server C/S LLM nacos 爬虫 网络爬虫 efficientVIT YOLOv8替换主干网络 TOLOv8 MacOS录屏软件 卡死 rust Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 jupyter html microsoft 智能体 autogen openai coze Agent CrewAI edge浏览器 onlyoffice 浪潮信息 AI服务器 CPU 主板 电源 网卡 互信 vSphere vCenter 软件定义数据中心 sddc 硬件工程 游戏程序 ios Trae叒更新了? 冯诺依曼体系 ocr fork wait waitpid exit 硬件架构 系统架构 大语言模型 LLMs jmeter 软件测试 HCIE 数通 网络药理学 生信 生物信息学 gromacs 分子动力学模拟 MD 动力学模拟 database .netcore docker run 数据卷挂载 交互模式 selenium wsl 工具 gateway Clion Nova ResharperC++引擎 Centos7 远程开发 WLAN vmamba rdp 实验 rancher 王者荣耀 区块链 VMware安装mocOS macOS系统安装 电脑 NVML nvidia-smi kotlin android studio iphone TrueLicense Mermaid 可视化图表 自动化生成 jdk grep linux环境变量 安卓 su sudo sudo原理 su切换 阻塞队列 生产者消费者模型 服务器崩坏原因 灵办AI 产品经理 web3.py langchain deep learning samba yolov8 ruoyi nftables 防火墙 华为OD 华为OD机试真题 可以组成网络的服务器 软件需求 linux内核 5G jar termux dash 正则表达式 驱动开发 命名管道 客户端与服务端通信 intellij idea numpy MQTT 消息队列 智能手机 进程程序替换 execl函数 execv函数 execvp函数 execvpe函数 putenv函数 多层架构 解耦 源码剖析 rtsp实现步骤 流媒体开发 lvm 磁盘挂载 磁盘分区 pdf visual studio code gpu算力 scikit-learn 微信分享 Image wxopensdk 远程服务 政务 分布式系统 监控运维 Prometheus Grafana ci/cd 代码规范 ai小智 语音助手 ai小智配网 ai小智教程 智能硬件 esp32语音助手 diy语音助手 Ubuntu 22.04 MySql 算家云 算力租赁 matlab hadoop big data diskgenius 7z 串口服务器 大模型入门 大模型教程 kernel IPMI 深度求索 私域 shell powerpoint SSH Xterminal Kali Linux 黑客 渗透测试 信息收集 ecmascript nextjs reactjs OpenCore Linux Vim DevEco Studio 主从复制 linux上传下载 雨云 NPS 程序员 Python macbook 压测 ECS 宝塔 chatgpt oneapi 大模型微调 信号处理 tcpdump 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 vu大文件秒传跨域报错cors ROS2 db VMware创建虚拟机 软件工程 软件构建 MobaXterm 文件传输 微信小程序 odoo 服务器动作 Server action java-ee 创意 社区 沙盒 需求分析 提示词 新盘添加 partedUtil Qualcomm WoS QNN AppBuilder kali 共享文件夹 gitea cuda remote-ssh netty linuxdeployqt 打包部署程序 appimagetool 回显服务器 UDP的API使用 etcd 数据安全 RBAC 升级 CVE-2024-7347 漏洞 Cursor nohup 异步执行 RustDesk自建服务器 rustdesk服务器 docker rustdesk React Next.js 开源框架 3d 数学建模 计算生物学 生物信息 基因组 ESP32 camera Arduino 云原生开发 K8S k8s管理系统 css css3 html5 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 KylinV10 麒麟操作系统 Vmware 国产数据库 瀚高数据库 数据迁移 下载安装 报错 计算机科学与技术 rocketmq open webui GCC aarch64 编译安装 HPC ECT转Modbus协议 EtherCAT转485协议 ECT转Modbus网关 HP Anyware oceanbase 传统数据库升级 银行 es6 qt6.3 g726 vue mongodb Dell R750XS Maxkb RAG技术 本地知识库 网易邮箱大师 VSCode iot 话题通信 服务通信 iTerm2 服务器配置 elk VMware安装Ubuntu Ubuntu安装k8s 温湿度数据上传到服务器 Arduino HTTP miniapp 真机调试 调试 debug 断点 网络API请求调试方法 进程信号 notepad 读写锁 Trae IDE AI 原生集成开发环境 Trae AI VPN wireguard aws 职场和发展 嵌入式实习 Hive环境搭建 hive3环境 Hive远程模式 web 匿名管道 离线部署dify 硅基流动 ChatBox Kylin-Server 国产操作系统 服务器安装 kubeless ollama api ollama外网访问 Cline 镜像下载 freebsd rc.local 开机自启 systemd 麒麟 微服务 postgresql 智慧农业 开源鸿蒙 团队开发 n8n 工作流 express SRS 流媒体 直播 远程过程调用 Windows环境 常用命令 文本命令 目录命令 webdav 宝塔面板无法访问 vr linux驱动开发 银河麒麟 其他 Docker Desktop tcp eclipse 热榜 IIS服务器 IIS性能 日志监控 localhost 毕设 行情服务器 股票交易 速度慢 切换 股票量化接口 股票API接口 端口号 开放端口 访问列表 FTP服务器 具身智能 Isaac Sim 虚拟仿真 一切皆文件 openEuler 教育电商 Ubuntu22.04 虚拟化 开发人员主页 flash-attention WSL2 上安装 Ubuntu redhat GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 clickhouse Invalid Host allowedHosts junit initramfs Linux内核 Grub 论文阅读 ELF加载 文件系统 路径解析 .net GameFramework HybridCLR Unity编辑器扩展 自动化工具 数据集 c sqlserver 智能电视 webstorm ping++ 豆瓣 追剧助手 迅雷 弹性计算 云服务器 裸金属服务器 弹性裸金属服务器 Webserver 异步 deepseak 文心一言 豆包 KIMI 腾讯元宝 CNNs 图像分类 unity3d minicom 串口调试工具 小智 功能测试 自动化测试 数据采集 Crawlee Playwright log4j CosyVoice googlecloud rustdesk 动静态库 强制清理 强制删除 mac废纸篓 华为证书 HarmonyOS认证 华为证书考试 Web服务器 多线程下载工具 网络编程 PYTHON 考试 H3C dns 昇腾 大模型训练/推理 推理问题 mindie xml bcompare Beyond Compare 云服务 Web应用服务器 中间件 MAVROS 四旋翼无人机 数据可视化 推荐算法 个人博客 RAGFLOW go 自动化编程 openwrt 京东云 监控 自动化运维 安防软件 Docker Docker Compose Kubernetes 国产化 k8s集群资源管理 linux安装配置 联网 easyconnect 代理 ros2 moveit 机器人运动 软件卸载 系统清理 python3.11 pyside6 界面 嵌入式系统开发 监控k8s 监控kubernetes firefox kamailio sip VoIP nohup后台启动 双系统 大数据平台 建站 Netty NAS 海康 wireshark 显示过滤器 ICMP Wireshark安装 虚拟机安装 mybatis copilot ebpf uprobe Ubuntu共享文件夹 共享目录 Linux共享文件夹 信号 内核 搜索引擎 全文检索 图搜索算法 显示器 curl wget Carla 智能驾驶 java-rocketmq 中兴光猫 换光猫 网络桥接 自己换光猫 免密 登录 公钥 私钥 云桌面 微软 AD域控 证书服务器 VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 rime 安全漏洞 信息安全 WebVM 安装MySQL 单例模式 流水线 脚本式流水线 ufw springboot 系统开发 binder 车载系统 framework 源码环境 fpga开发 kylin v10 麒麟 v10 低代码 多产物 ux 鲲鹏 npu hosts hosts文件管理工具 开源软件 视频监控 网站 TRAE sysctl.conf vm.nr_hugepages 代码调试 ipdb 单一职责原则 3GPP 卫星通信 nac 802.1 portal vasp安装 iperf3 带宽测试 计算机系统 iis systemctl composer 移动云 perl MQTT协议 消息服务器 代码 stable diffusion EMQX 通信协议 框架搭建 桌面环境 sqlite3 换源 国内源 Debian 物联网开发 selete 远程看看 远程协助 浏览器自动化 Xinference kind 环境配置 export import save load 迁移镜像 r语言 Redis Desktop 腾讯云大模型知识引擎 网络用户购物行为分析可视化平台 大数据毕业设计 visual studio 基础环境 宠物 毕业设计 免费学习 宠物领养 宠物平台 实战案例 DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 beautifulsoup openstack Xen KVM less deepseek-v3 ktransformers openjdk Mac软件 Linux权限 权限命令 特殊权限 拓扑图 sdkman AD域 top Linux top top命令详解 top命令重点 top常用参数 底层实现 ssrf 失效的访问控制 docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap 工具分享 webpack 大屏端 P2P HDLC AI写作 autoware shell脚本免交互 expect linux免交互 强化学习 YOLOv12 锁屏不生效 视觉检测 MS Materials 知识图谱 直播推流 GRE yaml Ultralytics 可视化 机柜 1U 2U Windsurf 图文教程 VMware虚拟机 macOS系统安装教程 macOS最新版 虚拟机安装macOS Sequoia ftp服务 文件上传 图形化界面 AI作画 远程 执行 sshpass 操作 chatbox armbian u-boot CLion c/c++ 串口 CUPS 打印机 Qt5 ESXi gpt-3 mac设置host 状态管理的 UDP 服务器 Arduino RTOS arkUI WSL win11 无法解析服务器的名称或地址 加解密 Yakit yaklang openssl ftp 云耀服务器 seatunnel Samba 模拟实现 gradle 用户管理 okhttp ragflow 源码启动 Portainer搭建 Portainer使用 Portainer使用详解 Portainer详解 Portainer portainer Linux的基础指令 统信UOS bonding 链路聚合 NVM Node Yarn PM2 执法记录仪 智能安全帽 smarteye vscode1.86 1.86版本 ssh远程连接 glibc UOS 统信操作系统 arkTs LLM Web APP Streamlit threejs 3D cudnn anaconda java-zookeeper HarmonyOS Tabs组件 TabContent TabBar TabsController 导航页签栏 滚动导航栏 Reactor 设计模式 llama3 Chatglm 开源大模型 iBMC UltraISO g++ g++13 windows日志 系统 黑苹果 开发 OpenManus 版本 RDP 笔灵AI AI工具 DIFY HistoryServer Spark YARN jobhistory 域名服务 DHCP 符号链接 配置 alias unalias 别名 僵尸进程 ubuntu24.04.1 生活 磁盘监控 内网渗透 靶机渗透 高德地图 鸿蒙接入高德地图 HarmonyOS5.0 Cookie HiCar CarLife+ CarPlay QT RK3588 firewalld v10 软件 playbook 剧本 file server http server web server ollama下载加速 IO 大模型应用 webgl 隐藏文件 隐藏目录 管理器 通配符 SSH 密钥生成 SSH 公钥 私钥 生成 Kali 渗透 RAID RAID技术 磁盘 存储 tar KingBase photoshop 键盘 博客 word 负载测试 SSE 权限 k8s部署 MySQL8.0 高可用集群(1主2从) 网卡的名称修改 eth0 ens33 深度优先 图论 并集查找 换根法 树上倍增 Termux harmonyOS面试题 磁盘清理 开机自启动 桌面快捷方式 springcloud 跨平台 源代码管理 DevOps 软件交付 数据驱动 应用场景 fd 文件描述符 蓝耘科技 元生代平台工作流 ComfyUI 飞牛nas fnos SWAT 配置文件 服务管理 网络共享 retry 重试机制 单元测试 mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 pyicu 迁移指南 服务器扩容没有扩容成功 Github加速 Mac上Github加速 Chrome浏览器插件 计算机 mq x64 SIGSEGV xmm0 券商 股票交易接口api 类型 特点 arcgis zookeeper 视频平台 录像 视频转发 性能测试 视频流 DrissionPage 相机 矩阵 Linux的权限 CentOS Ardupilot 银河麒麟服务器操作系统 系统激活 sublime text3 设置代理 实用教程 软链接 硬链接 windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 ShapeFile GeoJSON Nginx ArkTs 电路仿真 multisim 硬件工程师 硬件工程师学习 电路图 电路分析 仪器仪表 工业4.0 Docker Hub docker pull daemon.json Office umeditor粘贴word ueditor粘贴word ueditor复制word ueditor上传word图片 ueditor导入word ueditor导入pdf ueditor导入ppt 客户端 可信计算技术 W5500 OLED u8g2 TCP服务器 SEO chfs ubuntu 16.04 Linux环境 csrf cnn VGG网络 卷积层 池化层 宝塔面板 同步 备份 SSH 服务 SSH Server OpenSSH Server vscode 1.86 网站搭建 serv00 边缘计算 服务器部署 本地拉取打包 蓝桥杯 直流充电桩 充电桩 微信开放平台 微信公众平台 微信公众号配置 pyautogui 远程登录 telnet lighttpd安装 Ubuntu配置 Windows安装 服务器优化 kvm qemu libvirt Attention NLP 7-zip cmake prompt fpga banner bot rpa web3 区块链项目 LVM lvresize 磁盘扩容 pvcreate mamba Vmamba DenseNet 网络穿透 群晖 火绒安全 Nuxt.js 管道 uniapp 电视剧收视率分析与可视化平台 cd 目录切换 恒源云 servlet 毕昇JDK 致远OA OA服务器 服务器磁盘扩容 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 RAGFlow 网络原理 RAG 检索增强生成 文档解析 大模型垂直应用 CORS 跨域 IPv4/IPv6双栈 双栈技术 网路规划设计 ensp综合实验 IPv4过渡IPv6 IPv4与IPv6 医疗APP开发 app开发 env 变量 飞腾处理器 apt 弹性服务器 virtualbox tensorflow 模拟器 Ark-TS语言 DocFlow 半虚拟化 硬件虚拟化 Hypervisor ubuntu24 vivado24 能力提升 面试宝典 技术 IT信息化 进程间通信 can 线程池 code-server mosquitto cmos resolv.conf Kylin OS 创业创新 Open WebUI pgpool 密码学 输入法 业界资讯 空间 查错 端口测试 模拟退火算法 xpath定位元素 田俊楠 技能大赛 wsgiref Web 服务器网关接口 Echarts图表 折线图 柱状图 异步动态数据 鸿蒙开发 可视化效果 echarts 信息可视化 网页设计 WINCC skynet 重启 排查 系统重启 日志 原因 hdc AISphereButler esp32 蓝牙 实时内核 Helm k8s集群 lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 dity make ukui 麒麟kylinos openeuler 服务器ssl异常解决 多端开发 智慧分发 应用生态 鸿蒙OS 图形渲染 d3d12 源代码 配置原理 HarmonyOS NEXT 原生鸿蒙 实习 Jellyfin Java Applet URL操作 服务器建立 Socket编程 网络文件读取 pthread 终端 Pyppeteer searxng VPS pyqt 大模型推理 WebRTC pppoe radius 本地部署AI大模型 Zoertier 内网组网 金仓数据库 2025 征文 数据库平替用金仓 RTMP 应用层 Reactor反应堆 postgres Dify重启后重新初始化 Qwen3 qwen3 32b vllm OD机试真题 服务器能耗统计 iNode Macos cpp-httplib opensearch helm scapy 服务器主板 AI芯片 MI300x 游戏服务器 TrinityCore 魔兽世界 wsl2 聊天服务器 套接字 Socket wps shard docker命令大全 智能音箱 智能家居 Typore IPMITOOL BMC 硬件管理 trae opcua opcda KEPServer安装 网络文件系统 crosstool-ng XCC Lenovo SVN Server tortoise svn asp.net上传文件夹 asp.net上传大文件 .net core断点续传 CentOS Stream 文件分享 make命令 makefile文件 Linux的基础开发工具 繁忙 服务器繁忙 解决办法 替代网站 汇总推荐 AI推理 xfce CDN token sas dba 崖山数据库 YashanDB 银河麒麟操作系统 lb 协议 nfs 裸机装机 linux磁盘分区 裸机安装linux 裸机安装ubuntu 裸机安装kali 裸机 服务器部署ai模型 SSL 域名 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 ceph saltstack 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 分析解读 embedding 网络攻击模型 lio-sam SLAM Bug解决 Qt platform OpenCV eNSP 网络规划 VLAN 企业网络 大模型面经 大模型学习 显卡驱动 AnythingLLM AnythingLLM安装 玩游戏 交叉编译 三级等保 服务器审计日志备份 机架式服务器 1U工控机 国产工控机 laravel yashandb LORA ajax netlink libnl3 内网环境 MDK 嵌入式开发工具 论文笔记 bootstrap 支持向量机 黑客技术 容器技术 k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm 流式接口 URL harmonyosnext api 序列化反序列化 docker desktop 镜像 小艺 Pura X 联想开天P90Z装win10 欧拉系统 ruby minio 文件存储服务器组件 线程 rclone AList fnOS cfssl EVE-NG IP配置 netplan 小番茄C盘清理 便捷易用C盘清理工具 小番茄C盘清理的优势尽显何处? 教你深度体验小番茄C盘清理 C盘变红?!不知所措? C盘瘦身后电脑会发生什么变化? 大文件秒传跨域报错cors 网工 archlinux kde plasma 充电桩平台 充电桩开源平台 向日葵 zerotier SSL证书 终端工具 远程工具 springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 数据库架构 数据管理 数据治理 数据编织 数据虚拟化 ssh远程登录 cpolar 健康医疗 互联网医院 VR手套 数据手套 动捕手套 动捕数据手套 GRUB引导 Linux技巧 Xshell thingsboard safari 历史版本 下载 av1 电视盒子 机顶盒ROM 魔百盒刷机 llamafactory 微调 Qwen 雨云服务器 免费 etl uv neo4j 数据仓库 数据库开发 电脑桌面出现linux图标 电脑桌面linux图标删除不了 电脑桌面Liunx图标删不掉 linux图标删不掉 es gaussdb nvidia 相差8小时 UTC 时间 keepalived 多进程 sonoma 自动更新 AI代码编辑器 js 无桌面 命令行 risc-v rnn chrome devtools chromedriver Alist mount 挂载 网盘 框架 ArcTS ArcUI GridItem 服务网格 istio Linux24.04 deepin 对比 meld DiffMerge Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 金融 Wi-Fi 我的世界 我的世界联机 数码 用户缓冲区 EasyConnect 支付 微信支付 开放平台 glm4 jina 产测工具框架 IMX6ULL 管理框架 SenseVoice 宕机切换 服务器宕机 录音麦克风权限判断检测 录音功能 录音文件mp3播放 小程序实现录音及播放功能 RecorderManager 解决录音报错播放没声音问题 mvc SysBench 基准测试 OpenHarmony DBeaver 服务器时间 cocoapods ecm bpm 影刀 Minecraft DOIT 四博智联 联机 僵尸毁灭工程 游戏联机 开服 idm 软件开发 信任链 小游戏 五子棋 cocos2d 3dcoat 树莓派 VNC miniconda iDRAC R720xd powerbi GPU训练 swift XFS xfs文件系统损坏 I_O error trea idea 测试用例 NLP模型 自学笔记 小米 澎湃OS Android 聊天室 源码 日志分析 系统取证 Node-Red 编程工具 流编程 iventoy VmWare OpenEuler 混合开发 环境安装 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 考研 在线office SoC FunASR ASR ECS服务器 安全架构 蓝桥杯C++组 基础入门 fast muduo 程序化交易 PTrade QMT 量化交易 量化股票 X11 Xming bat Claude Desktop Claude MCP Windows Cli MCP workflow 飞牛 图片增强 增强数据 AimRT 显卡驱动持久化 GPU持久化 IPv4 子网掩码 公网IP 私有IP linux 命令 sed 命令 Spring Security AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 Linux find grep anythingllm open-webui docker国内镜像 easyui Obsidian Dataview 显示管理器 lightdm gdm 代理服务器 jetty undertow ShenTong 自动化任务管理 飞牛NAS 飞牛OS MacBook Pro yum源切换 更换国内yum源 音乐服务器 Navidrome 音流 语音识别 client-go k8s二次开发 邮件APP 免费软件 wordpress Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 MNN Ubuntu Server Ubuntu 22.04.5 GeneCards OMIM TTD NAT转发 NAT Server 分子对接 autodock mgltools PDB PubChem Docker快速入门 mysql安装报错 windows拒绝安装 llama.cpp DeepSeek行业应用 Heroku 网站部署 conda配置 conda镜像源 卸载 列表 PyQt PySide6 Dell HPE 联想 浪潮 游戏机 星河版 DeepSeek r1 即时通信 NIO vsxsrv 思科模拟器 Cisco 免费域名 域名解析 nuxt3 Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 MAC SecureCRT .net mvc断点续传 稳定性 看门狗 micropython mqtt 大版本升 升级Ubuntu系统 solidworks安装 算力 服务器管理 配置教程 网站管理 办公自动化 pdf教程 comfyui comfyui教程 vpn rtc UDP UOS1070e 代码托管服务 社交电子 #影刀RPA# 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 hibernate yum换源 hexo navicat 小智AI服务端 xiaozhi TTS 运维监控 element-ui 上传视频并预览视频 vue上传本地视频及进度条功能 vue2选择视频上传到服务器 upload上传视频组件插件 批量上传视频 限制单个上传视频 AD 域管理 nosql figma 规格说明书 红黑树封装map和set SystemV rsync 内网服务器 内网代理 内网通信 备选 调用 示例 bigdata ranger MySQL8.0 知行EDI 电子数据交换 知行之桥 EDI 增强现实 沉浸式体验 技术实现 案例分析 AR 可用性测试 GoogLeNet mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 飞书 虚幻引擎 问题解决 Headless Linux Sealos 智能合约 哈希算法 wpf MVS 海康威视相机 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 sqlite 脚本 flink 嵌入式Linux IPC 三次握手 计算机学习路线 编程语言选择 gru 烟花代码 烟花 元旦 EMUI 回退 降级 OpenManage fiddler csrutil mac恢复模式进入方法 SIP 恢复模式 概率论 网络建设与运维 网络搭建 神州数码 神州数码云平台 云平台 做raid 装系统 极限编程 mybase h.264 项目部署到linux服务器 项目部署过程 超融合 PPI String Cytoscape CytoHubba deekseek open Euler dde 抗锯齿 react native 动态规划 高频交易 Python 视频爬取教程 Python 视频爬取 Python教程 Python 视频教程 Radius mcp服务器 client close chromium dpi dock 加速 Python基础 Python技巧 Anolis nginx安装 linux插件下载 proxy模式 linux子系统 忘记密码 性能调优 安全代理 本地知识库部署 DeepSeek R1 模型 whistle 僵尸世界大战 游戏服务器搭建 Claude GIS 遥感 WebGIS Apache Beam 批流统一 案例展示 数据分区 容错机制 网页服务器 web服务器 分布式账本 共识算法 设备树 ai工具 阿里云ECS ldap 多个客户端访问 IO多路复用 TCP相关API paddle 软考设计师 中级设计师 SQL 软件设计师 架构与原理 IDEA gunicorn autodl triton 模型分析 docker search 多路转接 Unity Dedicated Server Host Client 无头主机 USB网络共享 logstash Bandizip Mac解压 Mac压缩 压缩菜单 vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 ubuntu安装 linux入门小白 NFC 近场通讯 智能门锁 百度云 矩池云 数据下载 数据传输 线程同步 线程互斥 条件变量 浏览器开发 AI浏览器 项目部署 easyTier 组网 xshell termius iterm2 商用密码产品体系 sentinel 搭建个人相关服务器 midjourney System V共享内存 进程通信 IO模型 华为机试 信创 信创终端 中科方德 佛山戴尔服务器维修 佛山三水服务器维修 OpenSSH swoole 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 c/s deployment daemonset statefulset cronjob web开发 合成模型 扩散模型 图像生成 我的世界服务器搭建 跨域请求 Putty 花生壳 干货分享 黑客工具 密码爆破 匿名FTP 邮件传输代理 SSL支持 chroot监狱技术 kerberos finalsheel node 移动开发 brew 集群管理 语法 Charles seleium IMX317 MIPI H265 VCU 打不开xxx软件 无法检查其是否包含恶意软件 tailscale derp derper 中转 线性代数 电商平台 TCP协议 流量运营 C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 达梦 DM8 捆绑 链接 谷歌浏览器 youtube google gmail gitee go 欧标 OCPP Logstash 日志采集 lua 端口聚合 windows11 minecraft Ubuntu 24.04.1 轻量级服务器 homeassistant 音乐库 prometheus数据采集 prometheus数据模型 prometheus特点 pycharm安装 regedit 开机启动 image 材料工程 接口返回 软负载 内存管理 端口 查看 ss Qwen2.5-VL aac docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 玩机技巧 软件分享 软件图标 MinIO 可执行程序 #STC8 #STM32 技术共享 post.io 企业邮箱 搭建邮箱 游戏开发 WireGuard 异地组网 OS NVIDIA massa sui aptos sei 企业网络规划 华为eNSP ISO镜像作为本地源 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 钉钉 Erlang OTP gen_server 热代码交换 事务语义 IPv6 IPv6测试 IPv6测速 IPv6检测 IPv6查询 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 移动魔百盒 HTTP 服务器控制 ESP32 DeepSeek AI员工 dns是什么 如何设置电脑dns dns应该如何设置 银河麒麟桌面操作系统 抓包工具 xss win服务器架设 windows server hugo AzureDataStudio qt5 客户端开发 android-studio 查询数据库服务IP地址 SQL Server 分布式训练 deepseek r1 动态库 GCC编译器 -fPIC -shared AI agent 状态模式 授时服务 北斗授时 查看显卡进程 fuser ArtTS 金仓数据库概述 金仓数据库的产品优化提案 qt项目 qt项目实战 qt教程 机械臂 国标28181 监控接入 语音广播 流程 SDP win向maOS迁移数据 银河麒麟高级服务器 外接硬盘 Kylin VS Code 根服务器 子系统 Masshunter 质谱采集分析软件 使用教程 科研软件 finebi MacOS docker部署Python qtcreator 实时云渲染 云渲染 3D推流 solr pyscenic 生信教程 物理地址 页表 虚拟地址 数据库管理 lrzsz authing ABAP 大大通 第三代半导体 碳化硅 mcp协议 go-zero UFW 零售 存储维护 NetApp存储 EMC存储 ardunio BLE pxe 元服务 应用上架 自定义登录信息展示 motd 美化登录 网络库 西门子PLC 通讯 北亚数据恢复 oracle数据恢复 粘包问题 Qt QModbus 风扇控制软件 华为鸿蒙系统 ArkTS语言 Component 生命周期 条件渲染 Image图片组件 鸿蒙NEXT macOS 向量数据库 安装部署 milvus安装 输入系统 accept 工厂方法模式 进程池实现 GRANT REVOKE VM虚拟机 pythonai PlaywrightMCP tidb 火山引擎 设计规范 mujoco RK3568 webview 带外管理 云计算面试题 搜狗输入法 中文输入法 caddy sequoiaDB Metastore Catalog qwen2vl 服务器正确解析请求体 Unity插件 数字证书 签署证书 AI Agent 字节智能运维 接口优化 watchtower 计算虚拟化 弹性裸金属 流程图 mermaid 解决方案 软件商店 livecd systemtools 静态IP STL 李心怡 HAProxy 智能问答 Spring AI Milvus Async注解 SPI AWS 轮播图 华为昇腾910b3 mapreduce 定义 核心特点 优缺点 适用场景 ubantu 漏洞报告生成 vCenter服务器 ESXi主机 监控与管理 故障排除 日志记录 Multi-Agent 事件驱动 PostgreSQL15数据库 bert CAD瓦片化 栅格瓦片 矢量瓦片 Web可视化 DWG解析 金字塔模型 模板 泛型编程 AI提示词优化 web环境 notepad++ 命令模式 顽固图标 启动台 制造 环境 非root java毕业设计 微信小程序医院预约挂号 医院预约 医院预约挂号 小程序挂号 蜂窝网络 频率复用 射频单元 无线协议接口RAN 主同步信号PSS OpenGL 导航栏 devmem 烟雾检测 yolo检测 消防检测 能源 学习路线 vmware tools 手机 MLLMs VLM gpt-4v 文件共享 Modbus TCP openvino access blocked 破解 医院门诊管理系统 4 - 分布式通信、分布式张量 LSTM 实时日志 logs Ubuntu 24.04 搜狗输入法闪屏 Ubuntu中文输入法 LVS 数字比特流 模拟信号 将二进制数据映射到模拟波形上 频谱资源 振幅频率相位 载波高频正弦波 动态域名 代码复审 容器化 Serverless