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

Linux 进程间通信(IPC)详解

进程间通信(IPC)深入解析

一、进程间通信概述

在操作系统里,不同进程间常常需要进行数据交换、同步协调等操作,进程间通信(Inter - Process Communication,IPC)机制应运而生。在Linux系统中,常见的IPC机制有管道、信号量、共享内存、消息队列和套接字。这些机制各自具备独特的特性,适用于不同的应用场景,并且在各类系统和应用程序开发中得到广泛应用,也是技术面试中的重点考查内容。

思考问题1:为什么需要进程间通信?

在多进程的环境下,各个进程通常是独立运行的,但有些情况下,它们需要协同工作。比如,一个进程负责收集用户输入,另一个进程负责对这些输入进行处理,这就需要两个进程之间进行数据传递。另外,当多个进程需要访问同一资源时,为了避免冲突,就需要通过进程间通信来进行同步和协调。

思考问题2:不同的IPC机制适用于哪些场景?

不同的IPC机制有不同的特点和适用场景。例如,管道适合简单的父子进程间的数据传递;信号量主要用于进程间的同步和互斥;共享内存适合大量数据的快速传输;消息队列适用于需要按消息类型分类处理数据的场景;套接字则常用于网络环境下的进程间通信。

二、管道

2.1 管道分类

管道分为有名(命名)管道和无名管道。

有名管道

有名管道也叫命名管道,它以文件的形式存在于文件系统中,不同进程可以通过这个文件进行通信,即便这些进程没有亲缘关系。

mkfifo fifo		# 创建一个叫做fifo的管道

创建完成后,使用ls -l命令查看文件类型,会看到文件类型为p,这代表该文件是一个管道文件。

无名管道

无名管道是通过系统调用pipe创建的,它没有对应的文件系统实体,只能用于具有亲缘关系的进程(如父子进程)之间的通信。

2.2 管道的特点和阻塞行为

数据存储

管道大小在文件系统层面显示为零,但数据实际上是存储在内存中的。管道本质上是内核中的一块缓冲区,用于临时存储要传输的数据。

打开条件

读打开和写打开的进程必须同时打开管道。若只有读进程打开管道,读操作会阻塞,直到有写进程打开并写入数据;若只有写进程打开管道,写操作也会阻塞,直到有读进程打开管道读取数据。

读阻塞

读打开的进程在管道没有数据时会阻塞,直到有数据被写入管道。当所有写端关闭且管道中的数据都被读完后,读操作会返回0,表示已到达文件末尾。

写关闭处理

写打开的进程关闭管道时,读打开的进程会返回零。此时读进程知道写进程已经停止写入数据,可以结束读取操作。

2.3 如何使用管道在两个进程之间传递数据?

第一步,创建一个管道文件

使用mkfifo命令创建一个命名管道文件,例如:

mkfifo fifo
第二步,创建两个进程,a.cb.c
//a.c用来往管道里面写数据:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("./fifo", O_WRONLY);if (fd == -1){perror("open");exit(1);}printf("fd=%d\n", fd);while (1){printf("input:\n");char buff[128] = {0};fgets(buff, 128, stdin);if (strncmp(buff, "end", 3) == 0){break;}write(fd, buff, strlen(buff));}close(fd);exit(0);
}
//b.c用来在管道里面收数据:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("./fifo", O_RDONLY);if (fd == -1){perror("open");exit(1);}printf("fd=%d\n", fd);while (1){char buff[128] = {0};int n = read(fd, buff, 127);if (n == 0){break;}printf("buff = %s\n", buff);}close(fd);exit(0);
}
第三步,同时打开两个文件,进行通信

当使用管道进行数据传输时,必须有两个进程同时打开这个管道文件,一个负责读,一个负责写,否则不能正常打开。在打开文件后,当写进程没有进行写操作前,读进程将会阻塞。

管道的特点是读打开和写打开的进程可以循环读写数据。当管道文件被关闭后,读操作返回值为0,可以作为读进程结束的条件。

2.4 无名管道

无名管道通过pipe来创建。其实现原理是需要提供一个整型数组,数组的两个元素分别作为读端和写端的文件描述符。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main()
{int fd[2];if (pipe(fd) == -1){perror("pipe");exit(1);}// 父进程写,子进程读pid_t pid = fork();if (pid == -1){perror("fork");exit(1);}if (pid == 0){close(fd[1]);while (1){char buff[128] = {0};if (read(fd[0], buff, 127) == 0){break;}printf("child read:%s\n", buff);}close(fd[0]);}else{close(fd[0]);while (1){printf("input:\n");char buff[128] = {0};fgets(buff, 128, stdin);if (strncmp(buff, "end", 3) == 0){break;}write(fd[1], buff, strlen(buff));}close(fd[1]);}exit(0);
}

2.5 有名管道和无名管道的区别

  • 通信范围:无名管道只能在父子进程之间通信,而有名管道可以在任意两个进程之间通信。
  • 存在形式:无名管道没有对应的文件系统实体,而有名管道以文件的形式存在于文件系统中。

2.6 管道的通信方式

管道的通信方式是半双工,即能发送和接收数据,但不能同时进行发送和接收操作。

2.7 写入管道的数据位置

写入管道的数据存储在内存中。不使用文件进行数据传递是因为使用文件进行传递涉及到I/O操作,效率较低,而管道直接在内存中操作,数据传输速度更快。

2.8 管道的实现原理

假设管道有一个分配的内存空间,将其划分为一个字节一个字节的单元,有两个操作这块空间的指针。用size来表示管道的总大小,设一个头指针和一个尾指针指向管道的起始位置。写入数据时,头指针往后移动,指向待写入的下一个位置;读数据时,读掉尾指针所在位置的数据,然后尾指针往后移动。只要尾指针赶上头指针,说明管道中的数据已读完。等到头指针指到内存最末尾时,会循环到起始地址。管道的内存是有限的,在没读掉的位置不能写入数据,就像一个循环队列一样。

思考问题3:管道的缓冲区大小有限制吗?如果数据量超过缓冲区大小会怎样?

管道的缓冲区大小是有限制的,不同的系统可能有不同的默认值,通常为几KB到几十KB不等。当数据量超过缓冲区大小时,写操作会阻塞,直到有足够的空间可以继续写入数据。这是为了防止数据溢出,保证数据的有序传输。

思考问题4:管道在多进程环境下可能会出现哪些问题?如何解决?

在多进程环境下,管道可能会出现数据竞争、死锁等问题。例如,多个写进程同时向管道写入数据可能会导致数据混乱;如果读进程和写进程的操作不协调,可能会出现死锁。解决这些问题可以使用同步机制,如信号量、互斥锁等,来协调进程对管道的访问。

2.9 写端关闭和读端关闭的处理

当写端关闭时,读端read()会返回0;当读关闭时,写端write()会异常终止(触发SIGPIPE信号)。

三、信号量

3.1 PV操作

信号量通常是一个正数值,一般代表可用资源的数目。对信号量的操作主要有PV操作。

  • P操作:获取资源,对信号量的值减一。如果减一后信号量的值小于0,进程会进入阻塞状态,等待其他进程释放资源。
  • V操作:释放资源,对信号量的值加一。如果加一后信号量的值小于等于0,说明有进程在等待该资源,会唤醒一个等待的进程。

3.2 相关概念

  • 临界资源:一次仅允许一个进程使用的共享资源,如打印机、共享内存区域等。
  • 临界区:访问临界资源的代码段。为了保证临界资源的正确使用,需要对临界区进行保护,防止多个进程同时访问。

3.3 信号量的操作步骤

  1. 创建 初始化:使用semget函数创建信号量集,并使用semctl函数进行初始化。
  2. P操作,获取资源:使用semop函数执行P操作,获取对临界资源的访问权限。
  3. V操作,释放资源:使用semop函数执行V操作,释放对临界资源的访问权限。PV操作没有严格的先后顺序,要根据当时的使用需求来决定。
  4. 删除信号量:使用semctl函数删除信号量集。

3.4 信号量的操作和接口

信号量的主要操作函数有semget(创建)、semctl(控制,初始化)和semop(PV操作)。

//sem.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/sem.h>union semun 
{int val;    
};void sem_init();
void sem_p();
void sem_v();
void sem_destroy();
//sem.c
#include "sem.h"
static int semid = -1;void sem_init()
{semid = semget((key_t)1234, 1, IPC_CREAT | IPC_EXCL | 0600);if (semid == -1){semid = semget((key_t)1234, 1, IPC_CREAT | 0600);if (semid == -1){perror("semget");return;}}else{union semun a;a.val = 1;if (semctl(semid, 0, SETVAL, a) == -1) // SETVAL表示初始化值{perror("semctl setval");}}
}void sem_p()
{struct sembuf buf;buf.sem_num = 0; // 信号量的下标buf.sem_op = -1;buf.sem_flg = SEM_UNDO; // 这个标志位表示着当操作发生异常时由内核释放资源,避免资源一直占用if (semop(semid, &buf, 1) == -1){perror("semop p");}
}void sem_v()
{struct sembuf buf;buf.sem_num = 0;buf.sem_op = 1;buf.sem_flg = SEM_UNDO; // 这个标志位表示着当操作发生异常时由内核释放资源,避免资源一直占用if (semop(semid, &buf, 1) == -1){perror("semop v");}
}void sem_destroy()
{if (semctl(semid, 0, IPC_RMID) == -1) // IPC_RMID表示删除{perror("semctl destroy");}    
}

3.5 创建两个进程使用信号量进行资源调用

//a.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "sem.h"int main()
{sem_init();for (int i = 0; i < 5; i++){sem_p();printf("A");fflush(stdout);int n = rand() % 3;sleep(n);printf("A");fflush(stdout);n = rand() % 3;sleep(n);sem_v();}return 0;
}
//b.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "sem.h"int main()
{sem_init();for (int i = 0; i < 5; i++){sem_p();printf("B");fflush(stdout);int n = rand() % 3;sleep(n);printf("B");fflush(stdout);n = rand() % 3;sleep(n);sem_v();}sleep(10);sem_destroy();return 0;
}

思考问题5:信号量的值可以为负数吗?负数代表什么含义?

信号量的值可以为负数。当信号量的值为负数时,其绝对值表示正在等待该资源的进程数量。例如,信号量的值为 -2,表示有两个进程正在等待该资源的释放。

思考问题6:如果在使用信号量时忘记释放资源(即没有执行V操作)会怎样?

如果忘记执行V操作,信号量的值不会增加,其他等待该资源的进程将一直处于阻塞状态,无法获取资源,从而导致死锁或资源饥饿问题。因此,在使用信号量时,必须确保在适当的时候执行V操作,释放资源。

四、共享内存

4.1 共享内存的原理和优势

共享内存是一种高效的进程间通信方式,它允许不同进程直接访问同一块物理内存区域,避免了数据的多次拷贝,从而提高了数据传输的效率。
在这里插入图片描述

//a.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>int main()
{// 创建共享内存int shmid = shmget((key_t)1234, 128, IPC_CREAT | 0600);if (shmid == -1){perror("shmget");exit(1);}// 获取共享内存char* s = (char*)shmat(shmid, NULL, 0);if (s == (char*)-1){perror("shmat");exit(1);}while (1){char buff[128] = {0};fgets(buff, 128, stdin);strcpy(s, buff);if (strncmp(buff, "end", 3) == 0){break;}}// 分离共享内存shmdt(s);exit(0);
}
//b.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>int main()
{// 创建共享内存int shmid = shmget((key_t)1234, 128, IPC_CREAT | 0600);if (shmid == -1){perror("shmget");exit(1);}// 获取共享内存char* s = (char*)shmat(shmid, NULL, 0);if (s == (char*)-1){perror("shmat");exit(1);}while (1){if (strncmp(s, "end", 3) == 0){break;}printf("s=%s", s);sleep(1);}// 分离共享内存shmdt(s);// 销毁共享内存shmctl(shmid, IPC_RMID, NULL);exit(0);
}

4.2 存在问题及改进方案

上述代码存在问题:当没有输入值的时候,b.c会循环打印当时的共享内存中的值。改进方案是使用信号量,让两个程序不能同时访问临界资源。

4.3 共享内存和信号量的使用

需要使用信号量的两个文件sem.csem.h,跟之前不同的是之前使用了一个信号量,这次需要使用两个。

//sem.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/sem.h>union semun 
{int val;    
};void sem_init();
void sem_p(int index);
void sem_v(int index);
void sem_destroy();
//sem.c
#include "sem.h"
static int semid = -1;void sem_init()
{semid = semget((key_t)1234, 2, IPC_CREAT | IPC_EXCL | 0600);if (semid == -1){semid = semget((key_t)1234, 2, IPC_CREAT | 0600);if (semid == -1){perror("semget");return;}}else{union semun a;int arr[2] = {1, 0};for (int i = 0; i < 2; i++){a.val = arr[i];if (semctl(semid, i, SETVAL, a) == -1) // SETVAL表示初始化值{perror("semctl setval");}            }        }
}void sem_p(int index)
{struct sembuf buf;buf.sem_num = index; // 信号量的下标buf.sem_op = -1;buf.sem_flg = SEM_UNDO; // 这个标志位表示着当操作发生异常时由内核释放资源,避免资源一直占用if (semop(semid, &buf, 1) == -1){perror("semop p");}
}void sem_v(int index)
{struct sembuf buf;buf.sem_num = index;buf.sem_op = 1;buf.sem_flg = SEM_UNDO; // 这个标志位表示着当操作发生异常时由内核释放资源,避免资源一直占用if (semop(semid, &buf, 1) == -1){perror("semop v");}
}void sem_destroy()
{if (semctl(semid, 0, IPC_RMID) == -1) // IPC_RMID表示删除{perror("semctl destroy");}    
}
//a.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
#include "sem.h"int main()
{// 创建共享内存int shmid = shmget((key_t)1234, 128, IPC_CREAT | 0600);if (shmid == -1){perror("shmget");exit(1);}// 映射共享内存char* s = (char*)shmat(shmid, NULL, 0);if (s == (char*)-1){perror("shmat");exit(1);}sem_init();while (1){printf("input:\n");char buff[128] = {0};fgets(buff, 128, stdin);sem_p(0); // s1,第一个信号量,初始值为1strcpy(s, buff);sem_v(1); // s2,第二个信号量,初始值为0if (strncmp(buff, "end", 3) == 0){break;}}// 分离共享内存shmdt(s);exit(0);
}
//b.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
#include "sem.h"int main()
{// 创建共享内存int shmid = shmget((key_t)1234, 128, IPC_CREAT | 0600);if (shmid == -1){perror("shmget");exit(1);}// 获取共享内存char* s = (char*)shmat(shmid, NULL, 0);if (s == (char*)-1){perror("shmat");exit(1);}sem_init();while (1){sem_p(1);if (strncmp(s, "end", 3) == 0){break;}printf("s=%s", s);sem_v(0);}sem_destroy();// 断开共享内存映射shmdt(s);// 销毁shmctl(shmid, IPC_RMID, NULL);exit(0);
}

4.4 共享内存和管道的区别

共享内存对物理内存进行操作,管道对内核缓冲区进行操作,二者存在以下不同:

内存位置与管理方式
  • 共享内存:共享内存是在物理内存中开辟一块特定的区域,通过操作系统的内存管理机制,将其映射到不同进程的虚拟地址空间中。这样,多个进程可以直接访问同一块物理内存,实现数据共享。操作系统负责管理共享内存的分配与回收,进程通过系统调用如shmgetshmat等来请求和使用共享内存。例如,在多个进程需要频繁共享大量数据,如数据库系统中多个查询进程可能需要共享数据缓存时,就可以使用共享内存。
  • 管道:管道的内核缓冲区是由操作系统在内核空间中分配的一块内存区域,用于暂存管道两端进程间传输的数据。它的大小通常有一定限制,并且由操作系统自动管理其数据的进出和空间利用。当进程向管道写入数据时,数据被复制到内核缓冲区;读进程从管道读取数据时,再从内核缓冲区复制到用户空间。例如,在ls | grep这样的命令组合中,ls命令的输出通过管道传输到grep命令,中间的数据就是暂存在管道的内核缓冲区中。
数据访问特性
  • 共享内存:进程可以直接对共享内存进行读写操作,就像访问自己的内存一样,速度非常快,因为不需要进行数据在用户空间和内核空间之间的复制。但是,由于多个进程可以同时访问共享内存,为了保证数据的一致性和完整性,需要使用同步机制,如信号量、互斥锁等,来协调进程对共享内存的访问。否则,可能会出现数据竞争等问题。
  • 管道:管道的读写是有方向的,数据只能从写端流向读端。写进程将数据写入内核缓冲区,读进程从内核缓冲区读取数据。当管道满时,写进程会被阻塞,直到有数据被读走,腾出空间;当管道空时,读进程会被阻塞,直到有数据写入。这种机制保证了数据的有序传输,但也意味着数据的读写是顺序进行的,不能像共享内存那样随机访问。
数据可见性与持续性
  • 共享内存:一旦数据被写入共享内存,只要其他进程有访问权限,就可以立即看到更新后的数据,数据在共享内存中的存在是持续性的,直到被显式修改或删除。即使所有使用共享内存的进程都暂时退出,共享内存中的数据仍然存在于物理内存中,只要没有被操作系统回收或其他进程修改,下次进程再访问时,数据依然保持原来的状态。
  • 管道:管道中的数据具有临时性和一次性的特点。当数据被读进程从内核缓冲区读取后,数据就从管道中消失了,其他进程无法再次读取到相同的数据。而且,当所有与管道相关的文件描述符都被关闭后,管道所占用的内核缓冲区资源会被操作系统自动释放,其中的数据也会被清除。

思考问题7:共享内存可能会带来哪些安全隐患?如何防范?

共享内存可能带来的安全隐患包括数据泄露、数据被恶意篡改等。由于多个进程可以直接访问共享内存,若没有适当的权限控制和加密机制,敏感数据可能会被其他进程获取或修改。防范措施包括设置合理的访问权限,对共享内存中的数据进行加密处理,以及使用同步机制确保数据的一致性和完整性。

思考问题8:如果多个进程同时对共享内存进行写操作会怎样?

如果多个进程同时对共享内存进行写操作,会导致数据竞争问题,可能会使共享内存中的数据变得混乱,出现数据不一致的情况。为了避免这种情况,需要使用同步机制,如信号量、互斥锁等,来保证同一时间只有一个进程可以对共享内存进行写操作。

五、消息队列

5.1 消息队列的特点

添加消息和读取消息是消息队列的基本操作。消息是一个结构体,结构体名字由自己定义,特殊的地方是第一个成员是长整型(代表消息的类型),且该类型的值至少为1。

为什么消息类型必须大于零?

长整型如果是0号,就是不区分消息类型,在函数调用中,要是传入0的话,无论是什么消息类型都能读到。这样可以根据不同的消息类型对消息进行分类处理,提高消息处理的灵活性和效率。

5.2 消息队列的接口函数

  1. msgget():创建或获取消息队列。
int msgget(key_t key, int msgflg);

key是消息队列的键值,用于唯一标识一个消息队列;msgflg是标志位,用于指定创建方式和权限等。

  1. msgrcv():读取消息。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msqid是消息队列的标识符;msgp是用于存储接收到的消息的缓冲区;msgsz是缓冲区的大小;msgtyp是期望接收的消息类型;msgflg是标志位,用于指定操作方式。

  1. msgsnd():发送消息。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数含义与msgrcv类似。

5.3 示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/msg.h>struct mess
{long type;char buff[128];
};int main()
{int msgid = msgget((key_t)1234, IPC_CREAT | 0600);if (msgid == -1){perror("msgget");exit(1);}struct mess m;m.type = 1;strcpy(m.buff, "hello");if (msgsnd(msgid, &m, sizeof(m.buff), 0) == -1){perror("msgsnd");exit(1);}exit(0);
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/msg.h>struct mess
{long type;char buff[128];
};int main()
{int msgid = msgget((key_t)1234, IPC_CREAT | 0600);if (msgid == -1){perror("msgget");exit(1);}struct mess m;if (msgrcv(msgid, &m, sizeof(m.buff), 1, 0) == -1){perror("msgrcv");exit(1);}printf("read:%s\n", m.buff);exit(0);
}

思考问题9:消息队列的大小有限制吗?如果消息队列满了会怎样?

消息队列的大小是有限制的,不同的系统可能有不同的默认值。当消息队列满了时,后续的msgsnd操作会阻塞,直到队列中有空间可以容纳新的消息。这是为了防止消息队列溢出,保证消息的有序存储和处理。

思考问题10:消息队列在分布式系统中有哪些应用场景?

在分布式系统中,消息队列可以用于异步通信、任务调度、解耦服务等场景。例如,在一个电商系统中,订单服务在处理订单时可以将订单信息发送到消息队列中,库存服务、物流服务等可以从消息队列中获取订单信息并进行相应的处理,这样可以提高系统的并发处理能力和可扩展性。

六、IPC管理命令

6.1 ipcs

ipcs命令可以查看消息队列、共享内存、信号量的使用情况。例如:

  • ipcs -m:查看共享内存的使用信息。
  • ipcs -q:查看消息队列的使用信息。
  • ipcs -s:查看信号量的使用信息。

6.2 ipcrm

使用ipcrm命令可以进行删除操作。手动移除的命令格式为ipcrm -s/m/q +id,分别用于删除信号量、共享内存、消息队列。例如:

  • ipcrm -m 1234:删除ID为1234的共享内存段。
  • ipcrm -q 5678:删除ID为5678的消息队列。
  • ipcrm -s 9012:删除ID为9012的信号量集。

通过这些命令,系统管理员可以方便地管理系统中的IPC资源,确保系统的稳定运行。

思考问题11:在使用ipcrm命令删除IPC资源时需要注意什么?

在使用ipcrm命令删除IPC资源时,需要确保该资源不再被其他进程使用。如果在其他进程还在使用该资源时删除,可能会导致这些进程出现异常,甚至崩溃。因此,在删除之前,需要先确认相关进程已经停止使用该资源,或者采取适当的同步机制来确保安全删除。

思考问题12:如何定期清理系统中的IPC资源,以避免资源浪费?

可以编写脚本,结合ipcs命令查看IPC资源的使用情况,根据一定的规则(如资源的使用时间、是否有进程关联等)筛选出不再使用的资源,然后使用ipcrm命令进行删除。还可以设置定时任务,定期执行该脚本,以确保系统中的IPC资源得到及时清理。

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

相关文章:

  • 【计算机视觉】目标检测:yoloV1~yoloV11项目论文及对比
  • 【信息系统项目管理师-论文真题】2011上半年论文详解(包括解题思路和写作要点)
  • LVGL -文本显示 英文、中文
  • MaC QT 槽函数和Lambda表达式
  • Leetcode刷题记录29——矩阵置零
  • 【JavaScript】性能优化:打造高效前端应用
  • 数据赋能(212)——质量管理——统一性原则
  • ROS2学习笔记|实现订阅消息并朗读的详细步骤
  • Easy云盘总结篇-登录注册
  • C# 编程核心:控制流与方法调用详解
  • 力扣每日一题 ​838. 推多米诺​
  • PyCharm中全局搜索无效
  • 软件测试名词科普:驱动模块、桩模块
  • springAop代理责任链模式源码解析
  • Socket-TCP
  • 【信息系统项目管理师】【2017年-2024年】计算画图题汇总——案例分析
  • [更新完毕]2025东三省B题深圳杯B题数学建模挑战赛数模思路代码文章教学:LED显示屏颜色转换设计与校正
  • ES6入门---第二单元 模块三:对象新增、
  • 深入理解 HttpExchange_Java 中构建 HTTP 服务的基础组件
  • 0基础 | STM32 | TB6612电机驱动使用
  • 2025年- H22-Lc130-206. 反转链表(链表)---java版
  • FreeRtos实战从入门到精通--任务创建和删除(动态方法)--事了拂衣去,深藏功与名
  • scikit-learn在监督学习算法的应用
  • linux下,ollama会把模型文件保存在哪里?
  • 神经网络基础-从零开始搭建一个神经网络
  • 【掌握 DDL】:SQL 中的数据库与表管理
  • 安卓基础(悬浮窗分级菜单和弹窗)
  • 【现代深度学习技术】现代循环神经网络04:双向循环神经网络
  • 游戏引擎学习第256天:XBox 控制器卡顿和修复 GL Blit 伽玛问题
  • java学习之数据结构:三、八大排序