最新资讯

  • Linux的进程间通信

Linux的进程间通信

2025-05-19 23:00:50 0 阅读

目录

进程间通信介绍

进程间通信的概念

主要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/11381.html

搜索文章

Tags

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