【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给回收掉。

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

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

相关文章

在listener.ora配置文件中配置listener 1527的监听并且使用tnsnames连接测试

文章目录 前言&#xff1a;一、命令语句实现1、监听介绍2、编辑 listener.ora 文件&#xff1a;寻找配置文件对配置文件进行配置 3、重启监听4、配置TNS 二、图形化界面实现1、listener.ora文件配置2、tnsnames.ora文件配置 三、测试连接 前言&#xff1a; 命令实现和图形化实…

某60区块链安全之51%攻击实战学习记录

区块链安全 文章目录 区块链安全51%攻击实战实验目的实验环境实验工具实验原理攻击过程 51%攻击实战 实验目的 1.理解并掌握区块链基本概念及区块链原理 2.理解区块链分又问题 3.理解掌握区块链51%算力攻击原理与利用 4.找到题目漏洞进行分析并形成利用 实验环境 1.Ubuntu1…

Neo4j安装(Docker中安装Neo4j)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

Android File Transfer(安卓文件传输工具)

Android File Transfer 是一款安卓文件传输工&#xff0c;它允许在Mac操作系统和Android设备之间进行文件传输。 该软件通过USB连接将文件从Mac电脑传输到连接的Android设备&#xff0c;或者反过来从Android设备传输文件到Mac电脑。这包括照片、视频、音乐、文档和其他文件类型…

搞定这套Python爬虫面试题,大厂Offer拿到手软

文章目录 1、简述Python 的特点和优点2、Python 有哪些数据类型&#xff1f;3、列表和元组的区别4、Python 是如何运行的5、Python 运行速度慢的原因6、面对 Python 慢的问题&#xff0c;有什么解决办法7、描述一下全局解释器锁 GIL8、深拷贝 浅拷贝9、is 和 的区别10、文件读…

python 随机数生成

生成随机整数 使用 randint() 函数可以生成指定范围内的随机整数。 import random # 生成1到10之间的随机整数 random_int random.randint(1, 10) print(random_int) 生成随机浮点数 random() 方法用于生成 0 到 1 之间的随机浮点数。 import random # 生成0到1之间…

【性能测试】Jenkins+Ant+Jmeter自动化框架的搭建思路

前言 前面讲了Jmeter在性能测试中的应用及扩展。随着测试的深入&#xff0c;我们发现在性能测试中也会遇到不少的重复工作。 比如某新兴业务处于上升阶段&#xff0c;需要在每个版本中&#xff0c;对某些新增接口进行性能测试&#xff0c;有时还需要在一天中的不同时段分别进行…

Fe-safe 2023 新功能介绍

Fe-safe的功能增强 3DEXPERIENCE中的更新 疲劳耐久性分析界面集成到结构和力学分析App的UX和数据模型当中&#xff0c;支持在同一个App中和同一个仿真对象中定义分析步、相互作用、边界条件、载荷以及耐久性特征。 增强了焊接疲劳功能&#xff0c;在22xGA中集成了Verity的线…

geoserver的ECQL查询

ECQL Reference — GeoServer 2.24.x User Manual CQL and ECQL — GeoServer 2.24.x User Manual ECQL是CQL的扩展&#xff0c;类似sql查询&#xff0c;比ogc的xml格式简单&#xff0c;可以应用在wfs和wms查询上。 通过可视化页面查看过滤效果&#xff0c;默认视图 主键不会…

23111705[含文档+PPT+源码等]计算机毕业设计SSM框架美妆商城全套电商购物

文章目录 **软件开发环境及开发工具&#xff1a;****项目功能介绍&#xff1a;****论文截图&#xff1a;****实现&#xff1a;****代码片段&#xff1a;** 编程技术交流、源码分享、模板分享、网课教程 &#x1f427;裙&#xff1a;776871563 软件开发环境及开发工具&#xff…

C++——map和set

作者&#xff1a;几冬雪来 时间&#xff1a;2023年11月17日 内容&#xff1a;C板块map和set知识讲解 目录 前言&#xff1a; map与set与关联式容器&#xff1a; set底层&#xff1a; set的书写&#xff1a; insert&#xff1a; erase&#xff1a; lower_bound与upper_b…

【139.单词拆分】

目录 一、题目解析二、算法原理三、代码实现 一、题目解析 二、算法原理 三、代码实现 class Solution { public:bool wordBreak(string s, vector<string>& wordDict) {int n s.size();unordered_set<string> hash;for (auto x : wordDict) hash.insert(x);…

目前比较好用的护眼台灯?小白入门最适合的护眼台灯推荐

随着人们对家庭环境艺术的重视&#xff0c;台灯因其摆设在桌案台几上的特殊地位&#xff0c;也要进求特有的装饰效果。家居用台灯开始逐新分流为工艺台灯和书写台灯两类。前者追求外观效果&#xff0c;将发展思路放在材质的创新、造型的求异上&#xff0c;以配合风格多样的家居…

蓝牙耳机仓设计的单芯片解决方案

对于一款优秀的TWS耳机来说&#xff0c;除了耳机本身的音频配置&#xff0c;充电仓也是极为重要的一环。因为与传统有线耳机由设备电池供电不同&#xff0c;缺少了耳机仓&#xff0c;TWS耳机就完全的失去了充电的途径&#xff0c;设备在耗尽电量基本就告别使用了&#xff0c;因…

【GUI】-- 08 JButton、JRadioButton、JCheckBox

GUI编程 03 Swing 3.5 JButton 图片置于按钮之上的JButton&#xff1a; package com.duo.lesson05;import javax.swing.*; import java.awt.*; import java.net.URL;public class JButtonDemo01 extends JFrame {public JButtonDemo01() {Container contentPane getConten…

【Spring】使用三方包进行数据源对象(数据库)管理

在这里使用alibaba的druid来连接数据库&#xff0c;然后再Spring Config下配置数据库 目录 第一步&#xff1a;在pom.xml中导入坐标第二步&#xff1a;在bean中配置连接注 第一步&#xff1a;在pom.xml中导入坐标 在dependencies下写&#xff1a; <dependency><grou…

wpf devexpress显示总结

这个教程示范如何显示总结对于列分组和单个数据行。这个教程基于前一篇 GridControl 可以计算如下总结&#xff1a; 这个数据列&#xff08;Count&#xff09; 这个最大和最小值&#xff08;Max和Min&#xff09;。 总结和平均值&#xff08;Sum和平均值&#xff09; 自定义…

卡方检验-python代码

故事背景 问题 卡方检验的结果怎么计算&#xff1f; 方法 python代码 import numpy as np from scipy.stats import chi2_contingency# 观察频数矩阵 observed np.array([[47, 21, 17],[63, 29, 15],[11, 2, 4]])# 进行卡方检验 chi2, p, dof, expected chi2_contingency(o…

map和set

文章目录 关联式容器pair树形结构的关联式容器setmultisetmapmultimap 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff0c; 通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。 点击跳转到网站。 关联式容器 我们前…

使用drawio的图层构建更强大的图表

drawio中使用图层 drawio是一款强大的图表绘制软件&#xff0c;支持在线云端版本以及windows, macOS, linux安装版。 如果想在线直接使用&#xff0c;则直接输入网址draw.io或者使用drawon(桌案), drawon.cnhttps://www.drawon.cn?useSourcecsdn内部完整的集成了drawio的所有功…