进程间通信(匿名管道、命名管道、共享内存)
进程间通信
- 进程间通信的作用
- 管道
- 匿名管道
- 命令感受匿名管道
- 从内核角度去解释匿名管道
- 代码创建匿名管道
- 从PCB角度去分析匿名管道
- 匿名管道的非阻塞读写特性
- 创建管道,获取管道读写两端文件描述符的属性
- 给读写两端的文件描述符设置非阻塞属性
- 测试非阻塞
- 读端进行读(非阻塞),写端不写(不操作)
- 写端非阻塞进行写
- 命名管道
- 原理
- 创建命名管道及使用命名管道进行进程间通信
- 共享内存
- 原理
- 共享内存的接口
- 创建共享内存
- 附加共享内存到进程
- 分离共享内存
- 共享内存操作函数
- 共享内存的代码
- 消息队列&信号量
- 接口
进程间通信的作用
由于进程独立性的存在,两个进程想要直接交换数据是非常的困难的,所以需要进程间通信来解决进程与进程之间交换数据的问题
目前最大的进程间通信方式:网络
管道
匿名管道
命令感受匿名管道
这一个 “丨” 就是管道
作用:将 ps aux 命令的输出结果通过管道输入 grep 并作为 grep 的输入参数
从内核角度去解释匿名管道
管道就是内核当中的一块缓冲区(一块内存),进程A和进程B可以通过这个缓冲区进行交换数据
代码创建匿名管道
int pipe(int pipefd[2]);
参数:
pipefd:类型为整型数组,有两个元素,pipefd[0],pipefd[1]
pipefd[0],pipefd[1]:当中保存的是一个文件描述符
pipefd[0]:对应的文件描述符可以从管道当中进行读,不能写
pipefd[1]:对应的文件描述符可以往管道当中写,但不能读
pipefd[0],pipefd[1]当中的值是pipe函数进行赋值的,直白的说,当我们调用pipe函数的时候,只需要
给pipe函数传递一个拥有两个元素的整型数据的数组,pipe函数在创建完毕管道后,会给pipefd[0],pipefd[1]进行赋值
返回值:
-1:创建失败0:创建成功
闪烁有两种情况:
1、软链接指向的源文件被删除
2、软链接指向的是一块内存,而不是一个具体的文件
从PCB角度去分析匿名管道
1、匿名管道只适用于具有亲缘关系的进程,进行进程间通信
2、先创建管道,再创建子进程,父子进程才可以进行进程间通信
3、如果想要两个子进程使用匿名管道进行进程间通信,需要先创建管道,再创建子进程
4.管道的数据只能从写端流向读端,这是一种半双工的通信方式
全双工通信:数据可以从A端流向B端,也可以从B端流向A端
5、通过fd[0]从管道当中读取数据的时候,是将数据读走了(并不是拷贝了一份)
6、从管道当中去读数据的时候,可以指定读取任意大小的数据,如果管道当中没有数据,默认情况下,进行读则会阻塞
7、多次写入的数据之间是没有明显的分界的,上一条数据的末尾连接下一条数据的开头位置
8、匿名管道的生命周期跟随进程
匿名管道的非阻塞读写特性
非阻塞:
fcntl函数:设置/获取文件描述符的属性
int fcntl(int fd, int cmd, ...);
cmd 决定了 fcntl 函数究竟做什么事情
F_GETFL:获取文件描述符的属性,可变参数列表就可以不用传递任何值
F_SETFL:设置文件描述符的属性,需要制定设置文件描述符的属性,采用按位或的方式
非阻塞属性:O_NONBLOCK
返回值:
如果是获取(F_GETFL),返回文件描述符的属性
创建管道,获取管道读写两端文件描述符的属性
1 #include <stdio.h> 2 #include <fcntl.h>3 #include <unistd.h>4 5 int main()6 {7 int fd[2];8 int ret = pipe(fd);9 if(ret < 0)10 {11 perror("pipe fail\n");12 return 0;13 }14 15 //获取读端的文件描述符属性16 int flag = fcntl(fd[0],F_GETFL);17 printf("flag fd[0]: %d\n",flag);18 //获取写端的文件描述符属性19 flag = fcntl(fd[1],F_GETFL);20 printf("flag fd[1]: %d\n",flag);21 22 return 0;23 }
给读写两端的文件描述符设置非阻塞属性
首先是读端
1 #include <stdio.h>2 #include <fcntl.h>3 #include <unistd.h>4 5 int main()6 {7 int fd[2];8 int ret = pipe(fd);9 if(ret < 0)10 {11 perror("pipe fail\n");12 return 0;13 }14 15 int flag = fcntl(fd[0],F_GETFL);16 printf("flag fd[0]: %d\n",flag);17 18 fcntl(fd[0], F_SETFL, flag | O_NONBLOCK);19 20 flag = fcntl(fd[0],F_GETFL); 21 printf("flag fd[0]: %d\n",flag);22 23 return 0;24 }
随后是写端
1 #include <stdio.h>2 #include <fcntl.h>3 #include <unistd.h>4 5 int main()6 {7 int fd[2];8 int ret = pipe(fd);9 if(ret < 0)10 {11 perror("pipe fail\n");12 return 0;13 }14 15 int flag = fcntl(fd[1],F_GETFL);16 printf("flag fd[0]: %d\n",flag);17 18 fcntl(fd[1], F_SETFL, flag | O_NONBLOCK);19 20 flag = fcntl(fd[1],F_GETFL); 21 printf("flag fd[0]: %d\n",flag);22 23 return 0;24 }
测试非阻塞
1 #include <stdio.h> 2 #include <unistd.h>3 #include <fcntl.h>4 5 /* 1、创建匿名管道,之后创建子进程,让子进程进行进程间通信6 * 2、因为父子进程当中的文件描述符表都是拥有fd[0],fd[1],规定父进程读,子进程写7 * 3、再测试非阻塞属性*/8 9 void SetNonBlock(int fd)//提供一个函数给对应的fd[x]加上非阻塞属性10 {11 int flag = fcntl(fd, F_GETFL);12 fcntl(fd, F_SETFL, flag | O_NONBLOCK);13 }14 15 int main()16 {17 int fd[2];18 int ret = pipe(fd);19 if(ret < 0)20 {21 perror("pipe fail");22 return 0;23 }24 25 ret = fork();26 if(ret < 0)27 {28 perror("fork fail");29 return 0;30 }31 else if(ret == 0)32 {33 //child34 close(fd[0]);//关闭读端,只留下写端35 SetNonBlock(fd[1]);36 } 37 else 38 {39 //father40 close(fd[1]);//关闭写端,只留下读端41 SetNonBlock(fd[0]);42 }43 return 0;44 }
读端进行读(非阻塞),写端不写(不操作)
1、写端不关闭
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 5 void SetNonBlock(int fd)6 {7 int flag = fcntl(fd, F_GETFL);8 fcntl(fd, F_SETFL, flag | O_NONBLOCK);9 }10 11 int main()12 {13 int fd[2];14 int ret = pipe(fd);15 16 if(ret < 0)17 {18 perror("pipe fail");19 return 0;20 }21 22 ret = fork();23 24 if(ret < 0)25 {26 perror("fork fail");27 return 0;28 }29 30 else if(ret == 0)31 {32 //child33 close(fd[0]);//关闭读端,只留下写端34 SetNonBlock(fd[1]);35 36 //写端不关闭37 while(1)38 {39 sleep(1);40 }41 }42 43 else44 {45 //father46 close(fd[1]);//关闭写端,只留下读端47 SetNonBlock(fd[0]);48 49 char buf[1024] = {0};50 int read_size = read(fd[0], buf, sizeof(buf) - 1);51 52 while(1)53 {54 printf("read_size : %d\nbuf : %s\n", read_size, buf);55 }56 }57 58 return 0;59 }
但是此时无法确定读端调用 read,read 函数返回-1,是因为管道当中没有内容还是由于调用函数出错表示的,因此需要更改
44 else 45 { 46 //father 47 close(fd[1]);//关闭写端,只留下读端 48 SetNonBlock(fd[0]); 49 50 while(1) 51 { 52 char buf[1024] = {0}; 53 int read_size = read(fd[0], buf, sizeof(buf) - 1); 54 55 if(read_size < 0) 56 { 57 if(errno == EAGAIN) 58 { 59 printf("管道为空\n"); 60 printf("read_size : %d\nbuf : %s\n", read_size, buf); 61 } 62 } 63 } 64 }
此时需要额外包一个头文件
#include <errno.h>
再运行一次
如果错误码为 EAGAIN ,应该认为是正常情况
2、写端关闭
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 #include <errno.h>5 6 void SetNonBlock(int fd)7 {8 int flag = fcntl(fd, F_GETFL);9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);10 }11 12 int main()13 {14 int fd[2];15 int ret = pipe(fd);16 17 if(ret < 0)18 {19 perror("pipe fail");20 return 0;21 }22 23 ret = fork();24 25 if(ret < 0)26 {27 perror("fork fail");28 return 0;29 }30 31 else if(ret == 0)32 {33 //child34 close(fd[0]);//关闭读端,只留下写端35 close(fd[1]);//写端关闭36 37 while(1)38 {39 sleep(1);40 }41 }42 43 else44 {45 //father46 close(fd[1]);//关闭写端,只留下读端47 SetNonBlock(fd[0]);48 49 char buf[1024] = {0};50 int read_size = read(fd[0], buf, sizeof(buf) - 1);51 printf("read_size : %d, buf = %s\n", read_size, buf);52 }53 54 return 0;55 }
调用 read 返回 -1
写端非阻塞进行写
1、读端关闭
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 #include <errno.h>5 6 void SetNonBlock(int fd)7 {8 int flag = fcntl(fd, F_GETFL);9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);10 }11 12 int main()13 {14 int fd[2];15 int ret = pipe(fd);16 17 if(ret < 0)18 {19 perror("pipe fail");20 return 0;21 }22 23 ret = fork();24 25 if(ret < 0)26 {27 perror("fork fail");28 return 0;29 }30 31 else if(ret == 0)32 {33 //child34 close(fd[0]);//关闭读端,只留下写端35 36 int count = 0;37 while(1)38 {39 write(fd[1], "a", 1);40 printf("count : %d\n", count++);41 }42 }43 44 else45 {46 //father47 close(fd[1]);//关闭写端,只留下读端48 close(fd[0]);//关闭读端49 50 while(1)51 {52 sleep(1);53 }54 }55 return 0;56 }
因为此时读端已经被关闭了,而写端在进行写入,就好比一个水管不停的往里边输水,但是把出水口堵住,最终水管会破裂,也就导致了现在的僵尸进程
此时加上非阻塞
31 else if(ret == 0)32 { 33 //child 34 close(fd[0]);//关闭读端,只留下写端35 SetNonBlock(fd[1]); 36 37 int count = 0;38 while(1)39 {40 write(fd[1], "a", 1);41 printf("count : %d\n", count++); 42 }43 }
可以看到还是一样的情况,都是僵尸进程
即当前在通过 fd[1] 往管道当中去写的时候,会导致管道破裂,调用写的进程会被终止(信号终止)
2、读端不关闭
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 #include <errno.h>5 6 void SetNonBlock(int fd)7 {8 int flag = fcntl(fd, F_GETFL);9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);10 }11 12 int main()13 {14 int fd[2];15 int ret = pipe(fd);16 17 if(ret < 0)18 {19 perror("pipe fail");20 return 0;21 }22 23 ret = fork();24 25 if(ret < 0)26 {27 perror("fork fail");28 return 0;29 }30 31 else if(ret == 0)32 {33 //child34 close(fd[0]);//关闭读端,只留下写端35 SetNonBlock(fd[1]);36 37 int count = 0;38 while(1)39 {40 write(fd[1], "a", 1);41 printf("count : %d\n", count++);42 }43 }44 45 else46 {47 //father48 close(fd[1]);//关闭写端,只留下读端49 // close(fd[0]);不关闭读端50 51 while(1)52 {53 sleep(1);54 }55 }56 return 0;57 }
可以看到现在就不断的往里边写了
但是数值还是有点问题,于是修改一下
31 else if(ret == 0)32 {33 //child34 close(fd[0]);//关闭读端,只留下写端35 SetNonBlock(fd[1]);36 37 int count = 0;38 while(1)39 {40 int write_size = write(fd[1], "a", 1);41 42 if(write_size < 0)43 {44 printf("write_size: %d\n",write_size);45 if(errno == EAGAIN)46 {47 printf("管道已满\n");48 break;49 }50 }51 printf("count : %d\n", count++);52 }
命名管道
原理
也是在内核当中开辟了一块缓冲区,这块缓冲区是有标识符,可以被任何进程通过标识符找到
创建命名管道及使用命名管道进行进程间通信
命令创建:mkfifo
p 代表的是管道文件
写:
1 #include <stdio.h> 2 #include <unistd.h>3 #include <fcntl.h>4 5 int main()6 {7 int fd = open("./fifo_test", O_RDWR);8 if(fd < 0)9 {10 perror("open fail\n");11 return 0;12 }13 14 while(1)15 {16 write(fd, "oulaoula", 8);17 sleep(1);18 }19 close(fd);20 return 0;21 }
读:
1 #include <stdio.h>2 #include <unistd.h>3 #include <fcntl.h>4 5 int main()6 {7 int fd = open("./fifo_test", O_RDWR);8 if(fd < 0)9 {10 perror("open fail\n");11 return 0;12 }13 14 while(1)15 {16 char buf[1024] = {0};17 read(fd, buf, sizeof(buf) - 1);18 printf("buf : %s\n", buf); 19 }20 21 close(fd);22 return 0;23 }
此时这两个进程便实现了进程间通信
命名管道的生命周期也跟随进程
小知识:fifo,为first in first out的缩写,即先进先出
因为命名管道有标识符,所以命名管道支持不同进程之间的进程间通信
其他特性同匿名管道一样
共享内存
原理
1、首先在物理内存中创建了一块内存
2、不同的进程通过页表映射,将同一块物理内存映射到自己的虚拟地址空间
3、不同的进程操作进程虚拟地址,通过页表的映射,就相当于操作同一块内存,从而完成了数据交换
共享内存的接口
创建共享内存
int shmget(key_t key, size_t shmflg);
key:共享内存的标识符,用来标识一块共享内存,在操作系统中,共享内存的标识是不能重复的,可以直接给一个32位的16进制数字
size:共享内存的大小
shmflg:IPC_CREAT:如果key标识的共享内存不存在,则创建IPC_EXCL | IPC_CREAT:如果key标识的共享内存存在,则新创建一个后报错权限:按位或 8进制数字 例:0664 创建用户可读可写,组内用户可读可写,其他用户可读
返回值:-1:创建失败了>0:成功,返回的是共享内存的操作句柄,后续是通过操作句柄来操作共享内存的
查看共享内存的命令:ipcs -m
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/shm.h>4 5 #define key 0x121212126 7 int main()8 {9 int shmid = shmget(key, 1024, IPC_CREAT | 0664);
10 if(shmid < 0)
11 {12 perror("shmget fail");
13 return 0;
14 }
15
16
17 return 0;
18 }
key:标识符
shmid:操作句柄
owner:创建者
perms:权限
bytes:共享内存大小
nattch:附加进程数量
ststus:状态
共享内存的生命周期跟随操作系统内核
附加共享内存到进程
void *shmat(int shmind, const void *shmaddr, int shmflg);
shmid:共享内存操作句柄,即shmget的返回值
shmaddr:将共享内存附加到shmaddr,一般情况下都不会自己去指定映射到共享区中的哪一个虚拟地址,而是传递NULL值,让操作系统去选择
shmflg:标志将共享内存附加到进程后,进程对共享内存的读写属性0:读写SHM_RDONLY:只读
返回值:附加成功:返回值为附加到共享区当中的虚拟地址附加失败:NULL
分离共享内存
int shmdt(const void *shmaddr);
shmaddr:刚刚附加的时候,返回的共享区的地址
共享内存操作函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存操作句柄
cmd: IPC_STAT:获取共享内存参数IPC_SET :设置共享内存属性IPC_RMID:删除共享内存
struct shmid_ds:共享内存属性对应的结构体
共享内存的代码
写:
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/shm.h>4 #include <string.h>5 6 #define key 0x777777777 8 int main()9 {10 int shmid = shmget(key, 1024, IPC_CREAT | 0664);11 if(shmid < 0)12 {13 perror("shmget fail\n");14 return 0;15 }16 17 //附加到当前的进程18 void* addr = shmat(shmid, NULL, 0);19 if(addr == NULL)20 {21 perror("shmat fail\n");22 return 0;23 }24 //写 25 strncpy((char*)addr, "i am write", 10);26 27 shmdt(addr);28 29 return 0;30 }
读:
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/shm.h>4 5 #define key 0x777777776 7 int main()8 {9 int shmid = shmget(key, 1024, IPC_CREAT | 0664);10 if(shmid < 0)11 {12 perror("shmget fail\n");13 return 0;14 }15 16 //附加到当前的进程17 void* addr = shmat(shmid, NULL, 0);18 if(addr == NULL)19 {20 perror("shmat fail\n");21 return 0;22 }23 24 while(1)25 {26 printf("%s\n", (char*)addr);27 sleep(1);28 }29 30 shmdt(addr);31 32 return 0;33 }
共享内存读取的时候采用的是拷贝,而不是类似于管道一样的读走
更改一下
23 int count = 0;24 //写 25 while(1) 26 { 27 //strncpy((char*)addr, "i am write", 10); 28 sprintf((char*)addr, "%s-%d", "i am write", count++); 29 sleep(1);30 }
即共享内存在写的时候采用的是覆盖写的方式
使用 ipcrm -m [共享内存操作句柄] 可以删除共享内存
如果删除了一个被进程附加的共享内存
当前共享内存的标识符会改变成为0x00000000,且共享内存的状态会变成dest(destory)
可以通过 ipcs -m 这个命令查看到当前被删除共享内存的信息,说明在操作系统内核,描述该共享内存的结构体没有被释放,但是共享内存所使用的空间
已经被释放了,所以附加的进程如果再次操作共享内存,则有崩溃的风险
消息队列&信号量
队列的特性:先进先出
消息队列本质上也是在内核当中维护的一个双向链表,但满足了先进先出的特性小,所以被称之为队列
消息队列当中的消息:消息只的是带有类型的数据,类型和类型之间是有优先级的
接口
int msgget(key_t key, int msgflg);
key: 消息队列的标识符
返回值:成功返回消息队列的操作句柄
int msgsnd(int msqid, const void *msgp, size_t msgsz, int masgflg);
msqid:消息队列的操作句柄msgp:要发送到消息队列的消息
msgsz:指定发送的数据大小,只计算自己发送数据的大小
msgflg:IPC_NOWAIT:非阻塞发送方式0:阻塞发送
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msqid:消息队列的操作句柄
msgp:将接收的数据放到哪里
msgsz:最大的接收能力
msgtyp:> 0:表示获取队列当中距离队首最近同类型的元素==0:直接拿队首的元素< 0:1、需要将小于0的msgtype的值取绝对值2、过滤从队首到[msgtype]区间的消息3、从区间中获取和[msgtype]一样的消息4、再去当中获取类型最小的数据
msgflg:阻塞接收:0非阻塞接收:IPC_NOWAIT
进程间通信(匿名管道、命名管道、共享内存)相关推荐
- 【Linux篇】第十二篇——进程间通信(管道+system V共享内存)
进程间通信介绍 概念 目的 本质 分类 管道 什么是管道 匿名管道 匿名管道的原理 pipe函数 匿名管道使用步骤 管道读写规则 管道的特点 管道的大小 命名管道 命名管道的原理 使用命令创建命名管道 ...
- Linux学习笔记-匿名和命名管道读写的相同点及不同点
目录 理论 例子 理论 匿名和命名管道读写的相同性 1.默认都是阻塞性读写: 2.都适用于socket的网络通信: 3.阻塞不完整管道(有一段关闭) a.单纯读时,在所有数据被读取后,read返回0, ...
- shmget物理内存_Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()...
下面将讲解进程间通信的另一种方式,使用共享内存. 一.什么是共享内存 顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存.共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式 ...
- 【Linux】进程间通信 —— 匿名管道 | 命名管道 | System V | 消息队列 | 共享内存
进程间通信 0. 进程间通信 1. 管道 1.1 匿名管道 1.1.1 匿名管道原理 1.1.2 创建匿名管道pipe 1.1.3 基于匿名管道通信的4种情况5个特点 1.2 命名管道 1.2.1 创 ...
- 嵌入式Linux多任务编程 进程 管道 命名管道
进程 进程是一个可并发执行的具有独立功能的程序关于某个数据集合的一次执行过程,也是操作系统执行资源分配和保护的基本单位. 程序的一次执行就是一个进程 一个程序可以派生多个进程 多个不同程序运行的时候, ...
- Linux进程间通信源码剖析,共享内存(shmget函数详解)
转至:http://blog.csdn.net/lanmanck/article/details/6092995 shmget int shmget(key_t key, size_t size, i ...
- 五十、进程间通信——System V IPC 之共享内存
50.1 共享内存 50.1.1 共享内存的概念 共享内存区域是被多个进程共享的一部分物理内存 多个进程都可把该共享内存映射到自己的虚拟内存空间.所有用户空间的进程若要操作共享内存,都要将其映射到自己 ...
- 【Linux】进程间通信 - 匿名/命名管道与System V共享内存
目录 前言 一.管道 0.什么是管道 1).管道的概念 2).管道的本质 3).管道指令: "|" 1.匿名管道 1).如何创建匿名管道 2).如何使用匿名管道进行通信 3).匿名 ...
- Linux进程通信——匿名管道、命名管道、管道的特性和共享内存
Linux进程通信--匿名管道.命名管道.管道的特性和共享内存 一.管道 1.1 什么是管道? 1.2 匿名管道 <1> 匿名管道参数说明 <2> fork共享管道原理 < ...
- win32下进程间通信方式之管道、邮件槽、剪切板、共享内存、消息、套接字、RPC、DDE等
#include "stdafx.h"/*32位Windows采用虚拟内存技术使每个进程虚拟4G内存,在逻辑上实现了对进程之间数据代码的分离与保护.那么相应的进程之间的通信也就有必 ...
最新文章
- 进击谷歌:多线程下程序执行顺序怎么稳定不乱?
- mysql多列索引不全用,MySQL多列索引的生效规则
- could not export python function call python_value. Remove calls to Python functions before export
- 算法系列之图--DFS
- background-image 与 img 动画性能对比
- 解决配置linux环境每次重新连接都需要bash ~/.bashrc的问题
- ★LeetCode(104)——二叉树的最大深度(JavaScript)
- php if变量满足数组,在php中使用if()查看数组中的日期是否等于变量中存储的日期...
- bim计算机考试,“全国BIM技能等级考试”三级(建筑设计)样题
- android 音量调节不起作用,Android音量控制
- 80004005错误代码_80004005错误代码怎么回事_安装软件错误代码0×80004005如何解决...
- wps文字如何取消英文首字母输入时自动变大写
- Exp6 信息搜集与漏洞扫描 20164302 王一帆
- 解读小红书2022年母婴行业报告:心智种草的流量密码
- 第十五周翻译-《Pro SQL Server Internals, 2nd edition》
- JDBC之execute、executeQuery和executeUpdate之间的区别
- 随感——冬天走了、微软社区精英会议
- linux 存储映射lun 给_LINUX系统下添加映射存储LUN
- 如何让DIV元素永远居中显示
- IOS开发UI-------button
热门文章
- pandas滑动窗口防止nan出现
- (良心)世上最全设计模式导读(含难度预警与使用频率完整版)
- 再生龙u盘复制linux,再生龙制作U盘启动盘教程 | 楚盟博客
- linux texmaker编译,在Ubuntu下安装和编译LaTex
- java 重载 大于_详解java重载与覆写的区别
- c语言链表输出header中的乱码,大家帮忙看看这段代码,最后一个链表输出后总带一些乱码?请教...
- 七. jenkins部署springboot项目(4)-linux环境--远程调试
- 微信小程序 - 授权页面
- log4j2配置文件log4j2.xml
- laravel contains 的用法