当前位置: 首页 > news >正文

Linux应用(4)——进程通信

一、简介

Linux下进程通信的目的
1.数据传输 2.通知控制3.数据同步(数据保护)
常见的进程通信方式
无名管道、命名管道、信号、共享内存、消息队列、信号量、套接字
进程通信的注意事项

  1. 要保证通信的双方/多方操作的是同一通信方式,遵循同一规范
  2. 进程之间的通信是在应用层完成的,但通信方式的实现都在内核层,用户需要借助于内核中的通信方式来实现应用层各进程之间的通信
  3. 前面说过内核层和应用层是通过文件操作实现交流的(文件io 和标准io)
  4. 所有进程通信方式的实现就是在文件操作的基础上新增 每种通信方式的特性
  5. 进程通信的API函数=文件操作API +进程通信特性api

常见进程通信方式的对比

通信方式

用途

无名管道

主要用于 父子进程之间少量的数据传输

命名管道

主要用于 任意两个进程之间少量的数据传输

信号

通知控制(类似于MCU中的中断),通常和其他进程通信方式结合使用,用来解决其他进程通信方式中的接受问题

信号+管道、信号+共享内存、信号+消息队列

共享内存

主要用于 多个进程之间 大量的数据传输-速率快、操作复杂

消息队列

主要用于 多个进程之间 大量的数据传输--速率慢、操作简单

信号量

数据同步、数据的保护--主要用在多线程通信中

套接字

主要用在网络通信 TCP /  UDP 数据传输

二、管道通信应用

2.1 简介

无名管道:主要用于 父子进程之间少量数据传输
命名管道:主要用于 任意两个进程之间少量数据传输
管道特性:

1.管道是内核层的一种进程通信方式,在内核层是不存在,需要使用前,先创建管道
2.管道只有两个端口,是一种单向的数据传输,遵循先进先出的原则 (类比水管)
一端: 读端----数据只能从读端出管道 (类似于出口口)
另一端:写端----数据只能从写端进入管道(类似于进水口)
3.实现写端操作: 还是调取 文件 写函数;实现读端操作: 还是调取 文件 读函数
4.读管道特性☆
特性1:管道不同于内存;内存--读内存后,内存中的数据还在;管道--读管道后,管道中的数据就没了(类似于接水后,水管中就没水了)
特性2:当进程读空管道时,就会造成进程的阻塞,直到管道中重新写入了数据,该进程堵到了数据,才取消阻塞--哪个进程读空管道,哪个进程就被阻塞
空管道——管道中没写数据;管道中写了数据但已经被读走了
5.写管道特性(了解)
往管道中一直写数据,不读,就会造成管道的写满,当管道写满后,哪个进程在写管道,就回造成哪个进程写阻塞。--大致64K字节左右

2.2 无名管道

2.2.1 相关API

 pipe    fork   exit  wait   waitpid   read  write

2.2.2 pipe 函数

头文件:#include<unistd.h>
函数原型:int pipe(int filedes[2]);
函数参数
@param1 
int filedes[2]: 创建成功后得到的读端、写端 的文件ID ---文件描述符
fd[0]: 读端的文件描述符   fd[1]: 写端的文件描述符
函数返回值:成功; 0,失败: -1
函数功能: 用来创建一根无名管道
函数注意事项:要先创建管道,然后再创建子进程 ---这样才能保证父子进程操作的是同一根管道

2.2.3 练习

创建父子进程,子进程写管道,父进程读管道,并打印读到的数据,当子进程写入”byebye”时,父子进程都结束

/*创建父子进程,子进程写管道,父进程读管道,并打印读到的数据,当子进程写入”byebye”时,父子进程都结束*/#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h> 
#include <sys/wait.h>
int main()
{pid_t pid ;//用于接收IDint fd[2];//用于接收文件描述符//创建管道if(pipe(fd)==-1){printf("管道创建失败\n");return -1;}//创建进程pid=fork();if(pid==0)//子进程{char buff[256];//用于接收while(1)//父进程阻塞,子进程运行{printf("请输入数据:");scanf("%s",buff);write(fd[1],buff,strlen(buff));//写管道,管道满时阻塞if(strcmp(buff,"byebye")==0){exit(0);}memset(buff,0,strlen(buff));//清除buff内容usleep(1);//子进程主动阻塞,父进程开始}}else if(pid>0)//父进程{char buff[256];//用于接收while(1)//默认先执行{read(fd[0],buff,20);//读管道,管道无数据时阻塞printf("this is farther read from son:%s\n",buff);if(strcmp(buff,"byebye")==0){wait(NULL);exit(0);}memset(buff,0,20);//清除buff内容}}
}

2.3 有名管道

2.3.1 相关API

mkfifo (可以是命令/也可以是函数名),这里主要使用函数-创建管道名
open  read  write   (fork  exit  wait)

2.3.2 mkfifo函数

头文件:
#include<sys/types.h> 
#include<sys/stat.h>
函数原型:int mkfifo(const char * pathname,mode_t mode);
函数参数:
@param1:const char * pathname:带路径的命名管道名 (管道名创建前不能存在)
@param2: mode_t mode             : 八进制管道权限(r w  x),只需保证当前登录用户 可读可写
函数返回值:若成功则返回 0,否则返回-1,
函数功能:创建一根命名管道
注意事项
1.创建成功的管道,只有一个管道名,还需要通过open来打开管道
2.需要保证两个进程open的是同一根管道,打开成功后,即可read  write 管道
3.同一个管道只能被创建一次

mkfifo函数只能被调用一次,因为同一个管道只能被创建一次

那这时mkfifo调用位置就需要注意

方式1:mkfifo在 进程1 或进程2 中调用

     那需要保证调用 mkfifo的进程先运行 (否则另一个进程运行就会失败)

方式2:mkfifo单独封装源文件mkfifo.c

     那需要先运行 mkfifo.c , 然后再运行进程1 或进程2

2.3.3 练习

1.创建2个独立的进程,采用命名管道通信,进程1 写管道,进程2读管道,当进程1写入”byebye”,结束两个进程

/*****************【创建管道代码】************************/
#include<sys/types.h> 
#include<sys/stat.h>
#include <stdio.h>int main(int argc,char*argv[])
{if(argc !=2){printf("argc error\n");return -1;}//创建管道if(mkfifo(argv[1],0666)==-1){printf("mkfifo false\n");return -1;}printf("mkfifo ok\n");return 0;
}
/****************【写管道代码】*********************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char*argv[])
{int fd;//定义文件描述符char write_buf[256];if(argc!=2){printf("argc error\n");return -1;}//打开管道fd=open(argv[1],O_EXCL|O_RDWR);if(fd<0)//以可读可写打开一个已经存在的管道{ printf("open error\n");return -1;}printf("open ok\n");while(1){printf("write waiting >");scanf("%s",write_buf);write (fd,write_buf,strlen(write_buf));if(strcmp(write_buf,"byebye")==0){exit(0);}memset(write_buf,0,strlen(write_buf));}  
}
/***********************【读管道代码】***********************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char*argv[])
{int fd;//定义文件描述符char read_buf[256];if(argc!=2){printf("argc error\n");return -1;}//打开管道fd=open(argv[1],O_EXCL|O_RDWR);if(fd<0)//以可读可写打开一个已经存在的管道{ printf("open error\n");return -1;}printf("open ok\n");while(1){read (fd,read_buf,sizeof(read_buf));printf("Reader收到>:%s\n",read_buf);if(strcmp(read_buf,"byebye")==0){exit(0);}memset(read_buf,0,strlen(read_buf));}  
}

    创建2个独立进程,采用2根命名管道通信,当任意进程输入”byebye”,结束两个进程

    思路:进程1 ----->进程2   采用  001 管道;进程1<-----进程2   采用  002 管道 

    进程1流程-->键盘得到数据-->写入001管道-->读取002管道数据
    进程2流程-->读取001管道数据-->键盘得到数据-->写入002管道

    三、信号

    3.1 简介

    信号类似于MCU开发中的软中断,主要和其他通信方式结合,来处理通信中的接受问题(因为通信中的接收、管道、消息队列 都有读阻塞的特性--导致接收是一个被动的),这时通信可以和信号结合,把接收操作放到信号处理函数中;那在没数据接收时,就不影响进程的运行

    3.2 特性

    1. 信号在内核层,本身是存在且数量固定的,不需要创建,也不能创建
    2. 内核层总共存在64种不同的信号,多数都是结束进程的
    3. 可以通信信号处理函数,重定义信号功能

    系统命令:  kill   -l   可以用来查看内核层所有的信号

    内核层总共存在64种不同的信号,这里用户无需记忆每种信号的具体功能,默认情况下,信号都是用来结束进程的(结束的方式不同),所有后续重新定义信号的功能--信号处理函数
    比如 2号信号  SIGINT  : 就是终端  ctrl  c
    信号的使用:kill  -信号值  进程PID

    kill  函数

    信号发送类似于中断的产生,kill (可以是命令/也可以是函数名)
    头文件:#include<sys/types.h> ,#include<signal.h>
    函数原型:int kill(pid_t pid,int sig);
    函数参数
    @param1: pid_t pid:  进程PID
    @param2: int sig   :  要发送的信号值 (只能在1-64之间)
    函数返回值:执行成功则返回 0,如果有错误则返回-1
    函数功能:给指定进程发送任意信号
    注意事项:kill信号发送函数--通常都是给其他进程发信号,而不是给自己发
    核心: 怎样得到其他进程的PID 

    信号的接收,pause函数

    保证进程不结束

    头文件:#include<unistd.h>
    函数原型:int pause(void);
    函数返回值:只返回-1。
    函数功能:令目前的进程暂停(进入睡眠状态),直到被信号(signal)所中断。

    所以保证进程不结束的几种方式
    方式1:常用--没接收到信号时,进程处理其他事情;接收到信号后,进程进入信号处理函数,处理完接着回到接收信号前执行
    while(1)
    {   // 进程处理其他事情}
    方式2:使用不多,因为这种情况,进程只能处理接受信号
    while(1)
    {pause(); }

    信号的处理,signal函数

    类似于中断服务函数
    头文件:#include<signal.h>
    函数原型:void (*signal(int signum,void(* handler)(int)))(int));
    函数参数:
    @param1: int signum : 重新设置的信号值
    @param2: void(* handler)(int)))(int): 函数指针
    如果实参为:
    SIG_IGN  :  忽略该信号
    SIG_DFL  :  采用信号默认方式处理
    自定义函数名: 采用用户重定义方式处理
    函数返回值:void 
    函数功能:设置信号处理方式

    // 用户封装信号重定义函数
    void  自定义函数名(int signum)// 参数 信号值
    {   // 信号处理函数}
    注意事项:
    1. 系统信号值1-64种信号,除了9 和19号信号不可重置,其他都可以修改,因为9 和19 默认只能杀死进程
    2.同一个信号如果多次重置功能,以最后一次为准
    signal(6,fun1); 
    signal(6,fun2); 
    signal(6,fun3);  //  信号处理函数
    3.当信号处理函数执行过程中(没有执行完),这时又收到同一个信号,
    处理方式:先执行完本次的信号处理,然后再执行下一次信号处理

    4.当信号处理函数执行过程中(没有执行完),这时又收到另一个信号
    处理方式:直接打断当前的信号处理,转去执行新的信号处理函数,执行完再回到当前信号处理

    练习

    创建2个进程,其中进程1死循环,不作任何事情;在进程2中调取kill信号发生函数,给进程1发信号,结束进程1

    /************【进程1】*****************/
    #include <stdio.h>
    int main ()
    {while(1){printf("runing\n");}
    }
    /***************【进程2】****************/
    #include <stdio.h>
    #include <sys/types.h> 
    #include <signal.h>
    #include <stdlib.h>
    //./test2 进程PID,信号值
    int main(int argc,char*argv[])
    {int ret;pid_t pid;//用于接收pidint signal_num;if(argc!=3){printf("argc error\n");return -1;}pid=atoi(argv[1]);signal_num=atoi(argv[2]);ret=kill(pid,signal_num);if(ret==-1){printf("no kill\n");return -1;}else{ printf("kill ok\n");return 0;}}

    创建2个进程,其中进程1注册信号;在进程2中调取kill信号发生函数,给进程1发信号

    /*创建2个进程,其中进程1注册信号;在进程2中调取kill信号发生函数,给进程1发信号*/
    /*****************【程序1】*******************/
    #include <stdio.h>
    #include<signal.h>
    #include<unistd.h>
    //中断处理函数
    void myfun1(int sign_num)
    {printf("sign_num=%d\n",sign_num);
    }int main ()
    {//注册信号signal(12,myfun1);//将12信号与fun1关联,当发送信号为12时,此时内核会将12给到sign_num,会执行函数myfun1,while(1){pause();}
    }
    /*****************【程序2】*******************/
    #include <stdio.h>
    #include <sys/types.h> 
    #include <signal.h>
    #include <stdlib.h>
    //./test2 进程PID,信号值
    int main(int argc,char*argv[])
    {int ret;pid_t pid;//用于接收pidint signal_num;if(argc!=3){printf("argc error\n");return -1;}pid=atoi(argv[1]);signal_num=atoi(argv[2]);ret=kill(pid,signal_num);if(ret==-1){printf("no kill\n");return -1;}else{ printf("kill ok\n");return 0;}
    }

    创建2个进程,进程1 写管道001,读管道002,进程2 写管道002,读管道001;当进程2发信号时进程1读管道,当进程1发送信号时进程2读管道;当任意一方发送“byebye”进程1,2时结束(命名管道+信号结合)

    /*************【程序1】*****************/
    //./test1 001 002
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <signal.h>
    #include <stdlib.h>
    int fd_w,fd_r;//用于接收文件描述符
    char buff_w[20],buff_r[20];void fun1(int sign_num)
    {//读管道002信息read(fd_r,buff_r,20);printf("read>:%s\n",buff_r);if(strcmp(buff_r,"byebye")==0)exit(0);
    }
    int main(int argc,char*argv[])
    {if(argc!=3){printf("argc error\n");return -1;}//打开管道001fd_w=open(argv[1],O_EXCL|O_RDWR);if(fd_w<0){printf("001 open error\n");return -1;}//打开管道002fd_r=open(argv[2],O_EXCL|O_RDWR);if(fd_r<0){printf("002 open error\n");return -1;}//获取自己的PID写入管道001pid_t myselfID = getpid();sprintf(buff_w,"%d",myselfID);write (fd_w,buff_w,20);printf("myslefID:%d\n",myselfID);//通过管道002读取其他进程PIDread(fd_r,buff_r,20);pid_t otherID=atoi(buff_r);printf("otherID:%d\n",otherID);//注册信号signal(12,fun1);while(1){//写管道001printf("write>:");scanf("%s",buff_w);write (fd_w,buff_w,20);if(kill(otherID,13)==-1){printf("kill error\n");return -1;}if(strcmp(buff_w,"byebye")==0)exit(0);}
    }
    /***************【程序2】****************/
    //./test2 002 001
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <signal.h>
    #include <stdlib.h>
    int fd_w,fd_r;//用于接收文件描述符
    char buff_w[20],buff_r[20];void fun1(int sign_num)
    {//读管道001信息read(fd_r,buff_r,20);printf("read>:%s\n",buff_r);if(strcmp(buff_r,"byebye")==0)exit(0);
    }
    int main(int argc,char*argv[])
    {if(argc!=3){printf("argc error\n");return -1;}//打开管道002fd_w=open(argv[1],O_EXCL|O_RDWR);if(fd_w<0){printf("001 open error\n");return -1;}//打开管道001fd_r=open(argv[2],O_EXCL|O_RDWR);if(fd_r<0){printf("002 open error\n");return -1;}//获取自己的PID写入管道002pid_t myselfID = getpid();sprintf(buff_w,"%d",myselfID);write (fd_w,buff_w,20);printf("myslefID:%d\n",myselfID);//通过管道001读取其他进程PIDread(fd_r,buff_r,20);pid_t otherID=atoi(buff_r);printf("otherID:%d\n",otherID);//注册信号signal(13,fun1);while(1){//写管道002printf("write>:");scanf("%s",buff_w);write (fd_w,buff_w,20);if(kill(otherID,12)==-1){printf("kill error\n");return -1;}if(strcmp(buff_w,"byebye")==0)exit(0);}
    }
    

    四、IPC通信

    4.1 简介

    1. inux系统中进程IPC通信包括:共享内存、消息队列、信号量
    2. 也就是说在Linux下共享内存、消息队列、信号量存在一定的共性

    4.2 系统相关命令

    命令1:查看类
    ipcs  -m     查看系统下已存在的共享内存
    ipcs   -q     查看系统下已存在的消息队列
    ipcs   -s     查看系统下已存在的信号量

    命令2: 删除类
    ipcrm  -m   shmid   删除指定的共享内存(通过shmid 共享内存id)
    ipcrm  -q    msqid  删除指定的消息队列(通过msqid  消息队列id)
    ipcrm  -s    semid  删除指定的信号量(通过semid  信号量id)

    4.3 操作流程

    共享内存的操作流程:

    1. 打开或创建一块共享内存--通过key值,保证多个进程创建的是同一块
    2. 把内存空间映射到进程中(哪个进程想是否该共享内存,就映射)
    3. 映射同一块共享内存的多个进程即可通信 (对共享内存读写操作)
    4. 当哪个进程不想通信,取消映射(进程和共享内存断开联系)
    5. 当所有的进程都结束通信,删除该共享内存

       消息队列的操作流程:

    1. 打开或创建一条消息队列---通过key值,保证多个进程创建的是同一条
    2. 打开同一条消息队列的进程就可以发送数据、接收数据
    3. 当所有进程都结束通信,删除该消息队列

    五、共享内存

    5.1 简介

    主要用于 多个进程之间 大量的数据传输,内核层存在内存,但不是共享的,所以共享内存使用前也需要创建
    速率快: 直接对内存读写
    操作复杂:对内存读写--要用到指针,
    读操作: 内存中数据不影响
    写操作---写是覆盖,所有在写操作之前需要考虑原内存中的数据是否还需要

    相关函数:ftok(file to key)   shmget(share memory get)   shmctl(share memory contral)   shmat(share memory attach)  shmdt(share memory detach)

    5.2 ftok 函数

    头文件 #include <sys/types.h> #include <sys/ipc.h>
    函数原型:key_t ftok(const char *pathname, int proj_id);
    函数参数:
     @param1 const char *pathname:  带路径的必须存在文件名
    @param2 int proj_id                   : 任意字符
    函数返回值:key_t: 产生得到的键值
    函数功能:产生一个唯一的键值
    函数特性:文件名和任意字符 在多个使用同一块共享内存中的值保证一样即可,无指定要求
    也就是说: 文件名和任意字符 一样---那得到的key键值就一样---那打开的就是同一块共享内存

    5.3 shmget 函数

    头文件:#include <sys/ipc.h> #include <sys/shm.h>
    函数原型:int shmget(key_t key, size_t size, int shmflg);
    函数参数:
    @param1 
    key_t key: 键值 要来保障多个进程创建的是同一块共享内存
    如果是父子进程通信:那该值写入 :  IPC_RPIVATE 或0
    如果是任意进程通信 : 那该值就需要通过 ftok函数生成
       @param2 size_t size: 共享内存的字节数
      @param3 int shmflg : 共享内存的权限标志 ,是权限(只要保证当前登录用户可得可写)和 IPC_CREAT 或 IPC_EXCL的按位或合成
    举例: 0600|  IPC_CREAT    共享内存不存在就创建,存在就直接打开
    0600 |  IPC_EXCL     共享内存不存在就报错结束,存在就直接打开
    函数返回值: 成功: 返回 shmid  共享内存id;失败:  -1
    函数功能:创建得到一块共享内存,内存空间默认为 0
    函数特性:
    与mkfifo 命名管道创建 不同
    1.mkfifo 同一根管道只能被创建一次(只能在一个进程中创建)
    2.shmget 同一块共享内存需要在每个进程中都创建(针对于父子进程来说在fork前创建一次即可;在父子进程中分别创建也可以 ,效果是一样的--因为key键值==0)

    5.4 shmat 函数

    头文件:#include <sys/types.h> #include <sys/shm.h>
    函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
    函数参数:
    @param1: int shmid: 共享内存id
    @param2: const void *shmaddr : 共享内存映射到进程中的地址,通常写入NULL表示让系统自动在进程中找一块可用地址
    @param3: Shmflg:该进程想对共享内存做的权限
    SHM_RDONLY  共享内存只读
    0                      共享内存可读可写
    函数返回值:void * : 得到的共享内存的首地址;失败返回-1;
    函数功能:把共享内存映射到进程中
    函数特性:
    1.共享内存的首地址可以是任意类型的 char *p    short *p  int *p  struct stu *p p+1
    2.首地址很重要,最好不要直接对首地址操作,最好引入中间指针变量

    5.5 shmdt 函数

    头文件 #include <sys/types.h>,#include <sys/shm.h>
    函数原型:int shmdt(const void *shmaddr);
    函数参数:const void *shmaddr: 映射成功后的首地址
    函数返回值:函数成功返回 0,失败时返回-1.
    函数功能:取消内存映射

    5.6 shmctl 函数

    头文件:#include <sys/ipc.h>,#include <sys/shm.h>
    函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    函数参数:
    @param1:
    int shmid : 共享内存id
     @param2:int cmd    :  控制方式
    IPC_STAT  :  读取信息
    IPC_SET    :  重新设置信息
    IPC_RMID :   删除共享功能--常用 
    @param3:struct shmid_ds *buf : 共享内存信息结构体指针
    struct shmid_ds {
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Last change time */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t        shm_nattch;  /* No. of current attaches */

              ...};
    1.如果 读取信息,那就定义一个结构体变量来接收信息
    struct shmid_ds  a;//结构体变量
    shmctl( shmid, IPC_STAT, &a);
    2.如果设置信息,那就定义一个结构体变量赋值
    struct shmid_ds  a={
    .shm_segsz =100,};
    shmctl( shmid, IPC_SET, &a);
    3.如果 删除共享内存,那直接 NULL 即可

    • 使用 IPC_RMID 时,共享内存不会立即被删除

    • 只有当所有进程都分离(detach)了该共享内存后,它才会被真正删除

    • 如果没有任何进程附加到该共享内存,则立即删除

    函数返回值:0:成功;-1:失败
    函数功能:共享内存控制函数--读取、设置、删除

    5.7 练习

    1. 创建父子进程,父进程写内存,子进程读内存
    /*创建父子进程,父进程写,子进程读*/#include <stdio.h>
    #include <sys/ipc.h> 
    #include <sys/shm.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    #include <string.h>
    //定义结构体类型typedef struct 
    {char str1[32];char str2[32];int number;int ready;//1-已写,0-已读,
    }share_data;
    int main()
    {/*1.创建一个共享内存*/int shmID= shmget(0,sizeof(share_data),0600|IPC_CREAT);if(shmID==-1){printf("shmget error\n");return -1;}/*2.创建父子进程*/pid_t PID=fork();if(PID<0){printf("fork error\n");shmctl(shmID,IPC_RMID,NULL);return -1;}if(PID==0)//进入子进程{/*共享内存映射*/share_data*son_data=(share_data*)shmat(shmID,NULL,0);if(son_data==(void*)-1){printf("shmat error\n");shmctl(shmID,IPC_RMID,NULL);return -1;}/*读数据*/while(1){while(son_data->ready!=1)//当父进程未写入时,就一直阻塞;当ready=0时阻塞{usleep(100);}//读printf("read:%s %s %d\n",son_data->str1,son_data->str2,son_data->number);son_data->ready=0;//表明已读,可再写if(strcmp(son_data->str1,"byebye")==0){break;}}printf("son kill\n");shmdt(son_data);exit(0); }else //进入父进程{/*共享内存映射*/share_data*far_data=(share_data*)shmat(shmID,NULL,0);if(far_data==(void*)-1){printf("shmat error\n");shmctl(shmID,IPC_RMID,NULL);return -1;}/*初始化内存*/memset(far_data,0,sizeof(share_data));far_data->ready=0;//初始值为0//写数据while(1){while(far_data->ready!=0)//当ready=1时阻塞{usleep(100);}printf("write>");if(scanf("%s %s %d",far_data->str1,far_data->str2,&far_data->number)!=3){printf("Input error\n");int c;while((c=getchar())!='\n' && c!= EOF);continue;}far_data->ready=1;//数据已写if(strcmp(far_data->str1,"byebye")==0){break;}}wait(NULL);//等待子程序结束shmdt(far_data);//取消映射shmctl(shmID,IPC_RMID,NULL);//删除共享内存printf("farther kill\n");exit(0);}
    }
    

    2./*创建父子进程,父进程写然后子进程读,然后子进程读后父进程读,任意一方输入byebye,结束进程*/

    /*创建父子进程,父进程写,子进程读*/#include <stdio.h>
    #include <sys/ipc.h> 
    #include <sys/shm.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    #include <string.h>
    //定义结构体类型typedef struct 
    {char str1[32];char str2[32];int number;int ready;//1-已写,0-已读,
    }share_data;
    int main()
    {/*1.创建一个共享内存*/int shmID= shmget(0,sizeof(share_data),0600|IPC_CREAT);if(shmID==-1){printf("shmget error\n");return -1;}/*2.创建父子进程*/pid_t PID=fork();if(PID<0){printf("fork error\n");shmctl(shmID,IPC_RMID,NULL);return -1;}/*进入子进程*/if(PID==0){/*共享内存映射*/share_data*son_data=(share_data*)shmat(shmID,NULL,0);if(son_data==(void*)-1){printf("shmat error\n");shmctl(shmID,IPC_RMID,NULL);return -1;}/*读数据*/while(1){ int son_inputnum;//用于接收scanf的值int c;//用于接收getchar()的值/*当父进程未写入时,就一直阻塞*/while(son_data->ready!=1){usleep(100);}/*子进程读*/printf("son read:%s %s %d\n",son_data->str1,son_data->str2,son_data->number);/*检查退出条件*/if(strcmp(son_data->str1,"byebye")==0){break;}/*子程序写*/do{printf("son write>");son_inputnum=scanf("%s %s %d",son_data->str1,son_data->str2,&son_data->number);if(son_inputnum!=3){printf("Input error\n");while((c=getchar())!='\n' && c!= EOF);}}while(son_inputnum!=3);/*子程序数据读写完*/son_data->ready=0;/*检查退出条件*/if(strcmp(son_data->str1,"byebye")==0){break;}}printf("son kill\n");shmdt(son_data);exit(0); }/*进入父进程*/else {int far_inputnum;//用于接收scanf的值int c;//用于接收getchar()的值/*共享内存映射*/share_data*far_data=(share_data*)shmat(shmID,NULL,0);if(far_data==(void*)-1){printf("shmat error\n");shmctl(shmID,IPC_RMID,NULL);return -1;}/*初始值为0,表明数据没有准备好*/far_data->ready=0;while(1){do{printf("far write>");far_inputnum=scanf("%s %s %d",far_data->str1,far_data->str2,&far_data->number);if(far_inputnum!=3){printf("Input error\n");while((c=getchar())!='\n' && c!= EOF);}}while(far_inputnum!=3);/*数据已就绪*/far_data->ready=1;/*检查退出条件*/if(strcmp(far_data->str1,"byebye")==0){break;}/*等待子进程读写完成*/while(far_data->ready!=0){usleep(100);}/*打印*/printf("far read:%s %s %d\n",far_data->str1,far_data->str2,far_data->number);/*检查退出条件*/if(strcmp(far_data->str1,"byebye")==0){break;}}wait(NULL);shmdt(far_data);shmctl(shmID,IPC_RMID,NULL);printf("farther kill\n");exit(0);}
    }
    

    3.共享内存+信号结合: 实现任意两个进程的数据交互

    /*共享内存+信号结合: 实现任意两个进程的数据交互*/
    /*步骤: 创建key值创建共享内存内存映射得到自身pid,写入共享内存读取对方pid信号注册
    */
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    typedef struct 
    {char str[1024];char flag1;int PID[2];
    }data;
    data*shm_data;
    int shmID;
    void fun(int signum)
    {printf("test1 read:%s\n",shm_data->str);if(strcmp(shm_data->str,"byebye")==0){shmdt(shm_data);shmctl(shmID,IPC_RMID,NULL);printf("test1 exit\n");exit(0);}printf("write>");fflush(stdout);
    }
    int main()
    {/*1.创建key*/key_t key = ftok("./test1.c", 'a');/*2.创建内存共享*/int shmID=shmget(key, sizeof(data),0600|IPC_CREAT);if(shmID==-1){printf("shmget error\n");return -1;}/*3内存映射*/shm_data= (data*)shmat(shmID, NULL,0);if(shm_data==(void*)-1){printf("shmat error");shmctl(shmID,IPC_RMID,NULL);return -1;}/*4.得到自身PID*/shm_data->PID[0]=getpid();printf("myslefPID=%d\n",shm_data->PID[0]);/*等待另一个程序写入*/printf("waiting other process...\n");while(shm_data->PID[1]==0);printf("otherPID=%d\n",shm_data->PID[1]);/*5注册信号*/signal(11,fun); while(1){/*写*/printf("write>");scanf("%s",shm_data->str);if(kill(shm_data->PID[1],12)==-1){printf("kill error\n");break;}if(strcmp(shm_data->str,"byebye")==0)break;}shmdt(shm_data);shmctl(shmID,IPC_RMID,NULL);printf("test1 exit\n");exit(0);}/********************************************************************/
    /*共享内存+信号结合: 实现任意两个进程的数据交互*/
    /*步骤: 创建key值创建共享内存内存映射得到自身pid,写入共享内存读取对方pid信号注册
    */
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <signal.h>
    #include <string.h>
    typedef struct 
    {char str[1024];char flag;int PID[2];
    }data;
    data*shm_data;
    int shmID;
    volatile sig_atomic_t need_prompt = 0;
    void fun(int signum)
    {printf("test2 read:%s\n",shm_data->str);if(strcmp(shm_data->str,"byebye")==0){shmdt(shm_data);shmctl(shmID,IPC_RMID,NULL);printf("test2 exit\n");exit(0);}printf("write>");fflush(stdout);
    }int main()
    {   /*1.创建key*/key_t key = ftok("./test1.c", 'a');/*2.创建内存共享*/shmID=shmget(key, sizeof(data),0600|IPC_CREAT);if(shmID==-1){printf("shmget error\n");return -1;}/*3内存映射*/shm_data= (data*)shmat(shmID, NULL,0);if(shm_data==(void*)-1){printf("shmat error");shmctl(shmID,IPC_RMID,NULL);return -1;}shm_data->flag=1;/*4.得到自身PID*/shm_data->PID[1]=getpid();printf("myslefPID=%d\n",shm_data->PID[1]);/*等待另一个程序写入*/printf("waiting other process...\n");while(shm_data->PID[0]==0);printf("otherPID=%d\n",shm_data->PID[0]);/*5注册信号*/signal(12,fun); while(1){printf("write>");scanf("%s",shm_data->str);if(kill(shm_data->PID[0],11)==-1){printf("kill error\n");break;}if(strcmp(shm_data->str,"byebye")==0)break;}shmdt(shm_data);shmctl(shmID,IPC_RMID,NULL);printf("test2 exit\n");exit(0);
    }

    六、消息队列

    6.1 简介

    用途:
    主要用于多个进程之间 大量的数据传输--使用较多
    速率慢: 相对于共享内存而言
    操作简单:编程思路简单

    特性:

    1. 消息队列在内核层本身不存在,需要使用前,要创建消息队列
    2. 一条消息队列上可以存在多个消息节点(类似于链表),每个消息节点都可以用作数据的收发
    3. 要使用消息队列通信的多个进程可以把数据发送/接收到某个消息节点上
    4. 消息队列和管道的读写阻塞特性对比
      管道: 读阻塞、写阻塞  特性  是规定的不可更改的
      消息队列: 读阻塞、写阻塞 特性是用户可选择的 (用户可以选择阻塞或非阻塞)
    5. 消息队列和共享内存、管道的读数据特性的对比
      管道: 读数据后,管道中就没有数据了
      共享内存:读数据后,共享内存中数据还在
      消息队列:读(接收)数据后,消息队列该节点上就没有数据了 (类似仓库)

    操作流程:

    1. 打开或创建一条消息队列---通过key值,保证多个进程创建的是同一条
    2. 打开同一条消息队列的进程就可以发送数据、接收数据
    3. 当所有进程都结束通信,删除该消息队列

    注意事项:

    1. 要保证通信的多个进程操作的是同一条消息队列--通过 key键值保证
    2. 要保证进程通信要操作的是同一个消息节点
    3. 发送/接收数据的类型是固定的结构体型,需要重新构造,并保证发送和接收同样的结构体类型
    4. 消息队列的读写阻塞特性是可以用户选择的
    5. 在删除队列的时候,后结束的进程负责删除队列

    相关API

    ipcs   -q     查看系统下已存在的消息队列
    ipcrm  -q    msqid  删除指定的消息队列(通过msqid  消息队列id)
    ftok msgget  msgsnd   msgrcv  msgctl

    6.2 msgget 函数

    头文件#include <sys/types.h>,#include <sys/ipc.h>,#include <sys/msg.h>
    函数原型:int msgget(key_t key, int msgflg);
    函数参数:
    @param1:
    key_t key: 键值 要来保障多个进程创建的是同一条消息队列
    如果是父子进程通信:那该值写入 :  IPC_RPIVATE 或0
    如果是任意进程通信 : 那该值就需要通过 ftok函数生成
       @param1:int msgflg:消息队列的权限标志,是权限(只要保证当前登录用户可得可写)和 IPC_CREAT 或 IPC_EXCL的按位或合成
    举例: 0600 | IPC_CREAT    消息队列不存在就创建,存在就直接打开
    0600 | IPC_EXCL     消息队列不存在就报错结束,存在就直接打开
    返回值:成功: 返回 msgid  消息队列id,失败:  -1
    函数功能:打开或创建并打开一条消息队列
    函数特性:
    与mkfifo 命名管道创建 不同
    mkfifo 同一根管道只能被创建一次(只能在一个进程中创建)
    msgget 同一条消息队列需要在每个进程中都创建(针对于父子进程来说在fork前创建一次即可;在父子进程中分别创建也可以 ,效果是一样的--因为key键值==0)

    6.3 msgctl 函数

    头文件:#include <sys/types.h>,#include <sys/ipc.h>,#include <sys/msg.h>
    函数原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    函数参数:
      @param1:int msqid:  消息队列id
      @param2:int cmd  : 控制方式
    IPC_STAT  :  读取信息
    IPC_SET    :  重新设置信息
    IPC_RMID : 删除消息队列--常用
    @param3:struct msqid_ds *buf: 消息队列信息结构体指针
    struct msqid_ds {
    struct ipc_perm msg_perm;     /* Ownership and permissions */
    time_t          msg_stime;    /* Time of last msgsnd(2) */
    time_t          msg_rtime;    /* Time of last msgrcv(2) */
    time_t          msg_ctime;    /* Time of last change */
    unsigned long  __msg_cbytes; /* Current number of bytes in queue (nonstandard) 
    msgqnum_t       msg_qnum;     /* Current number of messages in queue */
    msglen_t        msg_qbytes;   /* Maximum number of bytes allowed in queue */
    pid_t           msg_lspid;    /* PID of last msgsnd(2) */
    pid_t           msg_lrpid;    /* PID of last msgrcv(2) */};
    如果 读取信息,那就定义一个结构体变量来接收信息
    struct msqid_ds  a;//结构体变量
    msgctl( shmid, IPC_STAT, &a);
    如果设置信息,那就定义一个结构体变量赋值
    struct msqid_ds  a={ . __msg_cbytes =100};
    msgctl( shmid, IPC_SET, &a);
      如果 删除消息队列,那直接 NULL 即可
    函数返回值:0:成功;-1:失败
    函数功能:消息队列控制:查看、设置、删除

    6.4 msgsnd 函数

    头文件:#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
    函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    函数参数:
      @param1:int msqid: 消息队列id
      @param2:const void *msgp: 要发送的数据信息(消息)定义结构体变量/结构体数组并赋值
    struct msgbuf  msgdata={3,"hello",100};// 含义 把"hello"和100 发送到 3号消息节点上
      @param3:size_t msgsz  :  发送消息的字节数--不包含结构体首成员字节数的大小
    如果数据传输的是一个结构体变量 ,那msgsz= siezof(struct msgbuf)-8                   
    @param4:int msgflg    : 发送权限标志 (写阻塞标志)-无所谓,填充哪个都可以
    当一直往消息节点上发数据,不读,当写满后再写,就导致写阻塞--64K左右
    0 : 阻塞;IPC_NOWAIT : 非阻塞
    函数返回值:成功:0,失败:-1
    函数功能:给指定消息队列中指定节点发数据
    注意事项:要发送的信息必须重新构造信息结构体,不能是任意类型
    其中结构体名和第一个成员名固定,其他用户自定义
    格式如下:
    struct msgbuf {
    long mtype;   消息节点编号从1开始    /* message type, must be > 0 */
    /* 用户自定义--才是要发送的数据*/
    char buf[20];
    int num;...};

    6.5 msgrcv 函数

    头文件:#include <sys/types.h>,#include <sys/ipc.h>,#include <sys/msg.h>
    函数原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,  int msgflg);
    函数参数:
    @param1:int msqid : 消息队列id
    @param2:void *msgp: 接收到的数据--和发送同样的结构体类型
    @param3:size_t msgsz:要接收的字节数 -要和发送的字节数保持一致siezof(struct msgbuf)-8
    @param4:long msgtyp: 要从哪个消息节点上接收数据
    @param5:int msgflg : 读阻塞特性标志--读阻塞特性-当读空时是否阻塞
    0 :       阻塞;IPC_NOWAIT : 非阻塞
    区别: 当消息节点上有数据, 那效果是一样的
    当消息节点上没有数据,如果 填充0 : 那进程就会被阻塞
    如果 填充 IPC_NOWAIT : 那进程也不会被阻塞
    函数返回值:成功:实际接收到消息的长度;失败:-1
    函数功能:从指定消息队列的指定消息节点上读取数据
    注意事项:接收函数中接收数据类型也要适合发送中构造同样的结构体类型

    6.6 练习

    /*创建父子进程,父进程写内存,子进程读;子进程写,父进程读,byebye都结束*/

    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include<sys/types.h> 
    #include<sys/wait.h>
    #include <unistd.h>
    typedef struct msgbuf 
    {long mtype;   char buf[128];
    }data;
    int main()
    {/*创建消息队列*/int MsgId=msgget(0,0600|IPC_CREAT);if(MsgId==-1){printf("msgget error\n");return -1;}/*创建父子进程*/int PID=fork();if(PID<0){printf("PID error\n");msgctl(MsgId,IPC_RMID, NULL);return -1;}else if(PID==0){data son_data;while(1){/*从2号节点读消息队列*/if(msgrcv(MsgId, &son_data, sizeof(data)-8,2,0)==-1){printf("msgsnd error\n");break;}/*退出条件*/if(strcmp(son_data.buf,"byebye")==0)break;printf("son_read:%s\n",son_data.buf);/*往1号节点写消息队列*/printf("son write>");scanf("%s",son_data.buf);son_data.mtype=1;//1号节点/*退出条件*/if(strcmp(son_data.buf,"byebye")==0){if(msgsnd(MsgId,&son_data, sizeof(data)-8,0)==-1)printf("msgsnd error\n");break;}//发送if(msgsnd(MsgId,&son_data, sizeof(data)-8,0)==-1){printf("msgsnd error\n");break;}}exit(0);}else{data far_data;while(1){/*往2节点写消息队列*/printf("far write>");scanf("%s",far_data.buf);far_data.mtype=2;//2号节点/*退出条件*/if(strcmp(far_data.buf,"byebye")==0){if(msgsnd(MsgId,&far_data, sizeof(data)-8,0)==-1)printf("msgsnd error\n");break;}//发送if(msgsnd(MsgId,&far_data, sizeof(data)-8,0)==-1){printf("msgsnd error\n");break;}/*从1节点读消息队列*/if(msgrcv(MsgId, &far_data, sizeof(data)-8,1,0)==-1){printf("msgsnd error\n");break;}if(strcmp(far_data.buf,"byebye")==0)break;printf("far_read:%s\n",far_data.buf);}wait(NULL);msgctl(MsgId,IPC_RMID, NULL);exit(0);}
    }

    /*消息队列+信号结合实现两个进程通信*/

    /*步骤:得到key值创建队列得到自身PID写入队列读取对方PID信号注册*/
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <unistd.h>
    #include <signal.h>
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    typedef struct msgbuf
    {  long mtype;char  buff[128];pid_t PID;
    }Data;
    Data Buff; //数据buff
    int MsgId;//队列ID
    void Fun(int signum)
    {/*从2号节点读信号*/msgrcv(MsgId,&Buff,sizeof(Data)-8,2,0);printf("1.c read:%s\n",Buff.buff);if(strcmp(Buff.buff,"byebye")==0){msgctl(MsgId,IPC_RMID, NULL);//后结束的删除exit(0);}
    }int main()
    {/*获取key值*/key_t Key=ftok("./1.c", 'A');/*创建队列*/MsgId = msgget(Key,0600|IPC_CREAT);if(MsgId==-1){printf("msgget error\n");return -1;}/*获取自己PID*/Buff.PID =getpid();printf("MyselfPID=%d\n",Buff.PID);/*往1号队列写入自己的PID*/Buff.mtype=1;if(msgsnd(MsgId,&Buff,sizeof(Data)-8,0)==-1){printf("msgsnd error\n");msgctl(MsgId,IPC_RMID, NULL);return -1;}/*读2号队列的PID*/if(msgrcv(MsgId,&Buff,sizeof(Data)-8,2,0)==-1){printf("msgsnd error\n");msgctl(MsgId,IPC_RMID, NULL);return -1;}pid_t OtherPID=Buff.PID;printf("OtherPID=%d\n",Buff.PID);/*注册信号*/signal(11,Fun);while(1){/*写数据*/printf("1.c writer:");scanf("%s",Buff.buff);Buff.mtype=1;if(msgsnd(MsgId,&Buff,sizeof(Data)-8,0)==-1){printf("msgsnd error\n");break;}/*发信号*/if(kill(OtherPID,12)==-1){printf("kill error\n");break;}if(strcmp(Buff.buff,"byebye")==0)break;}exit(0);
    }
    /*****************************【程序2】********************************/
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <unistd.h>
    #include <signal.h>
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    typedef struct msgbuf
    {  long mtype;char  buff[128];pid_t PID;
    }Data;
    Data Buff; //数据buff
    int MsgId;//队列ID
    void Fun(int signum)
    {/*从1号节点读信号*/msgrcv(MsgId,&Buff,sizeof(Data)-8,1,0);printf("2.c read:%s\n",Buff.buff);if(strcmp(Buff.buff,"byebye")==0){msgctl(MsgId,IPC_RMID, NULL);//后结束的删除exit(0);}
    }int main()
    {/*获取key值*/key_t Key=ftok("./1.c", 'A');/*创建队列*/MsgId = msgget(Key,0600|IPC_CREAT);if(MsgId==-1){printf("msgget error\n");return -1;}/*获取自己PID*/Buff.PID =getpid();printf("MyselfPID=%d\n",Buff.PID);/*往2号队列写入自己的PID*/Buff.mtype=2;if(msgsnd(MsgId,&Buff,sizeof(Data)-8,0)==-1){printf("msgsnd error\n");msgctl(MsgId,IPC_RMID, NULL);return -1;}/*读1号读队列的PID*/if(msgrcv(MsgId,&Buff,sizeof(Data)-8,1,0)==-1){printf("msgsnd error\n");msgctl(MsgId,IPC_RMID, NULL);return -1;}pid_t OtherPID=Buff.PID;printf("OtherPID=%d\n",Buff.PID);/*注册信号*/signal(12,Fun);while(1){/*写数据*/printf("2.c writer:");scanf("%s",Buff.buff);Buff.mtype=2;if(msgsnd(MsgId,&Buff,sizeof(Data)-8,0)==-1){printf("msgsnd error\n");break;}/*发信号*/if(kill(OtherPID,11)==-1){printf("kill error\n");break;}if(strcmp(Buff.buff,"byebye")==0)break;}exit(0);
    }

    七、习题

    进程与程序的根本区别是:静态和动态特点
    进程对管道进行读操作和写操作都可能被阻塞

    fork()函数返回值大于0返回值为父进程的PID号
    共享内存和消息都是由Linux内核来管理和分配资源
    消息队列是将消息按队列的方式组织成的链表,每个消息都是其中的一个节点
    速度最快的进程通信方式是共享内存
    正在执行的进程由于其时间片用完而被暂停运行,此时该进程应从运行态变为就绪态
    一个进程退出等待队列而进入就绪队列,是因为进程获得了所等待的资源
    利用fork创建的子进程,它和父进程之间地址空间不同


    文章转载自:

    http://sGQm0BIw.xdpjs.cn
    http://EP1lNLLz.xdpjs.cn
    http://gS2pSPoY.xdpjs.cn
    http://L9tGgo4f.xdpjs.cn
    http://0U9IqfKY.xdpjs.cn
    http://0xUFp8V6.xdpjs.cn
    http://ikUeNuEN.xdpjs.cn
    http://4G6oIg71.xdpjs.cn
    http://CCJMx5dG.xdpjs.cn
    http://FLGPVtLC.xdpjs.cn
    http://5iLkkWf8.xdpjs.cn
    http://tC60EvnS.xdpjs.cn
    http://3Gw25tGt.xdpjs.cn
    http://Coa8GWVK.xdpjs.cn
    http://uPI4ci5A.xdpjs.cn
    http://FLSv3Gjh.xdpjs.cn
    http://jRvj9G5d.xdpjs.cn
    http://ZAWk1xbo.xdpjs.cn
    http://QoW1W4s9.xdpjs.cn
    http://kLnAQWow.xdpjs.cn
    http://YtmiCJaf.xdpjs.cn
    http://PyPZPAC3.xdpjs.cn
    http://6KFTExSb.xdpjs.cn
    http://g6JIzdlY.xdpjs.cn
    http://VHFk2Q3c.xdpjs.cn
    http://jKsDK3Wd.xdpjs.cn
    http://nvg5ga9P.xdpjs.cn
    http://3J36cbxc.xdpjs.cn
    http://cEefm41J.xdpjs.cn
    http://kGaF6YHL.xdpjs.cn
    http://www.dtcms.com/a/381822.html

    相关文章:

  1. 用C语言解决喝汽水问题
  2. 【开题答辩全过程】以 4S店汽车维修保养管理系统为例,包含答辩的问题和答案
  3. 边缘计算技术深入解析
  4. 三生原理的“素性塔“结构是否暗含共形场论中的算子乘积展开层级?‌
  5. 如何解决pip安装报错ModuleNotFoundError: No module named ‘cugraph’问题
  6. 评估硬件兼容性时如何快速判断老旧设备是否支持新协议
  7. [2025]使用echarts制作一个漂亮的天气预报曲线图
  8. 每日算法题推送
  9. DataSet-深度学习中的常见类
  10. Python编辑器的安装及配置(Pycharm、Jupyter的安装)从0带你配置,小土堆视频
  11. SystemVerilog 学习之SystemVerilog简介
  12. 中国联通卫星移动通信业务分析
  13. 学习游戏制作记录(实现震动效果,文本提示和构建游戏)9.13
  14. 【CMake】循环——foreach(),while()
  15. 对比Java学习Go——函数、集合和OOP
  16. AI时代的内容创作革命:深度解析xiaohongshu-mcp项目的技术创新与实战价值
  17. 3-11〔OSCP ◈ 研记〕❘ WEB应用攻击▸存储型XSS攻击
  18. 贪心算法应用:配送路径优化问题详解
  19. 神经网络稀疏化设计构架中的网络剪枝技术:原理、实践与前沿探索
  20. p5.js 绘制 3D 椭球体 ellipsoid
  21. Qt中自定义控件的三种实现方式
  22. leetcode34(环形链表)
  23. Jupyter Notebook 介绍、安装及使用
  24. 高并发场景下限流算法实践与性能优化指南
  25. 基于stm32的智能井盖系统设计(4G版本)
  26. 考研408计算机网络第36题真题解析(2021-2023)
  27. 【Linux系统】单例式线程池
  28. FreeSWITCH一键打包Docker镜像(源码编译)
  29. POI和EasyExcel
  30. 力扣-单调栈想法