从内核视角彻底搞懂Linux进程状态:运行、阻塞、挂起与内核链表的底层实现
从内核视角彻底搞懂Linux进程状态:运行、阻塞、挂起与内核链表的底层实现

📝 前言
提到进程状态,很多人的第一反应是课本上那个经典的"三态模型"——运行、就绪、阻塞。但当你真正打开Linux终端,用ps aux命令查看进程状态时,会看到R、S、D、T、Z等多种状态,这时候你可能会疑惑:这些状态和课本上的三态有什么关系?为什么会有这么多细分状态?进程状态转换的底层到底发生了什么?
课本上的三态模型是高度抽象的,而在Linux内核中,每一种状态都对应着一个具体的队列。进程的状态转换,本质上就是内核把进程控制块(PCB,在Linux中是task_struct结构体)从一个队列移动到另一个队列的过程。
通过本文,你将掌握:
| 技能 | 应用场景 |
|---|---|
| 理解内核三态模型的本质 | 从数据结构角度理解进程管理 |
| 掌握Linux 7种进程状态 | 实际排查进程问题(如D状态进程杀不死) |
| 理解侵入式链表设计 | 阅读内核源码、理解内核数据结构 |
掌握container_of宏原理 |
内核驱动开发、嵌入式系统编程 |
| 复现各种进程状态 | Linux系统运维、性能调优 |
| 理解僵尸进程与孤儿进程 | 避免内存泄漏、进程资源管理 |
📌 前置知识: 基本的C语言语法、Linux常用命令
文章目录
一、🔥 内核视角下的三态模型:运行、阻塞与挂起
课本上的三态模型是高度抽象的,而在Linux内核中,每一种状态都对应着一个具体的队列。进程的状态转换,就是内核把task_struct从一个队列移动到另一个队列的过程。
1.1 运行态(Running):在CPU调度队列中等待执行
运行态的本质:进程的task_struct结构体位于**运行队列(runqueue)**中,并且已经获得了CPU资源,正在执行代码。
注意:在Linux内核中,"运行态"其实包含了课本上的"运行"和"就绪"两种状态。也就是说,所有在运行队列中的进程,状态都是R (running)。调度器会从运行队列中选择优先级最高的进程,分配CPU时间片让它执行。
- 一个CPU对应一个运行队列(单核心系统只有一个运行队列)
- 运行队列中的进程按照调度算法(如FIFO、CFS)排序
- 当进程的时间片用完,或者有更高优先级的进程进入运行队列时,当前进程会被抢占,重新回到运行队列中等待
💡 在Linux内核源代码中,所有运行在系统里的进程都以
task_struct双链表的形式存在内核里。
1.2 阻塞态(Blocked):在硬件等待队列中等待资源
阻塞态的本质:进程需要等待某种硬件资源或事件(如键盘输入、磁盘IO、网络数据)就绪,因此被内核从运行队列中移除,加入到对应硬件设备的**等待队列(wait queue)**中。
这是很多人容易误解的地方:阻塞的进程不是"什么都不做",而是被内核挂到了具体硬件设备的等待队列上。当硬件设备完成操作后,会触发中断,内核在中断处理函数中,会把等待该设备的进程从等待队列中唤醒,重新加入运行队列。
比如:
- 当你在终端输入字符时,等待键盘输入的进程会被挂到键盘设备的等待队列上
- 当你读取一个文件时,等待磁盘IO完成的进程会被挂到磁盘设备的等待队列上
- 当你等待网络数据时,进程会被挂到对应网卡的等待队列上
1.3 挂起态(Suspended):被换出到磁盘交换分区
挂起态的本质:当系统内存不足时,内核会把一些暂时不运行的进程(包括运行队列中等待时间较长的进程,和等待队列中等待资源的进程)的内存数据**换出(swap out)**到磁盘的交换分区(swap partition)中,释放出物理内存给更需要的进程使用。
被挂起的进程,它的task_struct结构体仍然保留在内存中,但它的代码和数据已经不在物理内存里了。当内核需要运行这个进程时,会把它的代码和数据从交换分区**换入(swap in)**到物理内存中,然后重新加入运行队列。
挂起态是对阻塞态的进一步优化:当内存充足时,阻塞的进程仍然保留在内存中;当内存不足时,阻塞的进程会被换出到磁盘,进入挂起态。
二、🔗 进程状态流动的三大核心场所
现在我们来详细拆解进程状态流动的三个核心场所:运行队列、硬件等待队列和磁盘交换分区。理解了这三个队列,你就理解了进程状态转换的全部逻辑。
2.1 运行队列(Runqueue):CPU的"待办事项列表"
运行队列是内核中最重要的队列之一,它保存了所有等待CPU执行的进程。调度器的核心工作,就是从运行队列中选择下一个要运行的进程。
在单核心系统中,只有一个运行队列;在多核心系统中,每个CPU核心都有自己独立的运行队列。这样设计的好处是避免了多个CPU竞争同一个运行队列的锁,提高了调度效率。
运行队列中的进程状态:全部都是R (running)状态。
进程进入运行队列的时机:
- 进程被创建时(fork系统调用)
- 阻塞的进程被唤醒时(资源就绪)
- 挂起的进程被换入内存时
- 进程的时间片用完,被抢占后重新回到运行队列
进程离开运行队列的时机:
- 进程需要等待资源,进入阻塞态
- 进程退出(exit系统调用)
- 内存不足时,被换出到交换分区,进入挂起态
2.2 硬件等待队列(Wait Queue):设备的"等待者列表"
每一个硬件设备(键盘、磁盘、网卡、显示器等)在内核中都有自己的等待队列。当进程需要访问某个硬件设备,而设备当前忙时,内核会把进程加入到该设备的等待队列中,然后调度其他进程运行。
当硬件设备完成操作后,会触发一个硬件中断。内核在中断处理函数中,会遍历该设备的等待队列,把所有等待该事件的进程唤醒,将它们的task_struct从等待队列移动到运行队列。
等待队列中的进程状态:主要是S (可中断睡眠)和D (不可中断睡眠)状态,这两种状态都属于阻塞态。
进程进入等待队列的时机:
- 调用
read()读取键盘、磁盘、网络等设备 - 调用
write()写入设备 - 等待信号量、互斥锁等同步原语
- 调用
sleep()等延时函数
进程离开等待队列的时机:
- 等待的资源就绪,被中断处理函数唤醒
- 可中断睡眠状态的进程收到信号
- 内存不足时,被换出到交换分区,进入挂起态
2.3 磁盘交换分区(Swap Partition):内存的"后备仓库"
交换分区是磁盘上的一块特殊区域,用作物理内存的扩展。当系统物理内存不足时,内核会使用页面置换算法(如LRU),把一些不常用的内存页面换出到交换分区中。
对于进程来说,如果它的所有内存页面都被换出到交换分区,那么这个进程就进入了挂起态。挂起态的进程无法被调度执行,因为它的代码和数据都不在物理内存中。
交换分区中的进程状态:没有单独的状态标志,内核通过页表项来判断进程的页面是否在物理内存中。
进程被换出到交换分区的时机:
- 系统物理内存不足
- 进程长时间处于阻塞态,没有被调度执行
进程被换入到物理内存的时机:
- 进程被唤醒,需要被调度执行
- 系统有足够的空闲物理内存
三、🧩 为什么一个PCB会存在于多个数据结构中?
现在我们来回答一个关键问题:为什么一个进程的task_struct会同时存在于多个数据结构中?这是理解内核进程管理的核心。
3.1 内核需要从多个维度管理进程
内核需要从不同的角度来管理进程,因此需要把同一个task_struct加入到多个不同的链表中:
- 调度维度:需要把
task_struct加入到运行队列中,以便调度器选择下一个要运行的进程 - 设备管理维度:当进程等待某个设备时,需要把
task_struct加入到该设备的等待队列中 - 内存管理维度:需要把
task_struct加入到LRU队列中,以便内存不足时选择要换出的进程 - 进程关系维度:需要把
task_struct加入到进程树中,以便管理父子进程关系 - 信号管理维度:需要把
task_struct加入到信号队列中,以便处理发送给进程的信号
如果一个task_struct只能存在于一个链表中,那么内核就无法同时从多个维度管理进程。因此,task_struct结构体中包含了多个链表节点(list_head结构体),每个链表节点对应一个链表。
3.2 侵入式链表:内核数据结构的精髓
为了实现一个数据结构同时存在于多个链表中,Linux内核使用了**侵入式链表(Intrusive Linked List)**的设计。
普通链表的设计是:链表节点包含数据指针,指向具体的数据结构。而侵入式链表的设计正好相反:数据结构包含链表节点,链表节点嵌入在数据结构内部。
我们来对比一下代码实现:
普通链表:
struct Node {
void *data; // 指向数据的指针
struct Node *next;
struct Node *prev;
};
内核侵入式链表:
// 链表节点结构体,只包含前后指针
struct list_head {
struct list_head *next;
struct list_head *prev;
};
// 进程控制块结构体,包含多个链表节点
struct task_struct {
// ... 其他成员 ...
struct list_head run_list; // 运行队列节点
struct list_head wait_list; // 等待队列节点
struct list_head lru_list; // LRU队列节点
struct list_head sibling; // 兄弟进程节点
// ... 其他成员 ...
};
侵入式链表的优势非常明显:
- 一个数据结构可以同时存在于多个链表中,每个链表对应一个
list_head成员 - 不需要额外的内存分配来存储链表节点,节省了内存开销
- 链表操作非常高效,只需要修改指针即可
四、🎯 内核链表如何找到数据结构?container_of宏的魔法
现在你可能会问:当我们遍历链表时,得到的是list_head结构体的指针,怎么通过这个指针找到包含它的整个task_struct结构体呢?
这就要用到Linux内核中最经典的宏之一:container_of。这个宏的作用是:通过一个结构体成员的指针,计算出整个结构体的指针。
4.1 container_of宏的原理
container_of宏的定义如下(简化版):
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) ); \
})
其中:
ptr:指向结构体成员的指针type:整个结构体的类型member:结构体成员的名字
这个宏的原理非常巧妙:
- 首先把地址0强制转换为
type*类型,然后访问它的member成员,这样就得到了member成员在结构体中的偏移量 - 然后把
ptr指针转换为char*类型,减去这个偏移量,就得到了整个结构体的起始地址
我们用一个例子来理解:
struct task_struct {
int pid;
int state;
struct list_head run_list;
// ... 其他成员 ...
};
// 假设我们有一个指向run_list成员的指针
struct list_head *ptr = &task->run_list;
// 使用container_of宏得到整个task_struct的指针
struct task_struct *task = container_of(ptr, struct task_struct, run_list);
在这个例子中,offsetof(struct task_struct, run_list)会计算出run_list成员在task_struct结构体中的偏移量(假设是8字节)。然后把ptr指针减去8字节,就得到了task_struct结构体的起始地址。
4.2 内核链表的遍历
有了container_of宏,内核链表的遍历就变得非常简单了。内核提供了list_for_each_entry宏来遍历链表:
#define list_for_each_entry(pos, head, member) \
for (pos = list_entry((head)->next, typeof(*pos), member); \
&pos->member != (head); \
pos = list_entry(pos->member.next, typeof(*pos), member))
其中list_entry就是container_of的别名。
比如,遍历运行队列中的所有进程:
struct task_struct *task;
list_for_each_entry(task, &runqueue, run_list) {
// 处理每个进程
printk("PID: %d\n", task->pid);
}
五、📊 Linux内核中真实的进程状态
Linux内核在include/linux/sched.h文件中定义了7种进程状态。下面我们来看看这些状态之间的完整转换关系。
Linux内核中定义的进程状态数组如下:
static const char *const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
现在我们把这些状态和我们之前讲的三态模型对应起来:
| Linux状态 | 对应三态模型 | 说明 |
|---|---|---|
| R (running) | 运行态 | 进程在运行队列中,正在运行或等待调度 |
| S (sleeping) | 阻塞态 | 可中断睡眠,可以被信号唤醒 |
| D (disk sleep) | 阻塞态 | 不可中断睡眠,只能被资源就绪唤醒 |
| T (stopped) | 阻塞态 | 进程被停止,收到SIGCONT信号后继续运行 |
| t (tracing stop) | 阻塞态 | 进程被调试器暂停 |
| X (dead) | 终止态 | 进程已经完全退出,资源已经被释放 |
| Z (zombie) | 终止态 | 进程已经退出,但父进程还没有调用wait()回收资源 |
5.1 R状态(Running):运行态
R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中,要么在运行队列里。
也就是说,所有在运行队列中的进程,无论是否正在CPU上执行,状态都是R。
5.2 S状态(Sleeping):可中断睡眠
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠interruptible sleep)。
S状态是最常见的阻塞状态,进程在等待资源时可以被信号唤醒。比如sleep命令、等待键盘输入的进程都处于S状态。
⚠️ S状态的进程收到信号后会被唤醒,所以如果你用
kill命令给S状态的进程发信号,它会立即响应。
5.3 D状态(Disk Sleep):不可中断睡眠
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
D状态是Linux特有的状态,也是很多人最困惑的状态。很多人会遇到这样的情况:一个进程处于D状态,用kill -9都杀不死,这是为什么呢?
D状态的本质:进程正在执行关键的磁盘IO操作,内核为了保证数据一致性,不允许这个进程被任何信号中断。
当进程执行磁盘IO操作时(如读取文件、写入文件),它会向磁盘控制器发送IO请求,然后进入D状态等待IO完成。在这个过程中,如果进程被信号中断,可能会导致IO操作被打断,从而造成文件系统损坏或者数据丢失。因此,内核把这些进程设置为不可中断睡眠状态,只能等待IO操作完成后才能被唤醒。
D状态进程的特点:
- 不响应任何信号(包括SIGKILL信号)
- 只能等待IO操作完成后自动唤醒
- 如果IO操作永远无法完成(比如磁盘损坏),那么这个进程会永远处于D状态,只能通过重启系统解决
什么时候会进入D状态:
- 执行磁盘读写操作(read、write系统调用)
- 执行文件系统操作(如mount、umount)
- 等待块设备就绪
5.4 T状态(Stopped):停止状态
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
T状态的进程不执行任何代码,只是被暂停了。它仍然保留在内存中,可以被恢复执行。
5.5 t状态(Tracing Stop):跟踪停止
t跟踪停止状态(tracing stop):进程被调试器(如gdb)设置断点后进入的状态。
当你用gdb调试一个程序,在断点处停下来时,进程就会进入t状态。
5.6 Z状态(Zombie):僵尸进程
Z僵死状态(Zombies):是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时,就会产生僵死(尸)进程。
僵死进程会以终止状态保持在进程表中,并且会一直等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入Z状态。
僵尸进程的危害:
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在
task_struct(PCB)中 - 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费
- 这就是内存泄漏!
⚠️ 僵尸进程已经释放了大部分资源,只保留了
task_struct结构体和退出状态,等待父进程回收。
5.7 X状态(Dead):死亡状态
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
进程已经完全退出,所有资源都已经被释放,这个状态非常短暂,几乎看不到。
5.8 孤儿进程(Orphan Process)
除了上面7种状态,我们还需要了解孤儿进程的概念。
孤儿进程:父进程如果提前退出,那么子进程后退出,进入Z状态之后,该如何处理呢?
父进程先退出,子进程就称之为"孤儿进程"。孤儿进程会被1号init/systemd进程领养,由init/systemd进程负责回收。
💡 孤儿进程不是僵尸进程!孤儿进程的父进程变成了init进程,init进程会定期调用wait()回收子进程资源,所以孤儿进程不会导致内存泄漏。
六、🔧 实际实验:在Linux中复现各种进程状态
理论讲了这么多,现在我们来做实际实验,在自己的Linux机器上复现每一种状态。
6.1 实验准备
打开Linux终端,我们会使用以下命令:
ps aux:查看所有进程的状态top:实时查看进程状态kill:向进程发送信号dd:用于产生磁盘IO,复现D状态
💡
ps aux命令中:a表示显示一个终端所有的进程,包括其他用户的进程;x表示显示没有控制终端的进程;u表示以用户为中心的格式显示进程信息。
6.2 复现R状态(运行态)
R状态的进程正在占用CPU或者等待CPU调度。我们可以用一个死循环来复现:
# 后台运行一个死循环
while :; do :; done &
然后用ps aux查看这个进程的状态:
ps aux | grep "while :"
你会看到输出类似这样:
user 12345 99.9 0.0 4356 720 pts/0 R 10:00 0:10 bash
其中R就表示这个进程处于运行态。
6.3 复现S状态(可中断睡眠)
S状态是最常见的状态,大部分后台进程都处于S状态。我们用sleep命令来复现:
# 后台运行一个sleep进程,睡眠1000秒
sleep 1000 &
然后用ps aux查看:
ps aux | grep sleep
输出:
user 12346 0.0 0.0 4356 720 pts/0 S 10:01 0:00 sleep 1000
其中S表示可中断睡眠状态。我们可以用kill命令给它发信号唤醒它:
kill -9 12346
6.4 复现D状态(不可中断睡眠)
D状态的进程正在执行磁盘IO操作。我们用dd命令读写一个大文件来复现:
# 后台运行dd命令,创建一个10GB的文件
dd if=/dev/zero of=test bs=1M count=10000 &
然后立即用ps aux查看dd进程的状态:
ps aux | grep dd
你会看到输出类似这样:
user 12347 10.0 0.0 4356 720 pts/0 D 10:02 0:05 dd if=/dev/zero of=test bs=1M count=10000
其中D表示不可中断睡眠状态。现在你可以尝试用kill -9杀死它:
kill -9 12347
你会发现,这个进程并没有立即被杀死,而是会继续运行一段时间,直到IO操作完成后才会退出。这就是D状态的特点:不响应信号,只能等待IO完成。
6.5 复现T状态(停止)
我们可以用kill -STOP命令让一个进程进入停止状态:
# 后台运行一个sleep进程
sleep 1000 &
# 查看进程ID
ps aux | grep sleep
# 发送SIGSTOP信号
kill -STOP 12348
# 再次查看状态
ps aux | grep sleep
输出:
user 12348 0.0 0.0 4356 720 pts/0 T 10:03 0:00 sleep 1000
其中T表示停止状态。我们可以用kill -CONT命令让它继续运行:
kill -CONT 12348
# 再次查看状态
ps aux | grep sleep
输出:
user 12348 0.0 0.0 4356 720 pts/0 S 10:03 0:00 sleep 1000
6.6 复现Z状态(僵尸进程)
僵尸进程是子进程退出后,父进程没有回收资源导致的。我们写一个简单的C程序来复现:
// zombie.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程,立即退出
printf("子进程退出,PID: %d\n", getpid());
exit(0);
} else if (pid > 0) {
// 父进程,不调用wait,睡眠100秒
printf("父进程运行,PID: %d\n", getpid());
sleep(100);
} else {
perror("fork失败");
return 1;
}
return 0;
}
编译并运行:
gcc zombie.c -o zombie
./zombie &
然后用ps aux查看:
ps aux | grep defunct
输出:
user 12350 0.0 0.0 0 0 pts/0 Z 10:04 0:00 [zombie] <defunct>
其中Z表示僵尸状态,<defunct>表示这是一个僵尸进程。100秒后,父进程退出,僵尸进程会被init进程回收。
6.7 复现孤儿进程
孤儿进程是父进程先退出,子进程还在运行的情况。我们写一个简单的C程序来复现:
// orphan.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
} else if (id == 0) {
// 子进程,睡眠10秒
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(10);
printf("Child exit, ppid now: %d\n", getppid());
} else {
// 父进程,睡眠3秒后退出
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
编译并运行:
gcc orphan.c -o orphan
./orphan
运行后观察输出,你会发现父进程退出后,子进程的ppid变成了1(init/systemd进程)。
七、🧠 知识体系总结
整个进程状态管理体系是一个层层递进的结构,从抽象的理论模型到具体的内核实现,再到底层的数据结构支撑:
- 最上层:课本上抽象的三态模型,它是对进程状态的高度概括
- 中间层:内核的具体实现,通过三个核心队列来管理进程状态的流动
- 底层:数据结构的支撑,
task_struct、侵入式链表和container_of宏构成了内核进程管理的基石 - 最下层:Linux内核中真实的7种进程状态,它们是对三态模型的细化和扩展
- 最后:通过实际实验验证我们的理论,加深对进程状态的理解
八、🤔 几个思考题
学完本文,来试试回答这些问题:
1️⃣ 为什么D状态的进程用kill -9都杀不死?
答: D状态是不可中断睡眠状态,进程正在执行关键的磁盘IO操作。内核为了保证数据一致性,不允许这个进程被任何信号中断。如果强行中断,可能会导致文件系统损坏或数据丢失。所以D状态的进程只能等待IO操作完成后自动唤醒。
💡 如果D状态进程一直不退出,可能是磁盘故障或驱动问题,需要检查硬件。
2️⃣ 僵尸进程和孤儿进程有什么区别?
答:
- 僵尸进程:子进程已经退出,但父进程没有调用
wait()回收子进程的资源。僵尸进程保留了task_struct和退出状态,会造成内存泄漏。 - 孤儿进程:父进程先退出,子进程还在运行。孤儿进程会被init/systemd进程(PID=1)领养,由init进程负责回收,不会造成内存泄漏。
3️⃣ 为什么一个task_struct需要同时存在于多个链表中?
答: 内核需要从多个维度管理进程:调度维度(运行队列)、设备管理维度(等待队列)、内存管理维度(LRU队列)、进程关系维度(进程树)、信号管理维度(信号队列)。如果task_struct只能存在于一个链表中,就无法同时从多个维度管理进程。侵入式链表的设计让一个数据结构可以同时存在于多个链表中,每个链表对应一个list_head成员。
4️⃣ container_of宏的原理是什么?
答: container_of宏通过结构体成员的指针,计算出整个结构体的指针。它的原理是:
- 利用
offsetof宏计算出成员在结构体中的偏移量 - 将成员指针转换为
char*类型,减去偏移量,得到结构体的起始地址
公式:结构体地址 = 成员指针 - 成员偏移量
九、💡 结语
通过这篇文章,我们从内核数据结构的视角,彻底拆解了Linux进程状态的本质。我们明白了:
- 进程状态不是抽象的概念,而是进程在不同队列之间的流动
- 一个PCB会存在于多个数据结构中,因为内核需要从多个维度管理进程
- 内核使用侵入式链表和
container_of宏,高效地实现了一个数据结构同时存在于多个链表中 - Linux特有的D状态是为了保证磁盘IO的数据一致性,不能被信号中断
- 僵尸进程会造成内存泄漏,孤儿进程不会
- 所有的进程状态转换,本质上都是链表节点的增删改查
理解了这些底层原理,你再遇到进程状态相关的问题时(比如D状态进程杀不死、僵尸进程过多等),就能够从根本上分析问题的原因,而不是只会用kill命令盲目尝试。
✅ 本节完…
📝 作者:say-fall | 编辑:say-fall | 🌟 原创不易,如果对你有帮助,记得 👍 点赞 + ⭐ 收藏 哦!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)