【Linux】第十八站:进程等待

文章目录

  • 一、进程等待的必要性
    • 1.进程等待是什么
    • 2.进程等待的必要性
    • 3.为什么要进程等待呢?
  • 二、进程等待的方法
    • 1.问题
    • 2.wait
    • 3.waitpid
    • 4.status的原理
    • 5.等待失败
    • 6.与status有关的两个宏
    • 7.options

一、进程等待的必要性

1.进程等待是什么

通过系统调用wait/waitpid,来进行对子进程状态检测与回收的功能!

2.进程等待的必要性

  • 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.为什么要进程等待呢?

答案已经在前面回答了

就是因为僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题----必须解决的

我们要通过进程等待,获得子进程的退出情况,要能够知道布置给子进程的任务,它完成的怎么样了----要么关心,也可能不关心

二、进程等待的方法

1.问题

我们使用如下代码

#include<unistd.h>    
#include<stdlib.h>    
#include<stdio.h>
int main()    
{    pid_t id = fork();    if(id < 0)                                                                {                                          perror("fork:");    return 1;    }         else if(id == 0)    {                     int cnt = 5;    while(cnt)    {                   printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);    cnt--;    sleep(1);    }    exit(0);        }                 else     {                                                                               while(1)      {    printf("I am parent,pid:%d,ppid:%d\n",getpid(),getppid());    sleep(1);    }    }    return 0;                                                                                                                                                                                  
}

上述代码的功能不难理解。然后我们使用监控去运行一下

while :; do ps ajx | head -1 && ps ajx | grep -v grep |grep waitTest; echo "---------------------------------"; sleep 1;done 

运行结果如下所示

image-20231116145041367

为了解决上面的问题,我们可以让父进程通过调用wait/waitpid进行僵尸进程的回收问题!

2.wait

我们先来看wait函数

image-20231116145824190

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);

返回值:

  • 成功返回被等待进程pid,失败返回-1。

参数:

  • 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

这个系统调用的作用就是等待一个进程,直到它的状态发生改变。

对于wait函数,我们可以看到它需要传入一个指针,但是在wait函数,我们先不关心它的这个参数。也就是我们先给他一个NULL指针

这个wait它会返回一个值,这个值就是对应等待的子进程的pid

#include<stdio.h>                                                                                           
#include<unistd.h>                                                                                      
#include<stdlib.h>                                                                    
#include<sys/types.h>                                                                           
#include<sys/wait.h>                                                                                    
int main()                                                           
{                                                                                        pid_t id = fork();                                                                  if(id < 0)                                                                   {                                                                        perror("fork:");                                                                       return 1;                                                                }                                                                 else if(id == 0)                                                                        {                                                                   int cnt = 5;                                                                  while(cnt)                                                                 {                                                             printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);           cnt--;                                                                 sleep(1);                                                                    }                                                                      exit(0);                                                                            }                                                                    else                                                                        {        int cnt = 10;                                                                             while(cnt)                                                                  {                                                                         printf("I am parent,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);                        cnt--;                                                                        sleep(1);                                                                         }                                                                                          pid_t ret = wait(NULL);                                                                         if(ret == id)                                                                                  {                                                                                        printf("wait success:,ret : %d\n",ret);                        }                                                                      sleep(5);                                                                                         }                                                                                                                                                      return 0;                                                
} 

最终它的运行结果为

程序一共经历了三个5秒。第一个五秒钟,是父子进程都存在,第二个五秒,子进程进入僵尸状态。第三个五秒,子进程的僵尸状态被回收了。

image-20231116151813720

所以说,到目前为止,进程等待是必须的。因为这个进程等待最重要的作用就是回收僵尸进程

那么如果我们这个程序有很多个子进程呢?应该等待哪一个呢?其实wait是等待任意一个子进程退出。

所以如果我们要等待多个进程退出,我们要这样做

如下代码所示

#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>#define N 10    void RunChild()    
{    int cnt = 5;    while(cnt)    {    printf("I am a child process, pid:%d, ppid:%d\n",getpid(),getppid());    sleep(1);    cnt--;    }    
}    
int main()    
{    for(int i = 0; i < N; i++)    {    pid_t id = fork();    if(id == 0)    {    RunChild();    exit(0);    }    printf("create child process: %d success\n",id);    }    sleep(10);    for(int i = 0; i < N; i++)    {    pid_t id = wait(NULL);    if(id > 0)    {    printf("wait %d success\n",id);                                                  }    }    sleep(5);return 0;    
}

运行结果如下

image-20231116162422040

image-20231116162355469

上面都是子进程会退出的情况,那么如果子进程都不退出呢?

即我们将上面的子进程都改为死循环

那么最终的运行结果是一个也不退出,父进程也不退出,在那里阻塞等待。

image-20231116163402097

所以这说明,如果子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不返回,默认就叫做阻塞状态!

所以说阻塞不仅仅只是像我们之前要等待scanf的时候,需要等待硬件,还有可能等待软件

3.waitpid

现在我们已经知道对于子进程的僵尸进程问题是如何解决的了,就是使用wait即可。这解决了进程等待中最为重要的一点,那么如果父进程需要获得子进程的退出时的状态,得知子进程任务完成的怎么样了,那么应该如何解决呢?

pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

  • pid:

Pid=-1,等待任一个子进程。与wait等效。

Pid>0.等待其进程ID与pid相等的子进程。

  • status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

  • options:
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

从这里的描述,我们就可以知道,wait其实就相当于waitpid的一个子集

即在这段代码中,wait与waitpid是等价的

image-20231116170410856

image-20231116170457366

在我们这个wait和waitpid的函数中,他们都有一个status,这个就是用来获取退出时的状态的。如果我们不想用,直接设置为空即可,这两个函数的这个参数是一样的

image-20231116170713686

对于这个status,它是一个输出型参数。其次,这个int是被当作几部分来使用的。

我们可以先来使用一下,为了展示出效果,我们让子进程的退出设置为1

image-20231116172438286

最终运行结果为

image-20231116172521576

我们可以发现,我们退出信息本应该是1,但是结果却是256

这里我们就有如下几个问题

  1. 子进程退出,一共会有几种退出场景呢?

对于这个问题,我们在前面已经知道了:总共三种场景

①:代码运行完毕,结果正确 ②:代码运行完毕,结果不正确 ③:代码异常终止

  1. 父进程等待,期望获得子进程退出的哪些信息呢?

①:子进程代码是否异常?②:没有异常的话,结果对吗?这里可以用exitcode退出码来获取,不对是什么原因呢?可以用不同的退出码表示原因

所以我们可以看到,这个status需要做的事情其实挺多的,所以这个变量注定了不能被看作一个,而是要划分为几个部分

image-20231116173351873

对于这个status它一共有32位,但是我们目前只考虑它的低16位

低八位,用于表示是否出异常了。其中有一共比特位是core dump标志位,我们后面再谈

我们在前面说过,一共进程异常了,本质就是该进程收到了一个信号。

对于它的次低八位,代表的就是退出的状态,即退出码

image-20231116180710223

比如我们刚刚所说的明明是退出码是1,但是打印结果是256,其实就是退出码的最低位被置为了1

image-20231116180806236

对于第七位,我么可以看到总共有64种信号,但是我们会发现没有0号信号,是因为0要代表正常终止。所以总共65种状态,就需要七位来表示。

image-20231116180943671

那么在这里我们可能会有一个问题,就是我们觉得可能没有必要这样做,因为完全可以设置一个全局变量,然后再子进程退出的时候,修改这个全局变量来处理状态就可以了,不需要用wait来处理?

其实这是因为,进程具有独立性

即便子进程中将这个全局变量给修改了,但是父进程也是看不到的。所以必须得通过wait系统调用,让操作系统去拿这个数据。

我们可以这样做,就可以分别拿出两种码了

image-20231116183217652

运行结果为

image-20231116183253354

如果我们的退出码是11

image-20231116183415758

那么运行结果的退出码就是11

image-20231116183449280

我们也可以模拟当他出现异常的时候

image-20231116183853947

可以看到,子进程第一次就发生了除0错误,直接进入了僵尸状态。

并且最终就是8号信号浮点数错误

image-20231116184002668

我们也可以让他进入死循环,然后我们使用kill去杀掉这个信号

image-20231116184951852

4.status的原理

在下图种,意思是,父进程再某行种调用了waitpid这个函数,cpu去调度父进程,当子进程执行完毕的时候。子进程为了保证自己的结果可以被上层获取,子进程可以允许把代码和数据释放掉,但是子进程对应的进程控制块不能被释放掉。我们需要将子进程退出时的信息放入进程控制块中

image-20231116185916779

所以子进程中一定有sigcode,exitcode

如下在linux内核中就可以看到这两个

image-20231116190251450

当他退出时,会将值写入exit_code中,当他异常终止时,会将信号写入exit_signal中。然后waitpid就可以读取这里面的数据了

waitpid一方面可以检测当前进程是否退出了,比如说z状态。是z状态,就直接读取这两个值,通过位运算,让上层拿到

image-20231116190447768

所以waitpid的核心工作就是读取子进程的task_struct,内核数据结构对象,并且将进程的Z状态改为X状态

那么为什么不让我们写代码时候直接访问子进程的pcb中呢,而是必须要通过系统调用呢?

我们必须要通过管理者拿到被管理者的数据,不能直接拿被管理者的数据,因为操作系统不相信任何人

5.等待失败

前面我们知道,wait/waitpid函数在等待失败的时候,会返回-1,那么什么时候会失败呢?

这里有一个很经典的场景,那就是等待的进程不存在或者等待的进程不是该进程的子进程

image-20231116201118094

image-20231116201149355

这就说明了,等待的进程必须是该进程的子进程

6.与status有关的两个宏

其实在我们的代码中,如果要让我们去写这两个的话是比较麻烦的

image-20231116201653437

所以linux提供了两个宏

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

我们可以这样用

image-20231116202146635

image-20231116202211322

我们可以在试一下多进程的

image-20231116203830467

运行结果为

image-20231116203932399

不过要注意的是,我们这个具有一定的偶然性,因为多进程的话不一定越早的就一定是最快的。取决于CPU的调度


所以现在我们就知道了前面所说的进程等待的原因之二了:我们要通过进程等待,获得子进程的退出情况,要能够知道布置给子进程的任务,它完成的怎么样了----要么关心,也可能不关心。我们也就知道了main函数的返回到底是什么了

就好比,当我们正在运行我们上面的代码的时候,bash也在等待这个进程退出。因为这个进程也是bash的子进程。

7.options

我们知道,wait本身会等待子进程,但是当子进程一直停不下来的时候,父进程只能等待,wait会导致父进程阻塞调用,导致父进程陷入阻塞状态。options可以指定等待方式。如果是0,则是阻塞等待方式(即父进程要等待子进程,子进程一直处于R,父进程就一直处于S,然后将父进程投入到等待队列中,投入到了子进程的等待队列。当子进程结束的时候,再从这个等待队列中将父进程唤醒)

我们在等待的时候,还可以选择另外一种等待方式:非阻塞轮询

pid_ t waitpid(pid_t pid, int *status, int options);

在这个函数中,如果options是0,那么就是阻塞等待方式

还有一种选项是WNOHANG (wait no hang, hang可以理解为夯住了,类似于服务器宕机了,意思是等待的时候不要夯住,也就是非阻塞)

类似于小明有事找小王

如果小明在楼下给小王隔一会就打一下电话,因为小王一直说忙着等会马上到。这就是非阻塞轮询(小明是在不打电话的时候是非阻塞的,而且是一直循环的打电话)

如果小明在楼下给小王打电话,然后不挂了,知道小明事情做完才挂电话,这就是阻塞等待(因为小明啥也干不了了)

如果小明在楼下给小王隔一会就打一下电话,在这中间的间隙做一些自己的事情,就是非阻塞轮询+做自己的事情

与之对应的就是pid_t的返回值其实应该有三种

大于0:等待成功,即返回子进程的pid

小于0:等待失败

等于0:等待的条件还没有就绪。

如下就是非阻塞轮询的示例

image-20231116212255400

运行结果如下所示:

注意在非阻塞轮询中,最好加上sleep,否则的话频繁的printf,可能会对系统压力比较大,导致卡住了。达不到预期的结果。


那么这里我们可能会疑惑父进程具体要具体做什么样子的工作?

我们可以用下面这个例子

#include<stdio.h>    
#include<unistd.h>    
#include<stdlib.h>    
#include<sys/types.h>    
#include<sys/wait.h>    #define TASK_NUM 10    
typedef void(*task_t)();    
task_t tasks[TASK_NUM];    
void task1()    
{    printf("这是一个执行打印日志的任务,pid:%d\n",getpid());    
}    
void task2()    
{    printf("这是一个执行检测网络健康状况的任务,pid:%d\n",getpid());    
}    
void task3()    
{    printf("这是一个绘制图形界面的任务,pid:%d\n",getpid());    
}    
void InitTask()    
{    for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;    
}    
int AddTask(task_t t)    
{    int pos = 0;    for(pos = 0; pos < TASK_NUM; pos++)    {    if(!tasks[pos]) break;    }    if(pos == TASK_NUM) return -1;                                                                                                             tasks[pos] = t;    return 0;    
}    
void DelTask()                                     
{}
void CheckTask()
{}
void UpdateTask()
{}
void ExecuteTask()
{for(int i = 0; i < TASK_NUM; i++){if(!tasks[i]) continue;tasks[i]();}
}
int main()
{pid_t id = fork();if(id < 0)                                     {perror("fork:");return 1;}else if(id == 0){int cnt = 5;while(cnt){printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);cnt--;sleep(1);}exit(11);}else{int status = 0;InitTask();AddTask(task1);AddTask(task2);AddTask(task3);while(1) //轮询{pid_t ret = waitpid(-1, &status,WNOHANG); //非阻塞if(ret > 0){if(WIFEXITED(status)){printf("进程是正常跑完的,退出码为:%d\n",WEXITSTATUS(status));}else {printf("进程出异常了\n");}break;}else if(ret < 0){printf("wait fail\n");break;}else {ExecuteTask();usleep(500000);//printf("子进程还没有退出,在等等\n");//sleep(1);}}sleep(3);}return 0;
}

运行结果为

image-20231117202347179

在这里我们需要注意的是,在这里,我们等待子进程才是最核心的任务,这些其他的任务都是一些顺带的事情。

这些顺带的任务不可以太重了,应该得是轻量化的任务。比如打印日志,检测网络状况等。而且在这里,我们这里也只是延迟回收了一会子进程,而不是说不回收子进程了。

在上面的代码中,我们只是单进程的等待任务,如果我们想要改为多进程的等待任务的话,那么我们只需要将这里稍作修改即可,不让他直接break,而是设置一个计数器,计数子进程的个数,当一个子进程结束后,计数器减减即可。只有减到0以后,才是break。还有就是在出错的时候,也break即可

image-20231117203625030

最后一点需要注意的是

waitpid在回收子进程的时候,它可以保证父进程一定是最后一个被回收的。因为子进程可以全部被waitpid给回收掉。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微服务基础,分布式核心,常见微服务框架,SpringCloud概述,搭建SpringCloud微服务项目详细步骤,含源代码

微服务基础 系统架构的演变 随着会联网的发展&#xff0c;网站应用的规模不断扩大&#xff0c;常规的应用架构已经无法应对&#xff0c;分布式服务架构以及微服务架构势在必行&#xff0c;必须一个治理系统确保架构有条不紊的演进 单体应用框架 Web应用程序发展的早期&…

PyCharm 远程连接服务器并使用服务器的 Jupyter 环境

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

我所理解的 UI Toolkit 启蒙阶段(一)

我所理解的 UI Toolkit 启蒙阶段&#xff08;一&#xff09; 对于自己不会的新东西的学习&#xff0c;我认为最合适的路径就是&#xff1a; 实例教学视频 —> 实操熟悉 —> 官方文档查漏补缺 —> 拟定思路实现功能 但这 4 步并非每一步都需要下 100% 的功夫&#x…

虚拟机上安装docker,并安装flink镜像

1. 安装docker 官网步骤&#xff1a;https://docs.docker.com/engine/install/centos/ sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install docker-ce docker-ce-cli containerd.…

2024年山东省职业院校技能大赛中职组“网络安全”赛项竞赛试题-A

2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-A 一、竞赛时间 总计&#xff1a;360分钟 二、竞赛阶段 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 A、B模块 A-1 登录安全加固 180分钟 200分 A-2 本地安全策略设置 A-3 流量完整性保护 A-4 …

【整顿C盘】pycharm、chrome等软件,缓存移动

C盘爆了&#xff0c;特来找一下巨大的软件缓存&#xff0c;特此记录&#xff0c;跟随的各大教程&#xff0c;和自己的体会 一、爆炸家族JetBrains 这个适用于pycharm、idea、webstorm等等&#xff0c;只要是JetBrains家的&#xff0c;2020版本以上&#xff0c;都是一样的方法 p…

Notepad++ 通过HexEditor插件查看.hprof文件、heap dump文件的堆转储数据

文章目录 需求场景插件安装查看notepad的版本&#xff0c;看看是32位的还是64位的下载对应的版本解压导入插件打开notepad插件文件夹&#xff1a;Notepad安装目录新建一个HexEditor文件夹选中插件文件导入 重启notepad使用 需求场景 想要查看app内存的某个域的数据。 利用Andr…

基于蛾群算法优化概率神经网络PNN的分类预测 - 附代码

基于蛾群算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于蛾群算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于蛾群优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络的光滑…

03.webpack中hash,chunkhash和contenthash 的区别

hash、contenthash 和 chunkhash 是通过散列函数处理之后&#xff0c;生成的一串字符&#xff0c;可用于区分文件。 作用&#xff1a;善用文件的哈希值&#xff0c;解决浏览器缓存导致的资源未及时更新的问题 1.文件名不带哈希值 const path require(path) const HtmlWebpac…

SpringBoot 2.x 实战仿B站高性能后端项目

SpringBoot 2.x 实战仿B站高性能后端项目 下栽の地止&#xff1a;请看文章末尾 通常SpringBoot新建项目&#xff0c;默认是集成了Maven&#xff0c;然后所有内容都在一个主模块中。 如果项目架构稍微复杂一点&#xff0c;就需要用到Maven多模块。 本文简单概述一下&#xff0c…

通过maven命令手动上传jar私服Nexus

Nexus3在界面上传组件时报&#xff1a; Ext.JSON.decode(): Youre trying to decode an invalid JSON String: 查找了很多资料&#xff0c;都没有解决。有哪位大佬知道的评论告诉一下&#xff0c;万分感谢。 于是换成maven命令上传&#xff1a; mvn deploy:deploy-file -Dgr…

【C++】类和对象(5)--运算符重载

目录 一 概念 二 运算符重载的实现 三 关于时间的所有运算符重载 四 默认赋值运算符 五 const取地址操作符重载 一 概念 C为了增强代码的可读性引入了运算符重载&#xff0c;运算符重载是具有特殊函数名的函数&#xff0c;也具有其返回值类型&#xff0c;函数名字以及参数…

UE5 C++报错:is not currently enabled for Live Coding

解决办法&#xff1a; 再次打开项目&#xff0c;以此法打开&#xff1a;

ToolJet:开源低代码框架,轻松构建复杂可响应界面 | 开源日报 No.78

ToolJet/ToolJet Stars: 25.0k License: AGPL-3.0 ToolJet 是一个开源的低代码框架&#xff0c;可以通过最小化工程投入来构建和部署内部工具。ToolJet 的拖放式前端构建器允许您在几分钟内创建复杂、响应式的前端界面。此外&#xff0c;您还可以集成各种数据源&#xff0c;包…

【Linux网络编程】高级I/O

目录 五种I/O模型 阻塞和非阻塞 非阻塞I/O I/O多路复用之Select、Poll、与Epoll 本文目的是深入浅出理解高级I/O相关的知识&#xff0c;结尾附上代码加深理解相关知识。 五种I/O模型 1.阻塞I/O&#xff1a;在内核将数据准备好之前&#xff0c;系统调用会一直等待。所有的套…

设计模式(5)-使用设计模式实现简易版springIoc

自定义简易版springIoc 1 spring使用回顾 自定义spring框架前&#xff0c;先回顾一下spring框架的使用&#xff0c;从而分析spring的核心&#xff0c;并对核心功能进行模拟。 数据访问层。定义UserDao接口及其子实现类 public interface UserDao {public void add(); }public…

基于静电放电算法优化概率神经网络PNN的分类预测 - 附代码

基于静电放电算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于静电放电算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于静电放电优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

微信个人号api

简要描述&#xff1a; 登录E云平台 请求URL&#xff1a; http://域名地址/member/login域名地址开发者账号密码:后台系统自助开通 请求方式&#xff1a; POST 请求头Headers&#xff1a; Content-Type&#xff1a;application/json 参数&#xff1a; 参数名必选类型说…

​如何使用ArcGIS Pro制作渐变河流效果

对于面要素的河流水系&#xff0c;制作渐变效果方法比较简单&#xff0c;如果是线要素的河流有办法制作渐变效果吗&#xff0c;答案是肯定的&#xff0c;这里为大家介绍一下制作方法&#xff0c;希望能对你有所帮助。 数据来源 本教程所使用的数据是从水经微图中下载的水系数…

《网络协议》07. 其他协议

title: 《网络协议》07. 其他协议 date: 2022-10-07 18:24:02 updated: 2023-11-15 08:00:52 categories: 学习记录&#xff1a;网络协议 excerpt: IPv6、WebSocket、WebService&#xff08;SOAP&#xff0c;WSDL&#xff09;、HTTPDNS、FTP、邮件&#xff08;SMTP&#xff0c;…