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

进程间的通信

目录

一、IPC、管道、FIFO的引入

1、IPC

2、管道(无名管道)

特点

原型

创建

代码示例

3、FIFO(命名管道)

特点

原型

创建

代码示例

二、消息队列

1、概念

2、特点

3、创建

4、 常用API

msgget()

msgsnd()

msgrcv()

5、代码示例

 三、共享内存

概念

特点

原理

创建

常用API

shmget函数

shmat函数

shmdt函数

shmctl函数

代码示例

fork函数的补充

函数原型

 四、信号

原理

信号的处理

忽略信号

 捕捉信号

系统默认动作

signal函数


一、IPC、管道、FIFO的引入

1、IPC

进程间通信 (IPC,InterProcess Communication) 是指在不同进程之间传播或交换信息

IPC的方式通常有管道 (包括无名管道和命名管道) 、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

单机:若是在单一机器上,则为单机通信

半双工管道
全双工管道
消息队列
信号量
共享内存

多机:多台机器上,为网络通信

网络通信种类如下:

 

2、管道(无名管道)

管道,通常指无名管道(之所以叫无名管道是因为没有文件名),是 UNIX 系统IPC最古老的形式。

特点

(1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
(2)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
(3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
(4)管道中不储存数据,数据写进后读取就会消失,类似于水流。

原型

#include <unistd.h>     //函数pipe包含的头文件
int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

当一个管道建立时,它会创建两个文件描述符: fd[0]为读而打开,fd[1]为写而打开。如下图:

                                

要关闭管道只需将文件描述符关闭即可。

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

创建

单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 半双工通道。如下图所示:

 左图为调用fork函数创建了IPC半双工管道,右图为父进程到子进程的管道。

代码示例

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{int pid=0;int fd[2];char buf[128];if(pipe(fd) == -1)//如果管道创建失败{printf("creat pipe failed\n");}pid=fork();if(pid<0)//创建子进程失败{printf("creat failed\n");}else if(pid >0)//进入父进程{printf("this is father\n");close(fd[0]);//关闭读文件描述符write(fd[1],"hello from father",strlen("hello from father"));//将内容写入管道中wait();//等待子进程}else//进入子进程{printf("this is child\n");close(fd[1]);//关闭写文件描述符read(fd[0],buf,128);//将管道中内容读取到bufprintf("read form father:%s\n",buf);exit(0);//子进程退出}return 0;
}

以上代码实现了管道通信,但不知道父子进程谁先运行,所以子进程read在没有读取到内容时会阻塞,直到读取内容后才正常运行,可以做以下调试: 

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{int pid=0;int fd[2];char buf[128];if(pipe(fd) == -1)//如果管道创建失败{printf("creat pipe failed\n");}pid=fork();if(pid<0)//创建子进程失败{printf("creat failed\n");}else if(pid >0)//进入父进程{sleep(3);//进入父进程后睡眠3秒再运行printf("this is father\n");close(fd[0]);//关闭读文件描述符write(fd[1],"hello from father",strlen("hellofrom father"));//将内容写入管道中wait();//等待子进程}else//进入子进程{printf("this is child\n");close(fd[1]);//关闭写文件描述符read(fd[0],buf,128);//将管道中内容读取到bufprintf("read form father:%s\n",buf);exit(0);//子进程退出}return 0;
}

 

 方案是创建父进程后让其睡眠3秒后再执行父进程中的代码,可见在睡眠时子进程先运行其代码,但并没有执行read函数,此时表现为堵塞状态,直到3秒后父进程正常运行并将内容写入管道中,子进程才读取管道中的内容并成功打印。

3、FIFO(命名管道)

FIFO,也称为命名管道,它是一种文件类型。

特点

1.FIFO可以在无关的进程之间交换数据,与无名管道不同。
2.FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

原型

#include <sys/stat.h>
int mkfifo (const char *pathname, mode t mode) ;// 返回值: 成功返回0,出错返回-1

第一部分参数是文件的路径,第二部分的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/0函数操作它。如:open、read、write等函数。

当 open 一个FIFO时,是否设置非阻塞标志 (O_NONBLOCK) 的区别:

  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 。如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

创建

FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。

代码示例

read.c

#include <stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include <errno.h>
#include <fcntl.h>int main()
{int fd = 0;int n_read = 0;char buf[128];if(mkfifo("./file",0600) == -1 && errno!=EEXIST)//判断管道出错原因是不是在于已经创建{printf("mkfifo failure\n");perror("why");}else{if(errno==EEXIST)//管道已经创建{printf("file eexist\n");}else//管道未创建{printf("mkfifo successed\n");}}fd = open("./file",O_RDONLY);//只读方式打开printf("open file succeed\n");n_read = read(fd,buf,128);//需要等待写入完毕才能读取,才能执行下列代码printf("read %d byte from file,context is %s\n",n_read,buf);close(fd);return 0;
}

 write.c

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include <errno.h>
#include <fcntl.h>int main()
{char *buf="hello word!!!!!!!!!!";int fd;fd = open("./file",O_WRONLY);//只写方式打开printf("write file success\n");write(fd,buf,strlen(buf));//将字符串内容写入fd中,写完才可以读取close(fd);return 0;
}

 

 

 可见执行read文件时,显示管道已经存在后停止执行后续代码,当执行write文件后read文件继续执行后续代码,实现管道间的通信。

二、消息队列

1、概念

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

2、特点

1.消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。(消息队列是结构体)
2.消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
3.消息队列可以实现消息的随机查询,消息不一定要以先进先出的次席读取,也可以按消息的类型读取。

                

两者的队列ID需相同才能成功实现存放数据和取数据,如图都指向队列1的最后一个。

消息队列与管道的不同点:写入读取后内容还存在于Linux内核中,不会跟管道一样读取完就消失。

3、创建

从消息队列特点可知,两个进程分别需要同队列ID相同的队列进行写入数据并读取数据,此时要想成功创建一个消息队列,需关心两个问题:

问题一:进程B如何添加消息到队列

问题二:进程A如何读取队列的消息

头文件

#include <sys/msg.h>

4、 常用API

msgget()

创建或打开消息队列:成功返回队列ID,失败返回-1

int msgget(key_t key, int flag);

 key:是一个索引值,为非负数,将通过索引值在Linux内核找到队列

flag:打开队列的方式

在以下两种情况下,msgget将创建一个新的消息队列:
1、如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。

msgget(key,IPC_CREAT);

 2、key参数为IPC_PRIVATE。

msgget(key,IPC_PRIVATE);
msgsnd()

添加消息:成功返回0,失败返回-1

int msgsnd(int msqid, const void *ptr, size_t size, int flag);

msqid:消息队列的ID
ptr:写入的数据,指向消息缓冲区的指针,此位置用来暂时存储发送和接收的消息,是一个用户可定义的通用结构,形态如下:

struct msgbuf
{long mtype; //消息类型,必须大于0char mtext[1];//消息文本 
};

size:数据的长度
flag:0,表示忽略,表示进程将被阻塞直到函数可以从队列中得到符合条件的消息为止;(还有许多,此处省略)

msgrcv()

读取消息:成功返回消息数据的长度,失败返回-1

int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);

msqid:消息队列的ID
ptr:写入的数据,指向消息缓冲区的指针,此位置用来暂时存储发送和接收的消息,是一个用户可定义的通用结构,形态如下:

struct msgbuf
{long mtype; //消息类型,必须大于0char mtext[1];//消息文本 
};

 type:消息类型

                

可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。

size:数据的长度

flag:0,表示忽略,表示进程将被阻塞直到函数可以从队列中得到符合条件的消息为止;(还有许多,此处省略)

5、代码示例

get.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>struct msgbuf
{long mtype;char mtext[128];
};int main()
{int msgId;//创建消息队列IDstruct msgbuf readBuf;//定义一个读取数据的结构体msgId = msgget(1234,IPC_CREAT|0777);//在内核中打开或建立键值为1234的,权限为0777的消息队列if(msgId == -1)//如果创建失败则执行下面代码{printf("create queue failed\n");}msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0);//从队列中获取888类型的数据并存放到结构体的mtext中,如果队列中未出现888类型的数据,则程序阻塞在这里,这里的888需要与写入队列类型数据一致printf("read from queue:%s\n",readBuf.mtext);struct msgbuf sendBuf = {999,"thank you for reach\n"};//读取完毕后将字符串内容写入到999类型的数据中,这里的999类型需要与读取的类型数据一致msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);//将上一行的结构体数据写入1234消息队列中return 0;
}

send.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>struct msgbuf
{long mtype;char mtext[128];
};int main()
{int msgId;struct msgbuf sendBuf = {888,"this is message from queue\n"};//将字符串内容写入到888类型的数据中,这里的888类型需要与读取的类型数据一致struct msgbuf readBuf;msgId = msgget(1234,IPC_CREAT|0777);if(msgId == -1){printf("create queue failed\n");}msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);//将结构体内容写入到1234消息队列中msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),999,0);//写入之后从队列中获取999类型的数据并存放到结构体的mtext中,如果队列中未出现999类型的数据,则程序阻塞在这里,这里的999需要与写入队列类型数据一致printf("return form queue:%s\n",readBuf.mtext);return 0;
}

 

              (这里还可以使用——vimdiff get.c send.c 比较函数的区别,很有意思的页面)

 运行get.c,创建并打开键值为1234的消息队列,但此时表现为堵塞状态,因为队列里没有888类型的数据

 

 运行send.c,创建并打开键值为1234的消息队列,往队列里写入888类型的数据,此时接收端会接受到写入端写入消息队列的数据并将其读取,同时让接收端往队列里写入999类型的数据,让写入段接受999类型的数据并读取

 

 三、共享内存

概念

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

特点

共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
因为多个进程可以同时操作,所以需要进行同步。
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

原理

        

创建

1、创建共享内存
2、进程A连接共享内存,写入数据(这里需要给进程A一个睡眠时间:两个进程同时操作需要同步,进程A写入数据后睡眠一定时间,在这个时间内进程B将数据读取,实现数据交换)
3、进程A断开连接
4、进程B连接共享内存,读取数据
5、进程B断开连接
6、释放公共内存

常用API

头文件

#include <sys/types.h>
#include <sys/shm.h>
//以下几个API都包含以上两个头文件

shmget函数

功能:创建或获取一个共享内存

函数原型

int shmget(key_t key, size_t size, int shmflg);

参数:

key:由ftok生成的key标识,标识系统的唯一IPC资源

size:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍,即以兆为单位(1024)

shmflag:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,后面需要加权限标志,权限标志与文件的读取操作一样。如果是已经存在的,可以使用IPC_CREAT或直接传0(只需获取而不用创建,yi)

返回值:成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参数。失败返回-1并设置错误码。

shmat函数

功能:第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间,即将共享内存映射进进程中。

函数原型

void *shmat(int shm_id, const void *shm_addr, int shmflg); 

 参数:

shm_id:由shmget函数返回的共享内存标识

*shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空(为0),表示让系统为我们安排共享内存的地址

shmflg:若指定了SHM RDONLY位,则以只读方式连接此段,否则以读写方式连接此段(输入0即可,表示映射进的共享内存可读可写)

返回值:成功返回共享存段的指针(虚拟地址),并且内核将使其与该共享存段相关的shmid_ds(第四个函数的第三个参数)结构中的shm_nattch计数器加1 (类似于引用计数,即内存占用计入总内存) ; 出错返回-1。

shmdt函数

功能:当一个进程不需要共享内存的时候,就需要去关联。该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程。

函数原型

int shmdt(const void *shmaddr); 

参数解读

*shmaddr:是shmat函数返回的地址指针,只需将其返回值的函数变量名(代码地址)写入即可

返回值

调用成功时返回0,失败时返回-1。

shmctl函数

功能:控制共享内存

函数原型

int shmctl(int shm_id, int command, struct shmid_ds *buf);  

参数:

shm_id:shmget函数返回的共享内存标识符

command:command是要采取的操作,它可以取下面的三个值

                   IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存                                            的当前关联值覆盖shmid_ds的值

                   IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构                                        中给出的值

                   IPC_RMID(常用):删除共享内存段

*buf:buf是一个结构指针,它指向共享内存模式和访问权限的结构(不关心,一般写0)

返回值

成功时返回0,失败返回-1并设置错误码。

代码示例

shmw.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>int main()
{key_t key;int shmid;char *shmaddr = NULL;	key = ftok(".",1);//当前目录建立IPCshmid = shmget(key,1024*4,IPC_CREAT|0666);//以可读可写方式开辟一个4兆大小的共享内存if(shmid == -1){printf("create gxdl failed\n");exit(-1);}shmaddr = shmat(shmid,0,0);//将共享内存映射到进程中printf("connect success!\n");strcpy(shmaddr,"hello word!");sleep(5);//因为写入和读取是同步,所以此时执行完写入代码后需等待读取数据后一起关闭共享内存shmdt(shmaddr);//断开连接共享内存shmctl(shmid,IPC_RMID,0);//删除共享内存printf("over\n");return 0;
}

shmr.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>int main()
{key_t key;int shmid;char *shmaddr = NULL;	key = ftok(".",1);shmid = shmget(key,1024*4,0);//这里只需找到已经开辟的共享内存,所以参数为0即可if(shmid == -1){printf("create gxdl failed\n");exit(-1);}shmaddr = shmat(shmid,0,0);//映射共享内存printf("connect success!\n");printf("content is %s\n",shmaddr);//将共享内存数据打印出来shmdt(shmaddr);//断开共享内存printf("over\n");return 0;
}

shmw.c程序运行,写入端往共享内存写入数据,并让其等待5s,这5s内shmr.c程序运行,读取端读取共享内存数据并将内容打印出来,5s后两者同时关闭共享内存输出over。

fork函数的补充

功能

系统IPC键值的格式转换函数,系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。

函数原型

key_t ftok( const char * fname, int id );

参数:

fname:就是你指定的文件名(已经存在的文件名),一般使用当前目录

 id:子序号。虽然是int类型,但是只使用8bits(1-255)

例如

key_t key;
key = ftok(".", 1); //当前文件只需加.

 四、信号

原理

对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。
信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了ctrl+c来中断程序,会通过信号机制停止一个程序。

概述

  1. 信号的名字和编号
  2. 每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGUP(挂起) ”、“SIGINT(中断)、SIGQUIT(退出)”等等。
  3. 信号定义在signal.h头文件中,信号名都定义为正整数。
  4. 具体的信号名称可以 使用kill -l来查看信号的名字以及序号,
  5. 信号是从1开始编号的,不存在0号信号。kill对于信号0有特殊的应用。

信号的处理

信号的处理方式有三种,分别是忽略、捕捉和默认动作。

忽略信号

大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILLSIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景。

系统自带的忽略宏函数

 signal(SIGINT,SIG_IGN);//将SIGINT信号(ctrl+C、2)忽略

 捕捉信号

需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。( 在main函数外定义一个函数,用signal函数中的参数调用该函数并执行函数中的功能)

系统默认动作

对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。
具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。也可以参考 《UNIX 环境高级编程(第三部)》的 P251——P256中间对于每个信号有详细的说明。

例子说明:

 其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID 来杀死进程。比如,我在后台运行了一个 a.out 工具,通过 ps 命令可以查看他的 PID,通过 kill 9来发送了一个终止进程的信号来结束了 a.out 进程。如果查看信号编号和名称,可以发现9对应的是 SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。

kill -9 进程PID
kill -SIGKILL 进程PID

 

 可见,两者的执行结果相同。说明kill命令是发送信号的工具。

signal函数

功能:设置某一信号的对应动作

原型:

#include <signal.h>typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);//xxx_t就是结构体的意思

解读:

第一行是真实处理信号的函数:中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号,即注册函数的第二个参数可以调用信号处理函数并执行其中的功能。

第二行是信号处理注册的函数:

signum:信号的编号,如SIGKILL的编号是9

handler:中断函数的指针,写入后可以调用编写的真实处理信号函数并执行功能

signal()会依参数signum指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。

返回值

成功则返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。

代码示例

信号处理函数的注册

signal1.c

#include <stdio.h>
#include <signal.h>void handler(int signum)
{printf("get signum is %d\n",signum);printf("not quit\n");switch(signum){case 2:printf("SIGINT\n");break;case 9:printf("SIGKILL\n");break;case 10:printf("SIGUSR1\n");break;}
}int main()
{signal(SIGINT,handler);signal(SIGUSR1,handler);while(1);return 0;
}

代码编译后查看运行a.out工具,通过ps查看其编号

运用kill指令分别对信号进行处理

      

注:第一种按下crtl+c执行结果相同。

可见调用signal函数后匹配的正确编号后会执行handler中的功能(将函数编号打印出来)。第三个与前两个结果不一样是因为SIGKILL指令无法被忽略,这里的kill -9发出的是指令,由于代码为死循环,若SIGKILL被忽视,则会导致代码无法终止循环,所以一旦SIGKILL指令发出,程序立刻停止(被杀死)。

 发送信号处理函数

  signal2.c

#include <stdio.h>
#include <signal.h>int main(int argc,char **argv)//由于需要此代码发送指令另一部分代码才会执行,所以需要进行传参,参数为kill参数,格式为./a.out pid signum
{int signum;int pid;signum = atoi(argv[1]);pid = atoi(argv[2]);kill(pid,signum);//调用kill函数,将信号处理编号和工具的pid值输入即可printf("send signal success\n");return 0;
}

 先编译signal1.c(上一模块的代码)并运行

调用ps指令查看该程序的信号值

编译运行signal2.c中的代码传参即可运行signal1.c代码中的功能

将signum与pid输入后即可实现signal1.c中的功能,实现信号捕捉处理。

功能与signal2.c一样的代码:

#include <stdio.h>
#include <signal.h>int main(int argc,char **argv)
{int signum;int pid;char cmd[128] = {0};signum = atoi(argv[1]);pid = atoi(argv[2]);sprintf(cmd,"kill -%d %d",pid,signum);//cmd的指令格式为“”里的格式,即调用kill指令system(cmd);//调用cmd指令printf("send signal success\n");return 0;
}

注:
1、atoi (表示 ascii to integer)是把字符串转换成整型数的一个函数。
2、sprintf指的是字符串格式化命令,函数原型为

 int sprintf(char *string, char *format [,argument,…]);

主要功能是把格式化的数据写入某个字符串中,即发送格式化输出到 string 所指向的字符串。

http://www.xdnf.cn/news/5757.html

相关文章:

  • python-75-Nacos技术之Python+Nacos实现微服务架构
  • 打破效率枷锁,数企云外呼一骑绝尘
  • beyond compare 免密钥进入使用(删除注册表)
  • MacOS 上构建 gem5
  • 排错-harbor-db容器异常重启
  • PCB抄板过程、抄板软件介绍
  • 基于Qt6 + MuPDF在 Arm IMX6ULL运行的PDF浏览器(项目推介)
  • 做为一个平台,给第三方提供接口的时候,除了要求让他们申请 appId 和 AppSecret 之外,还应当有哪些安全选项,要过等保3级
  • BUUCTF Pwn hitcontraining_uaf WP
  • 学习黑客5分钟深入浅出理解系列之Windows注册表
  • Odoo 18 安全组与访问权限管理指南
  • SQLite 数据库常见问题及解决方法
  • 一般纯软工程学习路径
  • 使用达梦数据库官方管理工具SQLark导入与导出数据库表
  • 解决IDEA无法运行git的问题
  • CVE-2020-1957 漏洞报告
  • 基于MCP的智能体架构设计:实现智能体与外部世界的无缝连接
  • 辣椒青椒幼苗和杂草检测数据集VOC+YOLO格式706张2类别
  • IP协议、以太网包头及UNIX域套接字
  • 在 Java 8 中 常用时间日期类
  • 【Linux系统】自动化构建-make/Makefile的使用
  • AI Agent开发第64课-DIFY和企业现有系统结合实现高可配置的智能零售AI Agent(上)
  • #S4U2SELF#S4U2Proxy#CVE-2021-42278/42287
  • 按指定位置或关键字批量删除工作表-Excel易用宝
  • 关系实验课--笛卡尔积
  • cURL:通过URL传输数据的命令行工具库介绍
  • 请求参数:Header 参数,Body 参数,Path 参数,Query 参数分别是什么意思,什么样的,分别通过哪个注解获取其中的信息
  • 每日算法刷题Day4 5.12:leetcode数组4道题,用时1h
  • zabbix6.4监控主机并触发邮件告警
  • Egg.js知识框架