linux-进程间通信
文章目录
- 进程通信的原因
- 管道
- 匿名管道
- 情况1
- 情况2
- 情况3
- 情况4
- 命名管道
- system V 共享内存
- key
- 共享内存的使用4步
- 1,创建。我们使用shmget函数来创建共享内存。
- 2, 我们使用shmctl删除共享内存。
- 3, 我们使用shmat将进程挂接到共享内存
- 4, 取消对共享内存的挂接
- system V 信号队列
- system V 信号量
- 互斥
- 二元信号量的作用
为什么你的0,1,2号文件默认打开?
- 因为bash打开了,后面的进程继承了它的file_struct,所以只要一个进程默认打开,其他的子进程都会默认打开。
为什么file结构体不会为子进程创建? - 因为file结构体属于文件,不属于进程。只是和进程关联起来罢了。
进程通信的原因
- 前面我们说过进程之间具有独立性,进程的数据独有。但这不是绝对的,有的时候我们也需要进程之间具有联系。我们称这为进程间通信。进程之间会有什么样的联系呢?
- 交换数据,一个进程将数据发送给另外一个进程。
- 资源共享,多个进程之间共享资源。
- 通知事件的完成,一个进程向另外的进程发消息,比如父进程wait子进程。
- 一个进程通过通信操控其他的进程。
管道
- 管道是一种比较古老和经典的进程间通信方法。
- 进程间通信的本质是让不同的进程看到同一块资源。而管道利用的是文件系统。在两个进程之间创建一个文件,将这个文件以读和写的不同方式打开,然后一个进程接管读端,一个进程接管写端。而这个文件我们称之为管道。这也是一个形象的说法。
- 但是管道是一个文件,那么往文件中写和读取数据就是IO,这非常浪费效率,所以实际的管道是内核的缓冲区,也就是说管道在内存中完成进程的通信。
匿名管道
管道分为匿名管道和命名管道,我们先来介绍第一种。
匿名管道的原理就是创建一个匿名管道,然后使用读和写的方式分别打开该文件,然后将产生的两个文件描述符分别用于不同的进程。这样就实现了进程的通信。
从文件描述符的角度理解管道: 我们先在父进程中使用pipe函数创建管道,pipe函数的参数是输出型参数,返回一个数组。该数组的0号元素是读端的文件描述符,1号元素是写端的文件描述符。如果成功返回0,失败返回-1.
然后父进程fork子进程,子进程继承了父进程的fd_array,这样子进程就和父进程的fd_array一样,指向相同的管道。
再关闭父子进程的一个文件描述符,就实现了父子进程的通信。
从内核的角度理解管道:pipe是文件,那么系统就会为pipe创建inode,创建file结构体。在file中有inode信息,而在inode中就标识了pipe是一个管道文件。而在file中封装的函数指针就会帮助我们读和写管道文件。
情况1
- 代码如下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>int main(){int pipefd[2] = { 0 }; //定义一个数组接收pipe的文件描述符if(-1 == pipe(pipefd)){ //pipe的同时,检查是否错误perror("pipe error");exit(1);}
pid_t id = fork(); //创建子进程if(id < 0){ //fork失败perror("fork error");exit(2);
}
else if(id == 0){ //子进程,这里我让子进程写数据close(pipefd[0]); //先将读端关闭,节省资源,避免误操作const char *message = "I am child .";while(1){write(pipefd[1], message, strlen(buf)); //不断的写入数据,然后睡5秒。sleep(5); }
}
else{ // 父进程,读取数据close(pipefd[1]);char *buf[64] = { 0 };while(1){ssize_t s = read(pipefd[0], buf, sizeof(buf) - 1); //读取数据,不睡眠buf[s] = 0;printf("get message : %s\n", buf);}
}
return 0;
}
- 子进程每次写入数据的时候都会睡上5秒,父进程是一直读取数据打印到显示器上。但是我们发现一个现象:终端上的信息也是每隔一段时间才打印,准确的说是等待子进程写入才开始打印,不是迅速的打印。
- 这里我们发现的第一个现象也是管道读写规则的一条:
- 当我们在进行读取的时候,如果读取条件不满足(写端不关闭文件描述符且不写入数据), 那么读端就会堵塞等待。
情况2
- 接下来我们看第二种情况,如果不停的写,每隔一段时间才读呢?
只改变父子进程的里面的代码,其他部分和第一段代码相同。
我们让子进程不停的打印,然后根据count判断打印的次数。然后让父进程每隔1秒读取1次。我们发现结果是
先是子进程疯狂的打印,然后子进程停止打印,父进程开始每隔1秒读取数据。为什么不是子进程一直打印呢?因为管道是一个文件,它的容量有限。子进程一瞬间将管道写满了,导致没有地方写入,只能堵塞。然后就该父进程打印了。
这里就有第二条规则: 当我们在进行写入的时候,如果写入条件不满足(读端不关闭文件描述符且不读取数据), 那么写端就会堵塞等待。
情况3
- 我们让子进程写入一段时间后就直接关闭写端,然后看看父进程的打印结果。
- 我们发现,读端的read的返回值不久一直是0,我们查看read的手册发现,返回值是0代表没有数据可读了,代表已经到了文件末尾,不会堵塞等待。
- 规则3 : 如果写端关闭文件描述符,那么读端读取完数据之后不会堵塞等待,会读取到文件末尾。
情况4
我们让读端先退出,然后让写端不断读取数据,我们发现最终写端也会退出!我们没有退出写端啊,为什么会接收到写端的退出码呢?因为你的读端已经退出,操作系统判断这个管道已经没有,占用资源,就会发射13号信号,帮你干掉写端的进程。你可以打印出写端进程的退出信号:
第四条规则:如果读端关闭文件描述符,那么写端不会堵塞等待,系统会直接发射13号信号终止写端进程。匿名管道的特点:
1, 只有拥有血缘关系的进程才可以通信,因为需要继承fd_array数组。
2,是流式服务。数据读取没有明显的界限。
3,进程退出,管道的生命周期结束。
4,系统会对管道的读取和写入做互斥和同步。前两条规则均衡了读写速度。不能边写边读,写和读只能一个一个来。
5, 只能单向通信。如果想要双向通信,需要建立双向管道。
命名管道
命名管道顾名思义,有名字的管道。它也是管道,那么实现的思想都是一样的,利用文件来实现。
我们上述讨论,发现匿名管道有一个致命缺陷:不支持非血缘关系的进程通信。那么如果我想让两个毫不相干的进程通信,匿名管道就不太好用。这里就轮到命名管道登场了。
命名管道在语言层面的使用是mkfifo函数,fifo就是先进先出,因为管道是流式的服务,所以符合先进先出。这个名字也很形象。
第一个参数就是文件的路径和名字,目的是找到并且执行文件。第二个参数是文件的权限。如果创建管道成功,则返回0,失败返回-1.简单的一比。
//客户端
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h> #define FIFO_FILE "./pipe"
int main(){ char message[64] = { 0 }; int fd = open(FIFO_FILE, O_WRONLY); // 以写方式打开while(1){ printf("Please Enter Message $"); fflush(stdout); ssize_t s = read(0, message, sizeof(message) - 1); //从键盘读取数据到message中 message[s] = 0; write(fd, message, strlen(message)); //写数据到管道中} return 0;
} //服务器1 #include <stdio.h> 2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/stat.h> 5 #include <fcntl.h>6 #include <stdlib.h>7 8 #define FIFO_FILE "./pipe" 9 int main(){10 umask(0);11 if (-1 == mkfifo(FIFO_FILE, 0666)){ //创建命名管道pipe12 perror("mkfifo error");13 exit(1);14 } 15 int fd = open(FIFO_FILE, O_RDONLY); //以读方式打开16 char buf[64] = { 0 };17 while(1){18 ssize_t s = read(fd, buf, sizeof(buf) - 1); //从管道中读取数据19 buf[s] = 0;20 if(s < 0){21 perror("read error");22 exit(1);23 }24 else if(s == 0){25 printf("read over.\n"); //如果写端退出,那么读端读到文件末尾,也跟着退出。26 break;27 }28 else{29 printf("get message from client # %s\n", buf);30 }31 } 32 33 return 0; 34 }
运行起来就是这种效果:
可以看到,pipe的类型是p(管道),它的大小是0,因为它在内核的缓冲区中,它不会出现在磁盘空间上。
命名管道除了在语言层面上的mkfifo,还有命令mkfifo,在命令行上直接生成一个管道。
我们先对pipo进行输出重定向,然后用输入重定向读取到一个文件中去。
system V 共享内存
- system V是一种标准,我放到了文章下面去讲解。你现在可以无视它。
- 管道是一种比较经典的做法,但是它也有明显缺陷。你会发现,在管道的操作中,使用了大量的系统调用接口,这一定会产生大量的拷贝。导致效率降低。
- 写入数据得将数据从键盘写入C语言缓冲区,然后从C语言缓冲区拿到管道(系统内核缓冲区),然后读取的时候将数据从管道拿到C语言缓冲区,再输出到显示器。这其中伴随着大量的拷贝数据。
- 而共享内存是一种减少拷贝的进程间通信方法。在某种层面上你甚至可以说它是最快的通信方式。进程间通信的本质是不同进程看见同一块资源,管道使用了文件这个不属于进程本身的东西,而共享内存选择在内存中再开辟一块空间,这块空间是进程共享,本身属于进程,属于使用的是用户级的缓冲区。这样进程的写和读几乎没有拷贝。
- 再虚拟地址中有一块空间是共享区,这块空间有动态库,而共享内存也在这里。不同的进程通过页表映射到自己的共享区,然后映射到同一块物理内存,这块物理内存属于这两个进程,也就是说,A写入的数据,实际上是往B上写入。而B读取数据,实际上就是读取的A的数据。
- 而且管道一般是两个进程之间进行通信,而共享内存支持多个进程之间进行通信,只要内存同时被这几个进程共享即可。
- 共享内存也会被映射到物理内存,最终还是要操作系统来分配这块内存。而如果有多对正在通信的进程,那么就应该被管理起来,而管理的方法自然是先描述再组织。 在内核中也有对应的数据结构描述共享内存。
- 创建共享内存的步骤大致是:1,分配内存,创建对应的数据结构shmid_ds。2,将要通信的进程挂接到共享内存上。3,通信结束,结束挂接。4,删除共享内存。
key
- A进程和B进程想要进程通信。但是内存中有很多块共享内存,如果保证它们的共享内存是同一块呢?在描述共享内存的数据结构struct shmid_ds{}中,存储着标识共享内存的信息,这个信息就是key。一个共享内存有独一无二的key值,不同的进程只要找到相同的key值,就可以共享一块内存。
- 而在linux中,我们有函数ftok帮助我们生成独一无二的key值。这个函数的参数是任意的,几乎没有经过内核的操作,完全是返回一个key值罢了。
- 可以看到ftok的返回值是一个System V的共享内存的标识id,也就是key值。
共享内存的使用4步
1,创建。我们使用shmget函数来创建共享内存。
- shmget的第一个参数就是我们的key值。第二个参数是共享内存的大小,系统为我们分配的时候,是按照页为单位分配的,所以我们最好分配4096字节的整数倍。第三个参数shmflg就是创建的方式,我常用的是IPC_CTRAT和IPC_EXCL。
- IPC_CREAT意味着如果key对应的共享内存不存在则创建共享内存。
- IPC_EXCL意味着如果共享内存存在,则报错返回。如果不带上这个选项,那么如果共享内存存在会打开它。
- linux下一切皆文件。共享内存也可以看作文件,所以shmflg也可以带上权限。
- RETURN VALUE:返回值是一个共享内存id,这个id连接着key值。你可能会问,我们已经有了key值,为什么还要shmid?key值是系统用来标识的,我们的shmid是用户层面的标识。如果报错,那么shmid就是-1。
// shared.h
#pragma once
#define PATHNAME "./share"
#define PROJ_ID 0xff
#define SIZE 4096// server.c, 1 #include <stdio.h>2 #include <sys/shm.h>3 #include <sys/types.h>4 #include <sys/ipc.h>5 #include "share.h"6 #include <stdlib.h>7 8 int main(){9 key_t key = ftok(PATHNAME, PROJ_ID); //产生一个独一无二的key值10 11 int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存,权限是66612 if(shmid < 0){ //创建失败13 perror("shmget");14 exit(1);15 }16 printf("the shmid is %d, the key is %p\n", shmid, key);17 }
- 可以看到运行两次后,就出错了。这也证明了IPC_EXCL的作用。
- 我们可以直接在命令行上查看共享内存的情况:
ipcs -m # 查看共享内存的情况
ipcrm -m shmid #删除共享内存,
- 第一列是key值,shmid就是key值转换的共享内存id号。权限就是我们设置的权限。而字节就是我们给的大小。一般最好给4096的整数倍。nattch,number attch,连接这个共享内存的进程数量。
- 我们刚刚运行了server文件,产生了一段共享内存。当进程结束时我们查看,发现一个现象:共享内存竟然没有随着进程的结束而销毁!!!
- 管道是属于文件系统的,文件的生命周期跟随进程,所以进程结束,管道也释放资源。但是IPC资源的生命周期随内核,不随进程。如果不手动删除共享内存,那么这块资源不会被释放。所以需要我们手动释放,否则会造成资源浪费。
- 上面是我们在命令行上的手动删除共享内存。那么在代码层面有没有方法呢?有的。
2, 我们使用shmctl删除共享内存。
- 你可能对它的名字感到疑惑:shmctl?共享内存控制?为什么不是shmdel?因为这个函数的功能很强大,删除共享内存只是它的冰山一角,对共享内存的增删查改都由它来做。它的功能就是共享内存控制。
- 第一个参数就是shmget的返回值,shmid。cmd就是我们要shmctl执行的命令,我们只需要使用IPC_RMID就可以,这是删除指令。
- 最后一个参数是shmid_ds结构体,这是系统为管理共享内存创建的数据结构,这个参数的作用是你可以自定义一个shmid_ds结构体对象,然后这个共享内存的属性就根据你的来设置。不过我们一般使用默认的就可以,所以这个参数一般是NULL。
//这段代码跟上面的server.c
int main(){//sleep(10);shmctl(shmid, IPC_RMID, NULL);sleep(10);
}
- 我们使用脚本来检测共享内存的使用情况:
- 结果如我们所想:最先是系统中没有共享内存。等server运行起来,产生一个共享内存。10秒钟后,共享内存被删除,再过10秒,进程结束。
3, 我们使用shmat将进程挂接到共享内存
我们前面已经创建出了共享内存,并且掌握了删除共享内存的方法。但是我们接下来完成进程通信还需要挂接进程到共享内存上。
哪个进程调用这个函数,则挂接哪个进程。
第一个参数就是shmid,不再解释。第二个参数shmaddr代表你想要挂接的虚拟地址,也就是说你想要你的页表将共享内存映射到哪块虚拟地址上。一般我们直接给NULL,使用默认就行。第三个参数shmflg表示挂接的共享内存的使用方式,,比如只读,只写,我们也使用默认的(读写)就行,直接给0.
RETURN VALUE:如果你使用过malloc,那么这个返回值就很好理解。共享内存在进程的虚拟地址空间中会有映射,而返回值的void*指针就指向这段虚拟地址的起始地址。
4, 取消对共享内存的挂接
我们使用shmdt(shared memory dattach),这个函数就是取消当前进程对共享内存的挂接。
使用方法跟free一样,非常简单,它的参数就是shmat的返回值,也就是虚拟空间的首地址。它的作用仅仅是去除进程对共享内存的挂接,而非删除共享内存,这两点要分清楚。
所有代码:
1 #include <stdio.h> 2 #include <sys/shm.h>3 #include <sys/types.h>4 #include <sys/ipc.h>5 #include "share.h"6 #include <stdlib.h>7 #include <unistd.h>8 9 int main(){10 key_t key = ftok(PATHNAME, PROJ_ID); //拿到独一无二的key值11 12 int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建共享内存13 if(shmid < 0){14 perror("shmget");15 exit(1);16 }17 18 printf("the shmid is %d, the key is %p\n", shmid, key);19 sleep(5);20 char *str = (char*)shmat(shmid, NULL, 0); //将当前进程挂接到共享内存21 22 sleep(10);23 shmdt(str); // 取消挂接24 sleep(5);25 shmctl(shmid, IPC_RMID, NULL); //删除共享内存26 sleep(10);27 28 sleep(5);29 return 0;30 }
我在每个阶段中间都穿插了sleep,仅仅是为了我的脚本能看到现象。也可以去掉所有的sleep。
上面就是对一个进程而言,一个共享内存由创建到删除的所以过程。但是我们的目的是完成进程间通信,所以我们需要再来一个客户端,让client和server通过共享内存产生通信。
clinet需要与server相同的key值,所以ftok的参数在两个进程中必须相同。
通过运行程序我们发现确实使用共享内存实现了进程间的通信。但是我们还发现即使写的速度很慢,它的读取速度却不会被影响。这与管道不同。
共享内存底层不提供任何同步与互斥机制。也就是说如果A和B进程使用共享内存通信,那么A进程不知道有B进程的存在,它只会做自己的事。共享内存是纯的通信。如果想要加上控制,需要信号量。
system V 信号队列
- 信号队列是另外一种通信方式。由系统为我们创建一个消息队列,一个进程将数据写入队列,另外一个进程读取消息队列。借由队列这种数据结构完成进程间通信。
- 你可能对system V感到疑惑,这是什么呢?别急,下面为你解答。
- 消息队列的创建方式是使用函数msgget,
- 你是否对这个函数的参数感到熟悉?如果有多个进程之间通信,需要多个消息队列,那么系统就需要管理这些消息队列,而每个消息队列都需要一个key值来标识。这个函数的参数不就是shmget的参数吗?
- 消息队列的删除方法:
- 你是否对这个函数感到熟悉?没错,这个函数的参数和shmctl的参数一样。而且,第三个参数msqid_ds就是操作系统描述消息队列的数据结构,如果你看过它的源码,你会发现它里面的成员布局和shmid_ds类似!
system V 信号量
- 我们先来看一看信号量的创建和删除,
- 没错,信号量也有key值,函数接口跟上面的一样。那你可以试想一下信号量的删除函数,
- 描述信号量的数据结构是semid_ds,没错,你发现共享内存,消息队列,信号量的函数接口,参数,数据结构几乎一样,这就是system V,它是一个标准,意味着你写的进程通信方式要按照这个标准来。
互斥
- 我们前面说过共享内存不支持同步和互斥。这里我们先说一说互斥的概念。
- A和B进程在进程通信,A进程写到一半的时候,B进程也写入数据。这是不被允许的。也就是说一个进程在享有资源进行某些操作的时候,另外的进程不应该打扰它的操作。
- 系统中某些资源一次只允许一个进程使用,这样的资源叫做临界资源。
- 在进程中涉及对临界资源操作的程序段叫做临界区。
二元信号量的作用
所以我们需要想方设法使得临界资源每次只能一个进程使用,而我们采用的方法是给临界区上锁。这个说法很形象,就像你在上厕所,会把门关上,等你完事再打开门。信号量就是一种锁。
信号量分为二元和多元信号量,我们主要说一说二元信号量。二元信号量就是一种只有两种状态的锁。
比如进程1和进程2都想访问一段空间。都想进行写入操作。但是写入操作互斥。所以我们给进程1和进程2的临界区都加上锁,谁先到,谁就上锁。等完事,再解锁。然后下一个进程再上锁,解锁。
但是,我们再仔细想一想。如果想要完成互斥,需要所有想要使用这块资源的进程看到同一把锁,一把锁在某一时刻只能一个进程使用。临界区是对临界资源操作的程序段,所以,锁也是一种临界资源!所以,锁在保护进程段之前需要保护自己!所以锁必须是原子操作的。
什么是原子操作呢?i++需要几步?将内存中的i加载到cpu的寄存器中,将i++,将结果返回内存。需要三步!!原子操作是指一个操作只有两种状态,有或者没有,而i++需要3步,中间的那一步就是不定的状态。你可以粗暴的理解为原子操作就是汇编只有一句的操作!
ipcs -m #查看共享内存
ipcs -s #查看信号量
ipcs -q #查看消息队列# 删除
ipcsrm -m / -s --q
linux-进程间通信相关推荐
- linux进程间通信:POSIX 共享内存
文章目录 思维导图 通信原理 优势 POSIX 共享内存 编程接口 编程案例 思维导图 之前学习过sysemV 的共享内存的实现及使用原理,参考linux进程间通信:system V 共享内存 POS ...
- linux进程间通信:POSIX 消息队列 ----异步通信
在上一篇中linux进程间通信:POSIX 消息队列我们知道消息队列中在消息个数达到了队列所能承载的上限,就会发生消息的写阻塞. 阻塞式的通信影响系统效率,进程之间在通信收到阻塞时并不能去做其他事情, ...
- linux进程间通信:POSIX 消息队列
文章目录 基本介绍 相关编程接口 编程实例 消息队列通信实例 消息队列属性设置实例 基本介绍 关于消息队列的基本介绍,前面在学习system V的消息队列时已经有过了解,linux进程间通信:syst ...
- linux进程间通信:system V 信号量 生产者和消费者模型编程案例
生产者和消费者模型: 有若干个缓冲区,生产者不断向里填数据,消费者不断从中取数据 两者不冲突的前提: 缓冲区有若干个,且是固定大小,生产者和消费者各有若干个 生产者向缓冲区中填数据前需要判断缓冲区是否 ...
- Linux进程间通信--进程,信号,管道,消息队列,信号量,共享内存
Linux进程间通信--进程,信号,管道,消息队列,信号量,共享内存 参考:<linux编程从入门到精通>,<Linux C程序设计大全>,<unix环境高级编程> ...
- Linux进程间通信(二):信号集函数 sigemptyset()、sigprocmask()、sigpending()、sigsuspend()...
我们已经知道,我们可以通过信号来终止进程,也可以通过信号来在进程间进行通信,程序也可以通过指定信号的关联处理函数来改变信号的默认处理方式,也可以屏蔽某些信号,使其不能传递给进程.那么我们应该如何设定我 ...
- 20155301 滕树晨linux基础——linux进程间通信(IPC)机制总结
20155301 滕树晨linux基础--linux进程间通信(IPC)机制总结 共享内存 共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,它将出现在 ...
- Linux 进程间通信
引言 进程通信的目的: 数据传输 一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间 共享数据 多个进程想要操作共享数据,一个进程对共享数据 通知事 一个进程需要向另一个或一 ...
- Linux进程间通信中的文件和文件锁
Linux进程间通信中的文件和文件锁 来源:穷佐罗的Linux书 前言 使用文件进行进程间通信应该是最先学会的一种IPC方式.任何编程语言中,文件IO都是很重要的知识,所以使用文件进行进程间通信就成了 ...
- linux 进程间通信 dbus-glib【实例】详解四(上) C库 dbus-glib 使用(附代码)(编写接口描述文件.xml,dbus-binding-tool工具生成绑定文件)(列集散集函数)
linux 进程间通信 dbus-glib[实例]详解一(附代码)(d-feet工具使用) linux 进程间通信 dbus-glib[实例]详解二(上) 消息和消息总线(附代码) linux 进程间 ...
最新文章
- cad画流程图的插件_CAD制图太慢?62款辅助插件汇总,款款精品,效率提升80%
- 关于学习Python的一点学习总结(37->集合运算)
- Oracle-11g 从表空间删除数据文件
- 对于前端js框架对于事件处理的应用场景探讨
- 这代码她不美吗?——试题 基础练习 十六进制转八进制
- 阿里合伙人程立:阿里15年,我撕掉了身上两个标签
- [开源] 使用 Python 轻松操作已存在的表
- 采用GDI生成Code39条形码
- C语言使用信号量(Linux)
- 直播app开发中视频编码标准发展史
- Word小技巧:图片批量裁剪与大小调整
- 直接将ADB授权写入到手机的方法(手机需要有root权限)
- php 解析array,深度解析PHP数组函数array_slice
- Burp Spider 使用指南
- CSDN上代码块背景颜色的设置
- 电子商务格局下的营销未来
- 2030肢解中国-美国全球战略与中国危机(戴旭)
- python——class.__dict__
- 抢滩企业电子商刊(杂志)中国市场 iebook超级精灵全球发布
- 双目三维测距(python)
热门文章
- 本地web项目如何使用外网访问?教你轻松使用cpolar在windows搭建内网穿透
- Shp2osm:shp转换为osm格式文件
- 用Python串口实时显示数据并绘图
- 在设备树中时钟的简单使用
- 京东主图怎么保存原图_怎么把京东商城宝贝评价里面的图片保存下来
- Windows平台七牛批量上传工具使用教程
- (杭电2188)选拔志愿者
- 彬彬偷偷告诉了平行世界的其他杰哥们这个世界里的杰哥已经得到了阿伟,于是他们也来到了这个世界想要教阿伟登Dua郎,现在他们“成群杰队”地赶来了!
- oracle触发器报错语法,Oracle 触发器
- vue使用el-tabs实现标签页(内存+vuex)