【Linux操作系统】--进程间通信--匿名管道和命名管道
2022/10/17 5:23:57
本文主要是介绍【Linux操作系统】--进程间通信--匿名管道和命名管道,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
进程间通信介绍
有时候进程之间可能会存在特定的协同工作的场景!那么进程之间的协同工作,就是进程之间的通信。进程的通信就是一个进程要把自己的数据交付给另一个进程,让其进行处理。因为进程是具有独立性的,如果要进行通信,那么通信双方一定是通过某种介质来进行通信。比如我跟你通信是通过某信进行交流。所以进程间通信的介质时操作系统,操作系统要设计通信方式。
因为进程是具有独立性的!并且交互数据,成本很高。一个进程是看不到另一个进程的资源,所以必须得先看到一份公共的资源,这里的资源就是一段内存,这个公共资源是属于操作系统的。
所以进程间通信的前提本质:其实是由OS参与,提供一份所有通信进程都能看到的公共资源。这段内存提供公共资源的方式可能以文件方式提供,也可能以队列方式提供,也可能提供的就是原始的内存块。这也是通信方式很多种的原因。
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程 资源共享:多个进程之间共享同样的资源。 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信发展
- 管道 System V进程间通信 POSIX进程间通信
匿名管道
什么是管道
当创建了父子进程,因为父子进程是独立的两个进程,进程的PCB块是描述进程的控制块,其中也包括了文件操作符数组。当我们要向文件写入信息,要传入一个文件操作符,通过这个文件操作符fd,来查找对应的文件。就比如下图的文件属性结构体它的文件操作符是3,如果要向3这个文案金写入内容,那么就通过PCB里面的文件指针找到文件操作符数组,再通过文件操作符数组最硬的3找到对应的文件属性。
因为进程具有独立性,所以父子进程是两个独立的进程。因为是独立的进程,所以子进程要将父进程的所有内容拷贝一份,其中也包括文件操作符数组。但是文件属性结构体并不属于进程,而属于操作系统的,因为操作系统要向磁盘读取文件,就要把文件的信息都读入操作系统,这些属性放在一个结构体中,struct file和进程只是有关系,但并不属于进程。所以文件属性结构体不用复制两份。
当对文件进行写操作时,比如说write(3,"hello world");找到文件结构体后,在文件属性中找到对应的写操作,将进程缓冲区的“hello world”写到OS缓冲区中,然后找到磁盘驱动对应的写操作。进而将OS缓冲区的“hello world”写进磁盘文件中。
当父子进程都是指向同一块文件结构体,这就是操作系统参与,让不同的进程看到统一个内容,操作系统就起到了媒介作用。当父进程将“hello world”写进OS的内核缓冲区中,不刷新磁盘,不调用底层磁盘驱动的读写方法,将“hello world保留在缓冲区中,那么另一个进程子进程就可以通过它的文件描述符找到对应的同一个struct file,找到同一个缓冲区的数据。此时就做到了将一个进程的数据交给下一个进程,这就叫做让不同进程看到同一份资源,这种基于文件的通信方式叫做管道。
站在文件描述符角度-深度理解管道
第一步:父进程创建管道。创建管道用到的系统结构是pipe,它的参数是一个输出型参数:我们想通过这个参数读取到打开的两个fd。通过传递一个数组,数组会发生降维,传递一个数组就是传递一个指针,最后pipe将数据写入数组中,我们就能拿到对应的文件描述符fd。
pipe的返回值,文档告诉我们,如果返回0创建fd成功,如果失败返回-1.
第二步:父进程fork出子进程。分别以读方式和写方式打开一个管道,实际上这里的管道可以看作内核的缓冲区。在同一个进程中,文件可以被打开两次。就像管道,即可以读文件,又可以写文件,但我们通常只读或只写,不会两个都做。
管道是一个只能单向通信的通信信道,想要双向通信就建立两个管道。
第三步:如果子进程写,父进程读。子进程将读文件fd[0]关闭,父进程将写文件fd[1]关闭。
匿名管道
建立信道
第一步:创建管道
因为管道是对两个进程进行通信,所以数组有两个描述符。我们将它初始化为0。当管道创建完毕,我们可以知道这两个描述符是3和4,因为0,1,2是stdin,stdout,stderr,所以第一个最小空文件描述符是3,从3开始。
那么创建好的数组后,pipefd[0]=3,pipefd[1]=4。规定0下标代表读,1下标代表写。
#include <stdio.h> #include <unistd.h> int main() { int pipefd[2]={0}; if(pipe(pipefd)!=0)//如果返回的不是0,创建失败,打印错误信息,然后返回错误码 { perror("pipe error!"); return 1; } printf("pipe[0]:%d ",pipefd[0]); printf("pipe[1]:%d ",pipefd[1]); return 0; } #写一个makefile文件 [wjy@VM-24-9-centos pipe]$ cat makefile pipe_process:pipe_process.c gcc -o $@ $^ -std=c99 .PHONY:clean clean: rm -f pipe_process #运行结果 [wjy@VM-24-9-centos pipe]$ make gcc -o pipe_process pipe_process.c -std=c99 [wjy@VM-24-9-centos pipe]$ ./pipe_process pipe[0]:3 pipe[1]:4
第二步:创建父子进程,并让父进程读,子进程写。
创建了父子进程,那么就创建好了双向通信的信道。当我子进程写,关闭读pipefd[0]文件;当父进程读,关闭写pipefd[1]文件,这样才建立好管道,可以进行通信了。
[wjy@VM-24-9-centos pipe]$ cat pipe_process.c #include <stdio.h> #include <stdlib.h>//exit的头文件 #include <unistd.h> int main() { int pipefd[2]={0}; if(pipe(pipefd)!=0)//如果返回的不是0,创建失败,打印错误信息,然后返回错误码 { perror("pipe error!"); return 1; } printf("pipe[0]:%d ",pipefd[0]); printf("pipe[1]:%d ",pipefd[1]); //父进程读,子进程写。 if(fork()==0) { //子进程写,所以关闭读pipefd[0]文件 close(pipefd[0]); } //父进程读,所以关闭写pipefd[1]文件 close(pipefd[1]); return 0; }
开始操作-管道基本特性
特性1:
当我们要将内容写入子进程,因为pipefd数组都是文件描述符,所以我们进行读写还可以用系统调用接口read和write
子进程进行写入,每隔一秒写入一次。父进程进行读操作时候,将文件读入buffer缓冲区,然后再进行打印。这里可以发现,父进程是不断读的,子进程是间隔写的,也就是说父进程读的块,子进程写的慢。
然后运行程序打印结果,成功输出,这就是子进程将数据通过管道发送给父进程。
if(fork()==0)//子进程 { close(pipefd[0]); const char* msg="hello world!"; while(1) { write(pipefd[1],msg,strlen(msg));//这里的msg不需要+1【strlen(msg+1)】 sleep(1); } } //父进程 close(pipefd[1]); while(1) { char buffer[64]={0}; ssize_t s=read(pipefd[0],buffer,sizeof(buffer));//read的返回值为0,通常情况下文件读取结束,在这里代表子进程关闭文件描述符了。 //ssize_t 是一个有符号整数 if(s==0)//读取结束 break; else if(s>0)//读取成功 { buffer[s]=0;//读取的字符串以0结尾,遇到0结束。 printf("child say#%s ",buffer); } else //s<0读取失败,直接退出 break; } return 0; } #运行结果 [wjy@VM-24-9-centos pipe]$ ./pipe_process pipe[0]:3 pipe[1]:4 child say#hello world! child say#hello world! child say#hello world! child say#hello world! ichild say#hello world! child say#hello world! ^Z
当我们让子进程不休眠,一直不断的写入;父进程会休眠1秒,间接性的读取数据,每隔一秒读一次数据,我们再看一下结果。
父进程read向buffer缓冲区中少写一个字符,因为子进程不断写入,父进程每次读取都会写满整个缓冲区,这样的话没有地方放下面设置的结尾字符,所以要留出一个空间给到。
if(fork()==0) { close(pipefd[0]); const char* msg="hello world!"; while(1) { write(pipefd[1],msg,strlen(msg));//这里的msg不需要+1【strlen(msg+1)】 // sleep(1); } } close(pipefd[1]); while(1) { sleep(1); char buffer[64]={0}; ssize_t s=read(pipefd[0],buffer,sizeof(buffer)-1);//这里要少获取一个字符,用来给下面设置的结尾 if(s==0)//读取结e束 { printf("child quit... "); break; } else if(s>0)//读取成功 { buffer[s]=0; printf("child say#%s ",buffer); } else //s<0 { printf("child error... "); break; } } //运行结果 [wjy@VM-24-9-centos pipe]$ ./pipe_process pipe[0]:3 pipe[1]:4 child say#hello world!hello world!hello world!hello world!hello world!hel child say#lo world!hello world!hello world!hello world!hello world!hello child say#world!hello world!hello world!hello world!hello world!hello wor ^Z
最后发现,结果打印是一行一行显示的,而不是一个字符串的显示。这是因为pipe里面write只要有缓冲区,就一直写入。read在读取的时候,只要有数据就可以一直读取。这种特性就是字节流。这是管道的第一个基本特性.其实这个就是匿名管道,但是讲到现在,并不能特别体现出它是匿名管道,在对比了命名管道,我们才能体会到匿名管道的特性。 匿名管道特性: 管道是一个只能单向通信的通信信道。 管道是面向字节流的!
特性2:
我们让子进程不断写入字符a,写入的时候计数。父进程不将数据读取出来。我们查看结果,发现子进程到65536就不写了,这个打出的值就是我们具体写出来多少字节。
这个65536就是64*1024,也就是64KB。写端write写满64KB的时候就不再写入了,因为管道有大小,我用的是云服务器测试,云服务器的管道大小是64KB.为什么当写端write写满64KB的时候不写了?明明服务器在写满64KB后可以将还没有读出来的数据覆盖再写入,你不读我再刷一遍。实际上现在的技术是可以做到的,但是为什么没有这么做呢?因为要让读端来读,第一如果数据还没有被读出来就覆盖没有了,那么以前读操作所做的一切就白干。第二我们实际上是跟读来协作的,当对方没有来得及读的时候,覆盖了就不能再写了。不写的本质是我要等对方来读。
if(fork()==0)//写 { close(pipefd[0]); int count=0; while(1) { write(pipefd[1],"a",1); count++; printf("count:%d ",count); } exit(0); } close(pipefd[1]); while(1) { sleep(1); }
当从管道中读取数据,先读64个,发现读64个后,子进程不会向管道写入。当我们将父进程缓冲区大小变成4*1024个大小,4KB,子进程开始写入了。
父进程读取几个/64个都不行,子进程都不能写入,父进程必须写入4KB子进程才写入。所以管道的第四个特点:当管道还有数据,就一直读,直到管道数据被读完之后才能继续向管道中写,所以管道自带同步机制。
if(fork()==0)//写 { close(pipefd[0]); int count=0; while(1) { write(pipefd[1],"a",1); count++; printf("count:%d ",count); } exit(0); } close(pipefd[1]); while(1) { sleep(10); char c[1024*4+1]={0}; ssize_t s=read(pipefd[0],c,sizeof(c)); c[s]=0; printf("father take:%c ",c[0]); }
在pipe中有一个pipe capacity,Linux系统下是64KB,而pipe_BUF在linux系统下是4KB。
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。 当要写入的数据大于PIPE_BUF时,linux将不再保证写入的原子性。
什么是原子性,当管道写满的时候,读出数据需要一批一批的读出,这一批的大小就是4KB.我们可以来验证一下,当父进程一次读取2KB,那么是不会写入的,当第二次再读,将4KB都读取出来,那么子进程开始写入。所以管道写入唤醒是有管道策略的。
当子进程只写入一行数据,读数据不断读,出现的结果是读出了写入的一行数据之后,就读完文件了,那么read返回0,就读不出来数据了。
当子进程写入不断写入数据,父进程读数据只读把管道数据读出之后就退出父进程,读文件关闭,那么当父进程关闭后,子进程也会随之关闭。虽然子进程一直在写入数据,但是当我们关闭读端。写端还在写入,此时站在OS的层面,这是严重不合理的,已经没有人读了,你还在写入,本质就是浪费OS的资源,OS会终止写入进程!
//不断读入 void test2(int pipefd[]) { if(fork()==0)//写 { close(pipefd[0]); const char* msg ="hello world"; while(1) { write(pipefd[1],msg,strlen(msg)); sleep(10); break; } close(pipefd[1]); exit(0); } close(pipefd[1]); while(1) { char c[64]={0}; ssize_t s=read(pipefd[0],c,sizeof(c)); if(s>0) { c[s]=0; printf("father take:%s ",c); } else if(s==0) { printf("writer quit... "); break; } else break; } } //不断写入 void test3(int pipefd[]) { if(fork()==0) { close(pipefd[0]); const char* msg="hello world"; while(1) { write(pipefd[1],msg,strlen(msg)); } close(pipefd[1]); exit(0); } close(pipefd[1]); while(1) { char c[64]={0}; ssize_t s=read(pipefd[0],c,sizeof(c)); if(s>0) { c[s]=0; printf("father take:%s ",c); } else if(s==0) { printf("write quit.. "); break; } else break; break; } close(pipefd[0]); }
那么父进程结束,子进程怎么结束的呢?OS通过给目标进程发送信号SIGPIPE!
当父进程退出,子进程也跟着退出。子进程还在不断写入时候就被退出了,这也算异常。那么父进程可以用waitpid读取到子进程的退出状态。那么如何查看子进程如何退出的呢?
总结:
4种情况:
- 读端不读或者度的慢,写端要等读端
- 读端关闭,写端受到SIGPIPE信号直接终止
- 写端不写或写的慢,读端就要等写端
- 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾。
匿名管道5个特点:
- 管道是一个只能单向通信的通信信道
- 管道是面向字节流的
- 仅限于父子通信种--具有血缘关系的进程进行进程间通信
- 管道自带同步机制,原子性写入
- 管道的生命周期是随进程的。
命名管道
为了解决解决匿名管道只能父子通信,引入了命名管道。命名管道和匿名管道非常相似,出了一点匿名管道需要在父子间进行通信,而命名管道就是为了解决这个问题的。
命名管道的创建需要用mkfifo,这就是创建命名管道的命令。它的权限是以p开头的,这种文件我们称之为命名管道。
当我们打开两个服务器,两个服务器就是两个进程,当在一个进程中写入内容,另一个内容读取信息,我们通过这个管道从一个进程输入内容,在另一个进程显示出来。
命名管道
进程是具有独立性的,进程通信的成本其实比较高,必须先解决一个问题:让不同的进程看到同一份资源,这个公共资源有可能是内存文件,内存或队列,并且一定需要OS来提供。匿名管道pipe本质上是通过子进程继承父进程资源的特性,达到一个让不同的进程看到同一份资源。
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。
一个磁盘文件,我们怎么来标识它呢?
我们是通过路径+文件名的方式,并且这个路径具有唯一性。Linux的目录结构是一个树状结构,只要是树状结构,那么它一定有众多子节点,但是任何一个子节点或叶子节点向上追溯时,它只能由一个父节点。所以我们采用路径的方式,是可以标定该文件的。
A进程向内存中的文件进行写入,但是并不把数据刷新到磁盘上,然后让B进程在内存中读取数据。这样就实现了一份资源让两个进程操作,达到了进程间的通信。那么A和B两个进程时如何看到并打开同一个文件呢?就是通过路径+文件名的方式。这样通过文件+路径名的方式让两个进程看到同一份资源的方式就是命名管道。所以我们必须给管道起一个名字来确定它的唯一性。
创建一个命名管道
- 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
- 命名管道也可以从程序里创建,相关函数有
第一个参数是管道的名称,第二个参数是管道的权限设置。
返回值:当成功就是0,失败是-1,出错之后会设置错误码。
编写一个命名管道
我们创建两个文件用来进行管道通信
[wjy@VM-24-9-centos fifo]$ touch client.c [wjy@VM-24-9-centos fifo]$ touch server.c
makefile文件
因为需要同时执行两个文件,而make只能执行makefile文件中的第一个命令,所以我们用一个依赖方法:.PHONY:all。
[wjy@VM-24-9-centos fifo]$ cat makefile .PHONY:all all:client server client:client.c gcc -o $@ $^ server:server.c gcc -o $@ $^ .PHONY:clean clean: rm -f client server
创建管道
在当前目录下,创建一个名为fifo管道,MY_FIFO是重命名,它的权限是0666,如果管道的返回值结果小于零,说明创建管道失败,直接打印perror并返回1。
当我们make编译再运行后,用ll查看文件属性,管道fifo已经创建好了,发现它的权限变成了0664,这是为什么?
这是因为mkfifo在创建管到时,受系统的umask影响。umask也是一个系统调用接口。当创建文件的时候,我们自己自定义的权限设置是要受umask影响,所以我们将server.c的umask设为0就可以。
[wjy@VM-24-9-centos fifo]$ umask 0002
[wjy@VM-24-9-centos fifo]$ cat server.c #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #define MY_FIFO "./fifo" int main() { umask(0); if(mkfifo(MY_FIFO,0666)<0) { perror("mkfifo"); return 1; } return 0; }
当我们再次编译运行,发现创建失败,告诉我们file exists.但是我们不想用老的,只想用新的。所以我们在makefile文件的rm命令中加上fifo文件,清除一下,再编译就可以了。
服务器端读取
命名管道通信非常简单,只需要文件操作即可。我们一般用的是系统调用接口,如果用C/C++的也无可厚非,但是会有一些缓冲区的干扰。如果用系统调用接口,那么数据直接写入OS中的缓冲区中;如果用C/C++的数据将先存储在C/C++的缓冲区,然后系统自己调用系统调用接口写进OS缓冲区。所以还是直接采用系统调用。
我们使用客户端来读取数据:open打开返回文件描述符,如果打开文件失败文件描述符fd<0,打印错误信息,并返回。
int fd=open(MY_FIFO,O_RDONLY); if(fd<0) { perror("open"); return 2; }
下面我们来进行逻辑业务,也就是从文件中读取数据放入我们自定义的缓冲区buffer。我们不断读,并不断打印出来。
read系统调用接口是传文件描述符来找到文件的,read的返回值返回的是实际读取到的个数,如果返回值大于0,那么对方还在写入数据,管道里数据没有读完;如果返回值s等于0,那么管道里没有数据了,对面的客户端也不再进行写入。如果小于0说明出错了,直接break退出去。
//业务逻辑 while(1) { char buffer[64]={0}; ssize_t s=read(fd,buffer,sizeof(buffer)-1); if(s>0) { buffer[s]=0; printf("client#%s ",buffer); } else if(s==0) { printf("client quit... "); break; } else { perror("read"); break; } }
客户端client写入
当服务器端已经创建好了管道,那么客户端写入端是不用再创建管道的,只需要获取即可。因为都要用到管道的名字,所以我们创建一个公共的代码端,包含它的头文件即,这样写入方便一点。
[wjy@VM-24-9-centos fifo]$ cat comm.h #pragma once #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #define MY_FIFO "./fifo"
写端业务逻辑
先打开管道文件,创建文件描述符。
因为写端还没有数据,所以从stdin文件输入流读取数据,因为stdin对应的文件描述符是0,所以read时要从0读取数据,读到buffer缓冲区中,大小最后要-1,因为读取出来是字符串,要以结尾,最后预留的就是放用的。如果读取成功,读取到的数据数量大于0,也就是read返回值大于0,那么我们讲内容写进管道文件当中。
//客户端不用创建fifo了,只需要获取即可 int fd=open(MY_FIFO,O_WRONLY);//不需要O_CREAT,因为管道已经被读端创建好了 if(fd<0) { perror("open"); return 1; } //业务逻辑 while(1) { char buffer[64]={0}; ssize_t s=read(0,buffer,sizeof(buffer)-1); if(s>0)//读取成功 { buffer[s]=0; printf("%s ",buffer); write(fd,buffer,strlen(buffer));//不需要-1,不会把写进去的。 } } close(fd);//最后不写了,要把文件关闭
写到这,我们可以测试一下
那么这里写入端输入后为什么读端会多处一行呢?这是因为键盘输入的时候, 也是输入字符的一部分,它在整个缓冲区的最后面。所以让缓冲区的最后一个字符变成即可,也就是buffer[s-1]
然后在client写端加上一个提示符,这样知道我们正在读取。printf是向标准输出stdout写数据,也就是向显示器写数据。因为printf写输入会先写入C语言的缓冲区,因为没有 刷新到OS缓冲区,所以用fflush将提示语句刷新到OS缓冲区中,也就是刷新到管道中。这样提示语句写进了管道。
此时这个管道运行成功。
当我们让读端5秒之后再读取数据,那么写入的数据会存储在管道里。当我们每次写数据的时候,会发现管道的大小都是0,这就证明了向管道写入数据时存在OS的缓冲区中,而并没有刷新到磁盘上。
拓展功能
以上我们成功写出命名管道,那么我们在读端加入一些功能。
当从读取到的字符串是show,也就是写端写入了show命令,那么我们将Linux上ls命令结果打印出来,怎样打印出来呢?通过创建子进程进行程序替换,在子进程替换的过程中,父进程等待子进程执行完命令,父进程继续读取管道字符串。当替换ls命令后,需要exit退出,不然的话这条命令一直占用进程,父进程不能继续执行。
我们再来一个子进程替换进程,执行跑火车命令。同样,客户端写入run,当读端检测到run字符,俺么就执行/usr/bin/sl命令。这个跑火车命令需要下载sudo sudo apt-get install sl,下载即可,如果不行的话,在root用户下下载。
其它情况客户端写入什么,服务器端读取什么。
server.c代码
[wjy@VM-24-9-centos fifo]$ cat server.c #include "comm.h" #include <stdlib.h> int main() { umask(0); if(mkfifo(MY_FIFO,0666)<0) { perror("mkfifo"); return 1; } //只需要文件操作即可 int fd=open(MY_FIFO,O_RDONLY); if(fd<0) { perror("open"); return 2; } //业务逻辑 while(1) { char buffer[64]={0}; ssize_t s=read(fd,buffer,sizeof(buffer)-1); if(s>0) { buffer[s]=0; if(strcmp(buffer,"show")==0) { if(fork()==0) { execl("/usr/bin/ls","ls","-l",NULL); exit(1); } waitpid(-1,NULL,0); } else if(strcmp(buffer,"run")==0) { if(fork()==0) { execl("/usr/bin/sl","sl",NULL); } waitpid(-1,NULL,0); } else printf("client#%s ",buffer); } else if(s==0) { printf("client quit... "); break; } else { perror("read"); break; } } close(fd); return 0; }
client.c代码
[wjy@VM-24-9-centos fifo]$ cat client.c #include "comm.h" #include <string.h> int main() { //客户端不用创建fifo了,只需要获取即可 int fd=open(MY_FIFO,O_WRONLY);//不需要O_CREAT,因为管道已经被读端创建好了 if(fd<0) { perror("open"); return 1; } //业务逻辑 while(1) { printf("请输入# "); fflush(stdout); char buffer[64]={0}; ssize_t s=read(0,buffer,sizeof(buffer)-1); if(s>0)//读取成功 { buffer[s-1]=0; printf("%s ",buffer); write(fd,buffer,strlen(buffer));//不需要-1,不会把写进去的。 } } close(fd);//最后不写了,要把文件关闭 return 0; }
comm.h代码就不展示了,上面有。
总结
为什么我们之前的pipe叫做匿名管道,为什么现在的fifo叫做命名管道呢?
因为命名管道一定要有名字,为了保证不同的进程看到同一个文件,必须要有名字。
但是匿名管道文件没有名字,因为他是通过父子继承的方式,看到同一份资源,不需要名字来标识同一个资源。
这篇关于【Linux操作系统】--进程间通信--匿名管道和命名管道的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-18git仓库有更新,jenkins 自动触发拉代码怎么配置的?-icode9专业技术文章分享
- 2024-12-18Jenkins webhook 方式怎么配置指定的分支?-icode9专业技术文章分享
- 2024-12-13Linux C++项目实战入门教程
- 2024-12-13Linux C++编程项目实战入门教程
- 2024-12-11Linux部署Scrapy教程:新手入门指南
- 2024-12-11怎么将在本地创建的 Maven 仓库迁移到 Linux 服务器上?-icode9专业技术文章分享
- 2024-12-10Linux常用命令
- 2024-12-06谁看谁服! Linux 创始人对于进程和线程的理解是…
- 2024-12-04操作系统教程:新手入门及初级技巧详解
- 2024-12-04操作系统入门:新手必学指南