利用管道、信号量、信号、共享内存和消息队列进行多进程通信

news2025/1/17 22:06:21

一.管道(分为命名管道和匿名管道)

管道的特点:

①无论是命名管道还是匿名管道,写入管道的数据都存放在内存之中。

②管道是一种半双工的通信方式(半双工是指终端A能发信号给终端B,终端B也能发信号给终端A,但这两个过程不能同时进行)

③命名管道和匿名管道的区别:命名管道可以在任意进程间使用,匿名管道主要在父子进程间使用。

命名管道 

int mkfifo( const char *filename, mode_t mode);//filename 是管道名 mode 是创建的文件访问权限

 实践

打开两个独立的终端窗口,分别运行两个进程。

//终端1   进程a.c 创建管道 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
 {
    const char *fifoPath = "./myfifo";//'./'是指在当前目录下创建
    mode_t fifoMode = 0600;//第一个0代表10进制,第二个6代表用户有读写权限,后面两个0代表组用户和其他用户无权限

    // 使用 mkfifo 创建命名管道
    if (mkfifo(fifoPath, fifoMode) == -1) 
    {
        printf("mkfifo error\n");
        exit(1);
    }

    printf("Named pipe created at %s\n", fifoPath);
   
    exit(0);
}

先运行终端1,其运行结果如图: 

//终端2    进程b.c   读取数据
#include <stdio.h>
#include<stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main() 
{
    int fd = open("./myfifo", O_RDONLY);
    char buffer[20];
    read(fd, buffer, sizeof(buffer));
    close(fd);

    printf("Received: %s\n", buffer);

    exit(0);
}

由于程序是以阻塞模式打开 命名管道并尝试读取数据。这意味着如果没有其他进程写入数据到该管道,它会一直等待,直到有数据可读为止。所以b.c运行结果如图:

 再次打开终端1,并在其中写入如下程序:

//终端1 进程c.c  写入数据
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<string.h>

int main()
{
    int fd = open("./myfifo", O_WRONLY);
    const char *message = "Hello, Process B!";
    write(fd, message, strlen(message) + 1);
    close(fd);

    exit(0);
}

当运行c.c后,终端2中b.c的运行结果如图:

匿名管道

int pipe( int fds[2]);//pipe()成功返回 0,失败返回-1;fds[0]是管道读端的描述符;fds[1]是管道写端的描述符。

实践

用fork()创建子进程,父进程写入数据,子进程读取并输出数据。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<string.h>
#include <sys/types.h>


int main() 
{
    int fd[2];
    char buffer[100];
    int res = pipe(fd); // 创建管道

    pid_t pid = fork(); // 创建子进程

    if (res == -1) 
    {
        printf("pipe failed\n");
        exit(1);
    }

    if (pid == -1) 
    {
        printf("fork failed\n");
        exit(1);
    }

    if (pid == 0) // 子进程
    {
        //sleep(1);
        close(fd[1]); // 关闭写入端
        read(fd[0], buffer, sizeof(buffer));
        //fflush(stdout);
        printf("Child received: %s\n", buffer);
    } 

    else // 父进程
    {  
        close(fd[0]);  // 关闭读取端
        const char *message = "Hello, Child!";
        write(fd[1], message, strlen(message) + 1);
    }

    close(fd[0]);
    close(fd[1]);
    exit(0);
}

二.信号

kill()

int kill(pid_t pid, int sig);//用于向指定的进程发送信号。

//pid是要发送信号的目标进程的进程编号。

//sig是要发送的信号的编号。

实践:

打开两个独立的终端窗口,分别运行两个进程

//终端1 send.cpp

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() 
{
    pid_t receiver_pid;  // 需要知道接收方进程的pid

    // 获取接收方进程的PID
    printf("输入接收进程的pid: ");
    scanf("%d", &receiver_pid);

    // 发送自定义信号到接收方进程
    if (kill(receiver_pid, SIGUSR1) == 0) 
    {
        printf(" 发送给进程号为:%d的进程\n", receiver_pid);
    } 
    else 
    {
        perror("信号量发送失败");
    }

    return 0;
}
//终端2 recv.cpp

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// 信号处理函数
void signal_handler(int signum) 
{
    if (signum == SIGUSR1) 
    {
        printf("自定义信号量收到\n");
    }
}

int main() 
{
    // 约定信号
    signal(SIGUSR1, signal_handler);

    printf("接收进程的pid为: %d\n", getpid());

    while (1) 
    {
        // 等待信号的到来
        sleep(1);
    }

    return 0;
}

运行结果: 

三.信号量

信号量是一个特殊的变量,一般取正数值。它的值代表允许访问的资源数目。

p操作--原子减一--代表获取资源--可能阻塞(当信号量值为0,即无资源可用时,会阻塞)

v操作--原子加一--代表释放资源--不会阻塞

semget()

int semget(key_t key, int nsems, int semflg);//创建一个新信号量或取得一个已有信号量的键。

//key,算是一个标识符,拥有相同key值的进程可以进行通讯

//nesms,通常取值为1

//semflg,通常是IPC_CREAT,不过使用IPC_CREAT时要加权限,如0666

//成功返回一个正数,失败返回-1

semctl()

int semctl(int semid, int semnum, int cmd, ... );//控制信号量信息。

//semid,由semget()返回

//semnum,一般取0

//cmd,用于指定要进行的操作,如:GETVAL(获取信号量的值) 、SETVAL(设置信号量的值),                                                                IPC_RMID(删除信号量)

//...,也许会用到这个参数,它是union semun类型的参数,用于传递额外信息

union semun
{
    int              val;  //用于设置或获取单个信号量的值
    struct semid_ds *buf;  //用于传递IPC_STAT或IPC_SET命令的参数
    unsigned short  *array;//用于传递GETALL或SETALL命令的参数
};

实践:

用信号量和两个进程来模拟打印机:进程A输出'a',代表开始使用,再次输出'a'代表结束使用,进程B输出'b'代表开始使用,再次输出'b'代表结束使用,所以预期结果输出为aabbaabb......

/*sem.h文件*/

//封装P、V操作
#include<iostream>
#include<unistd.h>
#include<string.h>
#include<sys/sem.h>

using namespace std;

union semnum
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

void sem_init();   //创建信号量
void sem_p();      //p操作
void sem_v();      //v操作
void sem_destroy();//销毁信号量
/*sem.cpp文件*/

#include"sem.h"

static int semid = -1;//初始化信号量

void sem_init()
{
    semid = semget((key_t)1234, 1, IPC_CREAT | 0600);

    union semnum a;
    a.val = 1;//信号量的初始值
    if(semctl(semid,0,SETVAL,a)==-1)
    {
        cout << "semctl failed" << endl;
    }
}

void sem_p()
{
    struct sembuf buf;

    // 信号量的标志,通常使用 IPC_NOWAIT 或 SEM_UNDO
    buf.sem_flg = SEM_UNDO;

    // 信号量的编号,如果有多个信号量,可以使用不同的编号进行区分
    buf.sem_num = 0;
    buf.sem_op = -1;//p操作
    if(semop(semid,&buf,1)==-1)
    {
        cout << "semop falied" << endl;
    }
}

void sem_v()
{
    struct sembuf buf;
    buf.sem_flg = SEM_UNDO;
    buf.sem_num = 0;
    buf.sem_op = 1;//v操作
    if(semop(semid,&buf,1)==-1)
    {
        cout << "semop falied" << endl;
    }
}

void sem_destroy()
{
    if(semctl(semid,0,IPC_RMID)==-1)
    {
        cout << "semctl move failed" << endl;
    }
}
/*a.cpp文件*/

#include"sem.h"

int main()
{
    sem_init();

    for (int i = 0; i < 5;i++)
    {
        sem_p();

        cout << "a";
        fflush(stdout);
        sleep(5);
        cout << "a";
        fflush(stdout);

        sem_v();
        sleep(5);
    }

    sleep(10);//等待另一个进程完成
    sem_destroy();

    return 0;
}
/*b.cpp文件*/

#include"sem.h"

int main()
{
    sem_init();

    for (int i = 0; i < 5;i++)
    {
        sem_p();

        cout << "b";
        fflush(stdout);
        sleep(5);
        cout << "b";
        fflush(stdout);

        sem_v();
        sleep(5);
    }

    return 0;
}

另外两个重要的概念

临界资源:同一时刻,只允许被一个进程或线程访问的资源
临界区:访问临界资源的代码段

四.共享内存

共享内存是先在物理内存上申请一块空间,多个进程可以将其映射到自己的虚拟地址空间中。这是是一种用于在不同进程之间共享数据的机制,它允许多个进程访问同一块内存区域,从而实现高效的进程间通信。

shmget()

int shmget(key_t key, size_t size, int shmflg); //创建或获取共享内存段,并返回一个共享内存标识符,通常称为 shmid。

//key:共享内存标识键值

//size:指定要创建的共享内存段的大小

//shmflg:用于指定创建共享内存的权限和行为,通常是IPC_CREAT(表示如果指定的 IPC 资源不存在,则创建它,如果已经存在则忽略)或者IPC_EXCL(表示只创建 IPC 资源,如果资源已经存在,创建操作将失败)

shmat()

void *shmat(int shmid, const void *shmaddr, int shmflg);//用于将共享内存附加到进程的地址空间,以便进程可以访问共享内存中的数据。

//shmid:是由 shmget 函数创建的共享内存的标识符

//shmaddr:通常设置为 NULL

//shmflg:通常设置为0

shmdt()

int shmdt(const void *shmaddr);//用于将共享内存从进程的地址空间中分离,使得该进程不能再访问共享内存中的数据。

//shmaddr:是共享内存的地址,通常是在使用 shmat 函数时获得的指针。

shmctl()

int shmctl(int shmid, int cmd, struct shmid_ds *buf);//用于获取关于共享内存段的信息、修改权限、删除共享内存段等。

//shmid:使用 shmget 函数获得。

//cmd:指定要执行的操作,例如获取信息(IPC_STAT)、修改权限(IPC_SET)、删除共享内存段(IPC_RMID)等。

//buf:用于传递或接收有关共享内存段的信息。如果不需要传递或接收信息,可以将其设置为 NULL。

实践

write进程和read进程通过共享内存分别来写入数据和读取数据。

/*shm.h文件*/

//封装P、V操作
#include<iostream>
#include<unistd.h>
#include<string.h>
#include<sys/sem.h>

using namespace std;

//创建两个信号量
enum INDEX
{
    SEM1=0,
    SEM2
};

union semnum
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

void sem_init();   //创建信号量
void sem_p(enum INDEX i);      //p操作
void sem_v(enum INDEX i);      //v操作
void sem_destroy();//销毁信号量
/*shm.cpp文件*/
#include "shm.h"

static int semid = -1; // 初始化信号量

void sem_init()
{
    semid = semget((key_t)1234, 2, IPC_CREAT | 0600); // 创建2个信号量

    int arr[2] = {1, 0};
    union semnum a;
    for (int i = 0; i < 2; i++)
    {
        a.val = arr[i]; // 信号量的初始值
        if (semctl(semid, i, SETVAL, a) == -1)
        {
            cout << "semctl failed" << endl;
        }
    }
}

void sem_p(enum INDEX i)
{
    struct sembuf buf;
    buf.sem_flg = SEM_UNDO;
    buf.sem_num = i;
    buf.sem_op = -1; // p操作
    if (semop(semid, &buf, 1) == -1)
    {
        cout << "semop falied" << endl;
    }
}

void sem_v(enum INDEX i)
{
    struct sembuf buf;
    buf.sem_flg = SEM_UNDO;
    buf.sem_num = i;
    buf.sem_op = 1; // v操作
    if (semop(semid, &buf, 1) == -1)
    {
        cout << "semop falied" << endl;
    }
}

void sem_destroy()
{
    if (semctl(semid, 0, IPC_RMID) == -1)//销毁所有信号量
    {
        cout << "semctl move failed" << endl;
    }
}
/*write.cpp文件*/
#include"shm.h"
#include <sys/shm.h>

int main()
{
    // 创建共享内存
    int shmid = shmget((key_t)1234, 128, IPC_CREAT | 0600);
    if (shmid == -1)
    {
        cout << "write shget filed" << endl;
        exit(1);
    }

    // 将共享内存映射到当前的地址空间
    char *s = (char *)shmat(shmid, NULL, 0);
    if (s == (char *)-1)
    {
        cout << "write shmat failed" << endl;
        exit(1);
    }

    sem_init();
    // 往共享内存中写入数据
    while (1)
    {
        cout << "请输入内容:" << endl;
        char buff[128] = {0};
        cin >> buff;

        sem_p(SEM1);
        strcpy(s, buff);
        sem_v(SEM2);

        if (strncmp(buff, "end", 3) == 0)
        {
            break;
        }
    }

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

    return 0;
}
/*read.cpp文件*/
#include"shm.h"
#include <sys/shm.h>

int main()
{
    // 创建共享内存
    int shmid = shmget((key_t)1234, 128, IPC_CREAT | 0600);
    if (shmid == -1)
    {
        cout << "read shget filed" << endl;
        exit(1);
    }

    // 将共享内存映射到当前的地址空间
    char *s = (char *)shmat(shmid, NULL, 0);
    if (s == (char *)-1)
    {
        cout << "read shmat failed" << endl;
        exit(1);
    }

    sem_init();
    // 从共享内存中读取数据
    while (1)
    {
        sem_p(SEM2);
        if (strncmp(s, "end", 3) == 0)
        {
            break;
        }

        cout << s << endl;
        sem_v(SEM1);
    }

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

    //销毁共享内存
    shmctl(shmid, IPC_RMID, NULL);

    //销毁信号量
    sem_destroy();

    return 0;
}

 运行结果:

五.消息队列

msgget()

int msgget(key_t key, int msgflg);//创建消息队列

//key,键值

//msgflg,用于指定消息队列的创建方式和权限

msgctl()

int msgctl(int msqid, int cmd, struct msqid_ds *buf);//控制消息队列
//msqid,消息队列的标识符,由msgget()返回

//cmd,操作命令

//*buf,消息队列信息的结构体指针

msgrcv()

int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);//从消息队列中接收消息

//msgid,消息队列的标识符,由msgget()返回

//*msgp,消息缓冲区的指针,数据将被复制到这个缓冲区

//msgsz,缓冲区的大小,以字节为单位

//msgtyp,接收消息的类型,如果设为0,则不区分类型

//msgflg,控制信息接收的行为

msgsnd()

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);//用于向消息队列中发送信息

//msgid,消息队列的标识符,由msgget()返回

//*msgp,消息缓冲区的指针,其中包含要发送的数据

//msgsz,缓冲区的大小,以字节为单位

//msgflg,控制信息接收的行为

/*msga.cpp文件*/
#include<iostream>
#include<unistd.h>
#include<string.h>
#include<sys/msg.h>

using namespace std;

// 定义消息结构
struct message 
{
    long type;//必须为长整形
    char buff[32];
};

int main()
{
    int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
    if(msgid==-1)
    {
        cout << "msgget failed" << endl;
        exit(1);
    }

    struct message dt;
    dt.type = 1;//消息类型为1
    strcpy(dt.buff, "hello");

    msgsnd(msgid, &dt, 32, 0);

    return 0;
}
/*msgb.cpp文件*/
#include<iostream>
#include<unistd.h>
#include<string.h>
#include<sys/msg.h>

using namespace std;

// 定义消息结构
struct message 
{
    long type;//必须为长整形
    char buff[32];
};

int main()
{
    int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
    if(msgid==-1)
    {
        cout << "msgb.cpp msgget failed" << endl;
        exit(0);
    }

    struct message dt;
    msgrcv(msgid, &dt, 32, 1, 0);
    cout << "接收到内容为:" << dt.buff << "的消息" << endl;

    return 0;
}

运行结果:

 入上图:运行了2此msga.cpp,消息队列中有2条消息,当运行msgb.cpp之后,消息队列剩余消息如下:

六.socket

此部分详见文章用c语言编写简单的一对一服务器和多对一服务器。

 

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

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

相关文章

ISIS默认路由下发的各种机制

作者简介&#xff1a;大家好&#xff0c;我是Asshebaby&#xff0c;热爱网工&#xff0c;有网络方面不懂的可以加我一起探讨 :1125069544 个人主页&#xff1a;Asshebaby博客 当前专栏&#xff1a; 网络HCIP内容 特色专栏&#xff1a; 常见的项目配置 本文内容&am…

VC++使用GetProcessTimes获取进程创建时间、销毁时间、用户态时间、内核态时间

一、GetProcessTimes函数简介&#xff08;微软MSDN&#xff09; 微软提供了一个非常有用的API函数GetProcessTimes用来获取进程创建时间、销毁时间、用户态时间、内核态时间&#xff0c;msdn连接为&#xff1a;GetProcessTimes 函数 (processthreadsapi.h) 其函数原型为&#…

网络管理相关

管理功能分为管理站manager和代理agent两部分。 网络管理&#xff1a; 网络管理系统中&#xff0c;每一个网络节点都包含有一组与管理有关的软件&#xff0c;叫做网络管理实体NME。 管理站的另外一组软件叫做网络管理应用NMA&#xff0c;提供用户接口&#xff0c;根据用户命令显…

渗透测试(Lab1.0)

1、用搜索引擎Google或百度搜索麻省理工学院网站中文件名包含“network security”的pdf文档&#xff0c;截图搜索得到的页面。 2、照片中的女生在哪里旅行&#xff1f; 截图搜索到的地址信息。 搜索餐厅的名字&#xff0c;找一下关键词 3、手机位置定位 通过LAC&#xff0…

【Ambari】Python调用Rest API 获取YARN HA状态信息并发送钉钉告警

&#x1f984; 个人主页——&#x1f390;开着拖拉机回家_Linux,大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&am…

Javascript 函数介绍

Javascript 函数介绍 很多教程书一上来就讲解一堆语法&#xff0c;例如函数定义、函数调用什么。等读者看完了函数这一章都没搞懂什么是函数。 在讲解什么叫函数之前&#xff0c;我们先看下面一段代码&#xff1a; <!DOCTYPE html> <html xmlns"http://www.w3.…

介绍java spring 提供的默认数据库持久化技术 JdbcTemplate基本演示

之前 我们说过spring贴心的内嵌了三种数据源形式 其中默认为HikariCP 其实 spring 也提供了持久化数据库连接技术 这个技术其实大部分都接触过 那就是 JDBC 随着时代的发展 用他的人也越来越少了 那么 我们要演示 JdbcTemplate 导入 mybatis 或 mybatis-plus 的片段 就要注掉了…

nacos服务的分级存储

举例说明 一个服务可以有多个实例&#xff0c;比如我们当前有4个实例&#xff0c;都叫做nacos-user-service服务 ip地址端口服务器所属地区集群192.168.xxx.xxx18080广东GD192.168.xxx.xxx18081广东GD192.168.xxx.xxx18082广西GX192.168.xxx.xxx18083广西GX所以我们可以将nacos…

跨境电商独立站怎么获取流量 跨境电商引流的两个方法

独立站有着比SaaS和中心化站点不一样的特点&#xff0c;今天介绍两种引流的主要玩法&#xff1a; 独立站的第一种玩法是通过数据&#xff0c;找到爆款&#xff0c;搭建独立站&#xff0c;企业站加电商功能、交易、订单配送功能&#xff0c;搜索引擎seo引流法&#xff0c;外联渠…

服务器RAID系统的常见故障,结合应用场景谈谈常规的维修处理流程

常见的服务器RAID系统故障包括硬盘故障、控制器故障、电源故障、写入错误和热插拔错误。下面结合这些故障的应用场景和常规维修处理流程来详细讨论&#xff1a; 硬盘故障&#xff1a; 应用场景&#xff1a;在服务器RAID系统中&#xff0c;硬盘故障是最常见的问题之一。硬盘可能…

[MySQL--基础]事务的基础知识

前言 ⭐Hello!这里是欧_aita的博客。 ⭐今日语录&#xff1a;生活中最重要的决定就是要做出决定。 ⭐个人主页&#xff1a;欧_aita ψ(._. )>⭐个人专栏&#xff1a; 数据结构与算法 MySQL数据库 事务的目录&#x1f4d5; 前言事务简介&#x1f680;事务操作&#x1f680;准…

【头歌系统Python实验】顺序与选择结构

目录 第1关&#xff1a;顺序结构 第2关&#xff1a;选择结构&#xff1a;if-else 第3关&#xff1a;选择结构 &#xff1a; 三元操作符 如果对你有帮助的话&#xff0c;不妨点赞收藏评论一下吧&#xff0c;爱你么么哒&#x1f618;❤️❤️❤️ 第1关&#xff1a;顺序结构 …

批量AI写作生成器有哪些?免费的批量AI写作生成器

当今信息爆炸的时代&#xff0c;文案需求量庞大&#xff0c;传统文案写作已无法满足快速迭代的需求。批量AI写作生成器应运而生&#xff0c;成为许多行业的得力助手。在众多AI写作工具中&#xff0c;147原创助手以其批量AI写作功能和在各大平台显示原创首发的特性脱颖而出。本文…

大数据项目——基于Django/协同过滤算法的房源可视化分析推荐系统的设计与实现

大数据项目——基于Django/协同过滤算法的房源可视化分析推荐系统的设计与实现 技术栈&#xff1a;大数据爬虫/机器学习学习算法/数据分析与挖掘/大数据可视化/Django框架/Mysql数据库 本项目基于 Django框架开发的房屋可视化分析推荐系统。这个系统结合了大数据爬虫、机器学…

redis-学习笔记(hash)

Redis 自身已经是 键值对 结构了 Redis 自身的键值对就是通过 哈希 的方式来组织的 把 key 这一层组织完成后, 到了 value 这一层, 还可以用 哈希类型 来组织 (简单的说就是哈希里面套哈希 [数组里面套数组 -> 二维数组] ) [ field value ] hset key field value [ field va…

C++刷题 -- 链表

C刷题 – 链表 文章目录 C刷题 -- 链表1.删除链表的倒数第 N 个结点2.链表相交3.环形链表 1.删除链表的倒数第 N 个结点 https://leetcode.cn/problems/remove-nth-node-from-end-of-list/ 快慢指针的应用 fast指针先移动N步&#xff0c;slow依然指向head&#xff1b;然后fa…

对Spring源码的学习:二

目录 SpringBean实例化流程 Spring的后处理器 Bean工厂后处理器 SpringBean实例化流程 Spring容器在进行初始化时&#xff0c;会将xml配置的<bean>的信息封装成一个BeanDefinition对象&#xff0c;所有的BeanDefinition存储到一个名为beanDefinitionMap的Map集合中去…

成品短视频app源码选择指南

作为一名有志于开发短视频app的创业者&#xff0c;选择合适的成品短视频app源码至关重要。一款优秀的成品短视频app源码可以帮助你节省开发时间&#xff0c;加速上线进程&#xff0c;快速实现盈利。但在众多的选择中&#xff0c;如何找到适合自己的成品短视频app源码呢&#xf…

商业案例实战:Python数据可视化之四象限图

1.四象限图的含义 四象限图是一种针对二维数据&#xff08;x&#xff0c;y&#xff09;的平面图形。二维数据&#xff08;x&#xff0c;y&#xff09;的两个维度垂直交叉在一起&#xff0c;分别构成四象限图的X轴及Y轴。两个维度所有样本的均值&#xff08;即x的均值和y的均值…

在 Docker 容器中运行 macOS:接近本机性能,实现高效运行 | 开源日报 No.96

cxli233/FriendsDontLetFriends Stars: 2.6k License: MIT 这个项目是关于数据可视化中好的和不好的实践&#xff0c;作者通过一系列例子解释了哪些图表类型是不合适的&#xff0c;并提供了如何改进或替代它们。主要功能包括展示错误做法以及正确做法&#xff0c;并提供相应代…