管道进行进程间通信(上)

管道进行进程间通信

在posix和system V标准还没有出现的时候,进程间是如何进行通信的呢?这就要借助于我们今天学习的这个东西了。在进程间通信的标准没有出现之前,在os中就已经存在了文件了。而管道就是基于文件的一种进行进程间通信的方式。

什么是管道

首先一个文件是可以被一个进程打开并访问的,那么现在的问题是一个文件能否被多个进程打开并访问呢?如果能的话,那么这个文件不就是一个共享的空间吗?只要一个进程往这个文件中写数据,然后另外一个进程从这个文件中读取数据。不就实现了将一个数据从一个进程移动到另外一个进程上了吗?但是使用这种基于一个实体文件的方式有一个致命的缺点那就是,没写一个数据到文件中,这个数据就需要先刷新到磁盘上,然后再从磁盘将这个数据写到内存中,让另外一个进程读取这个数据。这样的方式就会伴生出很多的效率问题。但是我们可以借助这个思想。只不过这次我们的这个文件不会将数据往磁盘上去做刷新,因为并没有人规定一定要将将文件中的数据写到磁盘上。我们只使用内存也能够完成使用管道通信。

在上图中的那个|我们就称之为管道,将who打印出的信息交给wc -l命令,这是我在刚开始学习指令的时候就使用过的管道。上面的这个命令能够查看我们当前的这个系统正在被多少人使用 。

我们通过下面的几个步骤来学习管道这个东西。

下面我们来学习管道的原理:

管道的原理

首先每一个进程都会存在自己的task_struct 对象然后会存在一个文件描述符表,这个表中保存的就是一个一个的的file文件指针。因为是在数组中这些文件指针就自然有了下标,进程每打开一个文件,被打开的文件就会创建一个struct file对象。而默认打开的文件存在三个分别是stdin(标准输入){终端},stdout(标准输出){终端 },stderr(标准错误)分别对应数组的0,1,2下标。而通过改变0,1下标中的内容就能完成输入和输出重定向。

在某一个pcb对象打开一个文件的时候,会在数组的3号下标中(0 1 2中已经有值了,文件描述符的分配是从最小的未被使用的优先被使用)储存这个新打开文件的文件指针,同时将这个下标返回到上层。此时的上层就得到了一个新打开的文件。

而一个文件需要提供自己详细的属性。然后就是对应的操作这个文件的对应的方法集。然后就是每一个文件都需要有自己的文件页缓冲区。如果这个被打开的文件在磁盘中是存在的,那么就会根据磁盘中储存的这个文件的属性和数据去预加载对应的inode对象和文件页缓冲区。以供打开文件的进程来进行读写数据。在你完成写入之后,文件缓冲区中的数据和磁盘中这个文件的数据不一致,我们称之为“脏数据”(Dirty Data)。然后将这个脏数据刷新到磁盘中去,就完成了对这个文件的写入(落盘的过程)。如果你是要读取这个文件在磁盘中的数据,os也需要先将磁盘中这个文件的数据加载到内存中再进行读取。也就是无论是读还是写,磁盘中的文件一定需要首先将数据写到内存中去。

图:

那么这里我们设想一下能否让一个文件,不需要去打开磁盘呢?即让这个文件真正的成为一个内存级文件。在技术上是有可行性的。即这种文件并不需要在磁盘中是存在的,只需要我们能够将其运用起来就可以了。当然这种文件自然也能挂接到文件系统上,让用户能够看到这个文件,但是我们需要记住的是这个文件并不存在于磁盘中只存在于内存中。以上是内存级文件的特性。

假设这里一个进程以读的方式打开这个内存级文件。

然后这个进程再创建自己的子进程。

那么对于这个子进程我们之前打开的一批文件(键盘,显示器,显示器,内存级别文件)需不需要重新拷贝一份给这个子进程呢?首先创建子进程的时候pcb对象和files_struct对象肯定是要进行拷贝的,因为files_struct表明的是一个进程所打开的文件的列表。所以如果创建了子进程那么files_struct肯定是需要被拷贝的。但是那一批文件是不需要拷贝的。因为文件系统就那个和对应的进程管理系统是同级别的关系。因此在创建了进程之后,(因为files_struct中的内容是一样的)子进程的files_struct也会指向父进程指向的那张表。

此时父子进程就会看到同一份资源(类似于浅拷贝)。

这就是为什么我们在创建了一个子进程(fork)然后在子进程中往显示器打印信息,和父进程往显示器打印信息,都会打印在同一个终端中而不是打印到其它的终端中(因为父子进程指向的就是这一个终端)。

而我们的进程间进行通信的前提条件就是:

如果这个条件都不能满足就不能进行进程间通信。

此时父子进程看到的这个内存级别的文件就是同一份资源。并且这个文件还存在自己对应的文件缓冲区。如果父进程往这个文件中写入,那么子进程就能够通过这个文件完成对数据的读取。此时的双方就完成了进程间的通信,当然这只是原理还是存在漏洞的。

所以结论:管道的本质就是内存级文件

这里我们再思考一下如果我们的两个进程在通信的期间其中一方将这个管道文件关闭了,会不会影响另外一方呢?从我们刚刚讲解的原理来看大概率是存在这个问题的,但是os肯定也是考虑了这个问题的。如何解决呢?在一个文件的struct file对象中存在有一个int cnt的引用计数,这个计数的功能就是记录存在多少进程是打开这个文件的。此时如果我们的父进程关闭这个内存级别文件,因为cnt--之后并不为0,所以这里的这个内存级别文件是不会关闭的。

但是我们之前所提到过的那个情景时存在错误的,在上面的那个场景中,因为父进程是以读的方式打开的这个文件,那么由父进程创建的子进程自然也只能以读的方式打开这个内存级文件。如果是这样的话,是无法完成两个进程的通信的。那如果让进程在打开文件的时候以读写的方式打开呢?但是管道在设计的时候就不允许同时读写。

那么管道要如何进行通信呢?

那么此时我们就需要让父进程在打开共享文件的时候不能这么草率。我们的父进程在打开这个文件的时候,即以读的方式打开,又以写的方式打开。

那么由此创建出来的子进程也是这样的。

然后再根据具体的场景你是需要让父进程写文件,子进程读文件(反之),来关闭两个进程打开的文件,这里我以父进程写,子进程读为例子。

那么父进程就会关闭fd[0](读取),而子进程就会关闭fd[1](写入)。达到上面的父进程写,而子进程读的效果。

此时就建立了一个通信信道。

以上也还只是理论而已。我们来看一下实际的管道是如何实现这个理论的。

首先是父进程以读的方式打开了这个文件:

因为上面我们也看到了父进程是以读端和写端的方式打开的这个文件,现在读端已经就绪了,下面我们就需要再次打开这个文件,只不过是以写的方式打开。如果某一个进程再一次打开一个已经被自己打开的文件的时候,os还是会为这个文件再创建一个file对象的。 只不过这两个file对象指向的是同一个文件的inode对象,操作方法集,文件缓冲区。

我们就这么理解(朴素性理解)。

此时我们再去创建对应的子进程。

那么如果管道支持一个进程既能读又能写,就可能会出现下面的这种情况,在父进程写好了数据之后,再去读数据发现读到的任然是自己的数据,这种情况是存在的。并且对于文件缓冲区中的数据,也不好区分这是谁写的数据,应该让谁来读取(因为管道就是为了完成通信啊)。当然要解决也是可以的,但是很麻烦。所以设计管道进行通信的设计者就做了一个规定。

这就是为什么进程间通信的时候,直接将文件这一套拿过来就进行了通信,设计者其实是可以创造一个新的专门的模块来进行进程间通信的。

最后就是为了简单才这么设计的。既然只能单向通信究竟是让父进程读还是写,就由用户来决定了。

这里假设我们想让子进程写,而父进程进行读取。

但是此时对应的父子进程其实不关闭对应的另外一个不需要的文件描述符其实也是可以的(只要两个进程都保证不会使用另外一个文件描述符即可)。但是可能难免会出现误操作,所以最好还是将另外一个不需要使用的文件描述符关闭比较好。

再关闭了之后我们就建立了一个管道:

并且因为我们将父子进程中的那个不需要使用的文件描述符关闭了,对应的file r和file w中的引用计数都成为了1,也就不会存在进程通信受到影响了。

而这才是管道真正的原理。

而正是因为这种单向通信的原因,所以才将这种实现进程通信的方式称之为管道。

这里需要注意是因为这种单向通信的性质我们才将这种方式命名为管道。为什么只能单向通信的呢?第一这是基于文件实现的进程通信,第二本来设计者就是图简单才设计的这种通信方式。然后才有了管道的名字。

那么如果我们需要进行双向通信呢?其中的一种方式就是建立多个管道。

这里我们在思考一下如果两个想要通信的进程没有任何的关系,可以使用我们在上面说的这个原理进行通信吗?不能

即管道通信必须是父子关系。

那么如果一个父进程创建了多个子进程,然后某一个子进程能否和自己的兄弟进程进行通信吗?当然可以。

由此能够得出结论使用管道进行通信的进程必须要具有血缘关系。那么这个内存级文件有名字吗?当然没有,因为这个文件并没有存在于磁盘中自然就不需要路径来标识它,也不需要inode编号。进行通信的进程是通过继承的方式来得到这一个内存级文件的。而这种管道我们就称之为匿名管道。

到这里通信了吗?没有因为还没有发送信息,我们所做的这些事情都是在建立通信信道。

以上我们就介绍了基本的理论。

下面我们来进行对应的接口介绍和编写代码验证上面的理论。

接口以及代码编写

下面我们就来介绍一个非常重要的接口函数。

pipe函数

这里的问题就是为什么这里是pipefd[2]呢?

因为这个是一个输出型参数这个函数会将某个进程的读写端创建好(对管道文件的读写端)包括管道文件file对象的传创建,然后将读写端对应的fd下标放到数组之后将这个数组返回到上层。

而返回的pipefd数组中的两个位置的内容就是下面的:

下面我们就来写代码。虽然我们下面使用的代码是c++的代码,但是在调用系统接口的时候,还是需要使用c的代码,所以我们需要训练好c和c++混编的技能。

下面我们就来完善makefile文件和检查初步使用一些pipe函数。

makefile文件

下面是我们要运行的代码:

运行的结果:

我们一定要记住在这个数组中1是写端,而0是读端。

下面我们需要进行进程间的通信就需要创建子进程了。

代码的大体框架如下:

除此之外,如果你要在c++代码中使用c的库函数那么需要增加类似下面这样的头文件。

我们下面的目标就是完成Write和Read函数了。

Write函数

NUM是define定义的1024

然后我们编译运行一下是否运行成功了

这证明了在buff数组中确实是存在了这个信息,下面我们就会将这个信息写到管道文件中,最后我们的父进程就会读到上面的信息。

如何写呢?将buff发送到管道文件中,这也是Linux中一切皆是文件的好处,无论你要往磁盘写,还是往什么地方写,在Linux看来都是往一个文件中写,那么就可以使用write系统调用直接写。所以这里我们只需要使用write调用函数接口就可以了。

​write​​ 函数的返回值,如果写入成功,返回值是写入的字节数。还有其它的可能我们暂时这里先不关心,后面再学习网络的时候再去研究

此时子进程就会每一秒往父进程写一段字符串。

下面我们就来完善父进程的Read函数

当然在有时候是存在buff被写满的情况的,但是我们这里暂时不考虑。我们就假设这个buff不会被写满。

最后我们运行一下:

但是要让父进程和子进程都得到一个字符串还有一个方法那就是直接将这个字符串设置为全局的,不就让这个字符串既能让父进程得到又能让子进程得到了吗?但是这个方法只能传输静态的信息,如果我们这里传输的是一个动态的信息呢?正如我们上面将number加上了。如果使用的是全局的方法,其中一个进程对字符串进行了写入,就会发生写时拷贝,此时的另一个进程就得不到写入后的信息了。使用全局的那种方法只是单纯的将数据继承了下来,而不是达到了通讯的目的。

因为管道文件本质来说是操作系统的内核文件(资源由系统提供,而系统不相信父子进程),这个内核文件肯定是不会允许父子进程随意进行访问的。所以父进程在进行访问的时候需要使用系统调用read和write。防止父子进程对内核数据造成破坏。

那么父子进程在进行通讯的时候,通讯的信息经历了哪些次的拷贝呢?

首先在Write中将buff(用户层缓冲区中的数据)使用系统调用拷贝到了管道文件对应的文件缓冲区中。而Read函数中则是父进程将管道文件中的数据,使用系统调用将信息拷贝到了父进程的用户缓冲区中(这里没有考虑语言的缓冲区)。

所以答案是两次。从应用到内核再到用户。

也就是让管道文件充当了一个传话使的工作。

那么现在代码已经写了,我们就需要通过代码来得到管道的特征。

管道的特征

在讲解管道特征的同时我们会去伴生的了解一下管道通信的四种情况。

现在我们已经知道了两个管道通信的两个特征。

第三个我们回到上面写的代码可以看到在子进程的写端是具有sleep的但是在父进程的读端是没有sleep的。但是在运行的时候我们发现我们只是让我们的子进程每隔一秒打印一次,父进程为什么也是隔1秒才打印一次呢?父进程没有sleep。

原因1:如果父进程不关心缓冲区中是否是自己可以读取的信息就会发生读取到垃圾信息的情况。

因为缓冲区中是一定会存在二进制信息的,但是这个二进制信息不一定是父进程需要的信息。如果父进程会不断的读取缓冲区中的信息,就可能读取到垃圾信息。那么就会打印出乱码(上面的代码)。并且因为父进程不会去等待子进程,那么父进程会疯狂的打印。而子进程只会每隔一秒写入一条信息。但是事实上没有这个现象,也就是我的父进程是在

照顾子进程,如果父进程发现在缓冲区中没有自己需要的信息,那么父进程就会等到子进程写入。

下面我们将等待的时间改为50秒。再去编译运行一下。

可以看到在很长的时间里面父进程都是处于没有打印的情况的。说明父进程一直在等待子进程写入。

这说明了父子进程是会进行协同的。并不是各运行各自的。

在这里我们就能得到一个特征:首先我们得进程进行通讯的前提是需要让不同的进程看到同一份资源,那么这一份资源就是被多执行流共享的,那么就难免会出现冲突的情况。正如可能会出现你正在读的时候,又来写了,就可能出现将别人的数据覆盖的情况。这个问题也就是临界资源竞争的问题。后面会说明。这个问题导致的现象就是我们的父进程在前半段读了一个信息,但是在刚读取到的前半段的时候,后半段的信息已经被新的信息写入了,导致父进程读取的信息出错。

但是我们刚刚的实验证明了这个情况已经在os底层解决了原因就是我们黑体字说的

我们先记住这个结论就可以了。

总结下来我们的代码就是虽然子进程写的慢,但是父进程是会等待子进程的。父进程在没有数据的时候,是不会去进行读的。

那么反过来呢?如果我们的子进程在不停的写,而我们的父进程相隔很久才会读取又会发生什么呢?

在这里临时插入一个点,当我们的进程在读取管道中的数据的时候,对于读取完成的数据是会清理的,但是并不是使用0/什么其它的树去覆盖,而是代表这个被读取完的数据可以被覆盖了。

回到刚刚的那个代码,对于管道一共具有四种情况:

第一种情况我们刚刚已经演示了。

而第二种情况就是子进程在不停的写,而父进程相隔一段时间才会去读。

现在编译运行一下。

这里因为写端在不停的写,所以一瞬间就将管道写满了。因为写端已经将管道写满了,所以对应的2554这个数字就不再往上增长了。

这里就存在了一个子问题那就是管道是具有固定大小的?但是这个大小是多少呢?存在固定大小因为子进程一瞬间就将管道写满了。然后写端就被阻塞了。

总结就是:如果管道为空读端就需要等待,相反如果管道为满那么写端就要等待。但是我们这里的问题是:父子进程不是越好了只会读取字符串吗?怎么一瞬间就将这么一大串字符圈读取进来了。为什么父进程一次性就将这么多的数据一次全读取进来了呢?

因为我们这里的代码就是,父进程一次直接将所有写入的数据全部读进来,之前因为子进程写的慢,所以没有出现这种情况。而这里子进程写的是很快的,所以这里就直接将这么多的数据一起都进来了。

因为父进程不知道你要的是字符串,在父进程看来,它读取到的都是一个一个的字符。就算是之前能够一条一条打印也是因为子进程写的慢,而父进程读的块导致的。父进程并不知道这里需要读取的是一个字符串,在他看来自己读取的都是字符。在父进程看来分离这些字符串的工作是交给用户的。

由此我们就能得到最后一个特征了。

管道是面向字节流的

这个字节流在之后的博客我会说明。

也就是无论你的写端写多少的数据我的读端是不会管的。有可能读端会将写端写了好几次的数据一次性全部读取进来。在读端看来他读取的都是一大串的字符,而这些字符也就是我们说明的字节流。读端不会做任何的处理,对于这个字节流的处理读端是直接交给了用户的。

而为了处理这样的情况,就需要定制某些关于协议的东西。协议我也会在后面的博客详细说明。这里我们可以简单理解成我们让父子进程约定好读取多大,写多大即可。

最后还有一个特点,当我们的上面的父子进程结束之后,管道文件会自动地被os回收。

当进程都没有了,管道自然没有存在地必要了。

情况:

这两个情况也证明了,进行通信地两个进程是会进行协同的。

那么管道是多大呢?

在centeros7.6中管道大小是64kb。

在这里我们再次提出一个名词PIPE_BUF

​PIPE_BUF​​ 是一个常量,定义在 ​​<limits.h>​​ 头文件中。它表示在 POSIX 系统中管道(pipe)的原子写入操作的最大字节数。

​PIPE_BUF​​ 的值是系统特定的,表示了在一个原子写入操作中,保证不会被其他进程的写入操作中断或交叉写入的最大字节数。这个值通常是比较小的,例如在大多数 POSIX 系统中,它的值是 4096 字节。

当使用管道进行进程间通信时,写入操作可以分为多个写入调用,每次写入的字节数可能不同。但是,如果要确保多个写入操作不会被其他进程的写入操作中断或交叉写入,可以使用 ​​PIPE_BUF​​ 来限制每次写入的字节数。

请注意,即使写入的字节数小于 ​​PIPE_BUF​​,仍然可能发生写入操作被中断或交叉写入的情况。因此,对于需要确保原子写入的场景,需要使用其他机制来进行同步或加锁操作,以保证数据的完整性和一致性。

这里做了解即可,在后面的博客中我会详细的说明。

最后我们来看下一种情况。

也就是会告诉读端你已经读取完成了。需要做一下处理

那么最后一种情况还有其它很多的我还没说明的事情。我会在下一篇博客中写出。

写的不好请见谅,如果发现了任何错误欢迎指出。

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

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

相关文章

Redis 数据结构和常用命令

* 代表多个&#xff0c;&#xff1f;代表一个 &#xff08;不用全部敲出来&#xff0c;按住tab可以自动补全&#xff09; -2是无效&#xff0c;-1是永久有效 &#xff1b;贴心小提示&#xff1a;内存非常宝贵&#xff0c;对于一些数据&#xff0c;我们应当给他一些过期时间&a…

springboot 双数据源配置

1:pom <!--SpringBoot启动依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</group…

NNote插件:让网络阅读变得更高效,轻松同步至Notion笔记

NNote笔记 在这个互联网时代&#xff0c;我们每天都在浏览器中阅读大量的文章和资讯&#xff0c;时常会遇到让人眼前一亮的观点和想法。然而&#xff0c;当我们试图将这些精彩内容记录下来时&#xff0c;却常常感受到复制粘贴的繁琐。为了解决这个问题&#xff0c;NNote插件应运…

计算机网络物理层 习题答案及解析

2-1 下列选项中&#xff0c;不属于物理层接口规范定义范畴的是&#xff08; D &#xff09;。 A. 引脚功能 B. 接口形状 C. 信号电平 D. 传输媒体 【答案】D 【解析】 2-2 某网络在物理层规定&#xff0c;信号的电平范围为- 15V~15V &#xff0c; 电线长…

微信小程序开发系列-10组件间通信01

微信小程序开发系列目录 《微信小程序开发系列-01创建一个最小的小程序项目》 《微信小程序开发系列-02注册小程序》 《微信小程序开发系列-03全局配置中的“window”和“tabBar”》 《微信小程序开发系列-04获取用户图像和昵称》 《微信小程序开发系列-05登录小程序》 《…

java生产设备效率管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 java Web生产设备效率管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为ac…

kivy中的GridLayout

说明 GridLayout 是 Kivy 框架中的一个布局管理器&#xff0c;它允许你在网格中排列子控件。你可以指定网格的行数和列数&#xff0c;然后添加子控件到网格中。GridLayout 会自动调整子控件的位置和大小&#xff0c;以适应网格的单元格。 在 Kivy 框架中&#xff0c;size_hint…

动态内存分配函数

malloc void* malloc( unsigned size) 申请size个字节的地址连续的内存单元 成功则返回指向内存块的指针, 失败则返回NULL malloc不对申请的空间初始化 calloc void*calloc&#xff08;unsigned n&#xff0c;unsigmed size&#xff09; 申请n* size字节的个地址连续的内…

2024-01-01 服务器开发-11个最佳免费和便宜SSL证书颁发机构

摘要: 2024-01-01 服务器开发-11个最佳免费和便宜SSL证书颁发机构 ssl证书颁发机构 在网站上实施 SSL 证书不再被视为奢侈品。它不仅通过加密网站访问者与您的网站之间交换的通信来提高您的网站安全性&#xff0c;而且还提高了网站的 SEO 排名。此外&#xff0c;如果你托管的平…

深度学习核心技术与实践之计算机视觉篇

非书中全部内容&#xff0c;只是写了些自认为有收获的部分 计算机视觉背景 &#xff08;1&#xff09;视觉皮层的神经元是一列一列组织起来的&#xff0c;每一列神经元只喜欢某一种特定的形状或者某些简单的线条组合&#xff0c;而不是鱼、老鼠、鲜花 &#xff08;2&#xf…

HTTPS协议详解

目录 前言 一、HTTPS协议 1、加密是什么 2、为什么要加密 二、常见加密方式 1、对称加密 2、非对称加密 三、数据摘要与数据指纹 1、数据摘要 2、数据指纹 四、HTTPS加密策略探究 1、只使用对称加密 2、只使用非对称加密 3、双方都使用非对称加密 4、对称加密非…

如何使用Docker compose安装Spug并实现远程访问登录界面

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;网络奇遇记、Cpolar杂谈 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. Docker安装Spug二. 本地访问测试三. Linux 安装cpolar四. 配置Spug公网访问…

Git:常用命令(一)

取得项目的Git 仓库 从当前目录初始化 1 git init 初始化后&#xff0c;在当前目录下会出现一个名为.git 的目录&#xff0c;所有Git 需要的数据和资源都存放在这个目录中。不过目前&#xff0c;仅仅是按照既有的结构框架初始化好了里边所有的文件和目录&#xff0c;但我们还…

Python备忘录工具:创建自己的备忘录应用

更多Python学习内容&#xff1a;ipengtao.com 在日常生活和工作中&#xff0c;经常需要记录重要信息、任务清单和想法。为了更好地管理这些信息&#xff0c;可以使用Python创建一个备忘录工具。本文将介绍如何使用Python开发一个简单而功能强大的备忘录应用&#xff0c;以及提供…

Redis源码——压缩列表

压缩列表ziplist本质上就是一个字节数组&#xff0c;是Redis为了节约内存而设计的一种线性数据结构&#xff0c;可以包含多个元素&#xff0c;每个元素可以是一个字节数组或一个整数。Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。当有序集合或散列表的元素个数比…

使用 Hyper-V 创建虚拟机

使用 Hyper-V 创建虚拟机 官网教程修改存储目录Hyper-V管理器创建虚拟机启动虚拟机Win10安装教程Press any key to boot from CD or DVD...... 如何使用Windows自带的虚拟机工具来创建虚拟机&#xff0c; 快速创建虚拟机进行学习探讨&#xff0c;如果有环境问题可以立即创建一个…

牛客网SQL训练5—SQL大厂面试真题

文章目录 一、某音短视频1.各个视频的平均完播率2.平均播放进度大于60%的视频类别3.每类视频近一个月的转发量/率4.每个创作者每月的涨粉率及截止当前的总粉丝量5.国庆期间每类视频点赞量和转发量6.近一个月发布的视频中热度最高的top3视频 二、用户增长场景&#xff08;某度信…

【数学建模美赛M奖速成系列】Matplotlib绘图技巧(二)

Matplotlib绘图技巧&#xff08;二&#xff09; 写在前面2. 函数间区域填充函数fill_between()和fill()参数&#xff1a; 3. 散点图 scatter4. 直方图 hist5. 条形图 bar5.1 一个数据样本的条形图参数&#xff1a; 5.2 多个数据样本进行对比的直方图5.3 水平条形图参数 5.4 绘制…

c盘扩容时,d盘无法删除卷问题

C盘扩容时&#xff0c;磁盘管理中D盘右键无法删除卷的原因 首先&#xff0c;D盘下文件夹为空&#xff0c;但是显示可用空间不是100%&#xff0c;经过排查&#xff0c;发现是虚拟内存设置在了D盘导致无法删除卷&#xff0c;这里只需要将虚拟内存放到其他盘&#xff0c;如E盘即可…

2023年度学习总结

想想大一刚开始在CSDN写作&#xff0c;这一坚持&#xff0c;就是我在CSDN的第九个年头&#xff0c;这也是在CSDN最有里程碑的一年&#xff0c;这一年我被评为CSDN的博客专家啦&#xff01;先是被评为Unity开发领域新星创作者&#xff0c;写的关于一部分Unity开发的心得获得大家…