Linux 进程间通信:命名管道与 System V 共享内存深度解析

目录

  • 一、前置概念
    • [1. 匿名管道(Pipe)------ 仅作对比](#1. 匿名管道(Pipe)—— 仅作对比)
    • [2. IPC 设计哲学对比](#2. IPC 设计哲学对比)
  • [二、命名管道(FIFO / Named Pipe)](#二、命名管道(FIFO / Named Pipe))
    • [1. 为什么需要 FIFO?](#1. 为什么需要 FIFO?)
    • [2. 文件系统视角:FIFO 的特殊性](#2. 文件系统视角:FIFO 的特殊性)
    • 3.打开规则:阻塞语义详解
    • [4. API 深度解析](#4. API 深度解析)
    • [5. 完整示例:多写者单读者模型](#5. 完整示例:多写者单读者模型)
    • [6. FIFO 的边界与陷阱](#6. FIFO 的边界与陷阱)
  • [三、System V 共享内存](#三、System V 共享内存)
    • 1.设计哲学:零拷贝的极致
    • [2. System V IPC 标识体系](#2. System V IPC 标识体系)
    • [3. 核心 API 详解与陷阱](#3. 核心 API 详解与陷阱)
      • [ftok() ------ 键生成的不确定性](#ftok() —— 键生成的不确定性)
      • [shmget() ------ 创建与获取的语义](#shmget() —— 创建与获取的语义)
      • [shmat() ------ 地址映射的细节](#shmat() —— 地址映射的细节)
      • [shmctl() ------ 生命周期管理](#shmctl() —— 生命周期管理)
    • 4.完整示例:带生命周期协调的共享内存
    • [5. 同步机制:共享内存的必需伴侣](#5. 同步机制:共享内存的必需伴侣)
    • 6.管理命令与调试
    • [7. System V vs POSIX 共享内存](#7. System V vs POSIX 共享内存)
  • 四、选型决策树
  • 五、性能基准参考
  • 六、关键要点总结
  • 参考资源

本文聚焦:命名管道(FIFO)的文件系统机制与 System V 共享内存的内核实现,从 API 细节到实际应用场景,帮你掌握 Linux 高性能 IPC 的核心技术。


一、前置概念

1. 匿名管道(Pipe)------ 仅作对比

匿名管道是最基础的 IPC,通过 pipe(fd) 创建,仅限父子/兄弟进程使用。其局限性催生了命名管道。

2. IPC 设计哲学对比

机制 核心思想 关键差异
命名管道 文件抽象 --- 一切皆文件 通过路径名解耦进程关系
共享内存 内存映射 --- 零拷贝哲学 直接物理内存共享,绕过内核

解耦进程关系
零拷贝高性能
System V 共享内存
ftok生成key
shmget创建/获取
shmat映射到进程空间
直接物理内存访问
命名管道 FIFO
文件系统路径

/tmp/my_fifo
内核环形缓冲区
任意独立进程
任意独立进程


二、命名管道(FIFO / Named Pipe)

1. 为什么需要 FIFO?

匿名管道的致命缺陷:必须有亲缘关系 。命名管道通过文件系统路径 作为 rendezvous point(会合点),使任意两个独立进程能够找到彼此。
进程 B(读者)
进程 A(写者)
内核管道层
文件系统层
数据写入
数据读取
文件名作为标识
无亲缘关系
/tmp/my_fifo

类型: p (管道文件)
环形缓冲区

同匿名管道实现
写端 ← 数据流 → 读端
open(O_WRONLY)
write()
open(O_RDONLY)
read()

2. 文件系统视角:FIFO 的特殊性

FIFO 是伪文件 ------ 存在目录项,但不占用磁盘数据块:

bash 复制代码
$ mkfifo /tmp/my_fifo
$ ls -l /tmp/my_fifo
prw-r--r-- 1 user group 0 Apr 14 21:30 /tmp/my_fifo
# ↑ 'p' 表示管道类型,大小始终为 0(无实际数据存储)

关键洞察 :FIFO 的持久化的是管道端点,而非数据。数据始终在内核缓冲区流动。

3.打开规则:阻塞语义详解

FIFO 的 open() 行为是理解其同步机制的关键:
O_RDONLY
O_WRONLY
O_RDWR
O_NONBLOCK




O_RDONLY + 无写者
O_WRONLY + 无读者
open(FIFO_PATH, flags)
检查 flags
是否有写者?
是否有读者?
立即成功

⚠️ 破坏管道语义
非阻塞模式

特殊处理
阻塞等待
成功返回 fd
阻塞等待
成功返回 fd
立即成功

read() 返回 0
失败: ENXIO

打开方式 行为描述 典型用途
O_RDONLY 阻塞 直到有进程以 O_WRONLY 打开 消费者等待生产者
O_WRONLY 阻塞 直到有进程以 O_RDONLY 打开 生产者等待消费者
O_RDWR 不阻塞(但破坏管道语义) 避免使用
O_NONBLOCK + O_RDONLY 立即返回,但 read() 返回 0 直到有写入 轮询检查
O_NONBLOCK + O_WRONLY 失败返回 ENXIO(无读者) 快速失败模式

重要:多个进程可同时打开同一 FIFO 的读端或写端,内核会管理多路数据合并。

4. API 深度解析

4.1 创建 FIFO

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • modeumask 影响,实际权限 = mode & ~umask
  • 幂等性处理EEXIST 错误通常应忽略(FIFO 已存在即可复用)

4.2安全删除模式

c 复制代码
// 非原子操作的风险:删除后、重新创建前,可能有进程打开失败
unlink(pathname);  // 删除目录项
mkfifo(pathname, 0666);  // 重新创建 --- 竞态窗口!

// 更好的做法:使用文件锁或检查现有 FIFO 的权限
struct stat st;
if (stat(pathname, &st) == 0) {
    if (!S_ISFIFO(st.st_mode)) {
        // 存在同名非管道文件,报错或处理
    }
    // FIFO 已存在,直接使用
} else {
    mkfifo(pathname, 0666);
}

5. 完整示例:多写者单读者模型

reader.c ------ 聚合多个生产者的数据:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#define FIFO_PATH "/tmp/multi_writer.fifo"
#define BUF_SIZE 4096

volatile sig_atomic_t running = 1;

void handle_sigint(int sig) {
    running = 0;
}

int main() {
    signal(SIGINT, handle_sigint);
    
    // 创建 FIFO(幂等)
    if (mkfifo(FIFO_PATH, 0666) == -1) {
        // 忽略 EEXIST,其他错误退出
    }
    
    // 以阻塞读方式打开 ------ 等待第一个写者
    printf("Waiting for writers...\n");
    int fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    
    // 关键技巧:立即以写方式打开自己的读端,防止 EOF 轮询
    // 当最后一个写者关闭时,读端会收到 EOF(read 返回 0)
    // 如果不希望这样,可以保持一个写端打开:
    int fd_dummy = open(FIFO_PATH, O_WRONLY);
    
    char buf[BUF_SIZE];
    ssize_t n;
    
    while (running && (n = read(fd, buf, BUF_SIZE)) > 0) {
        // 处理数据:这里简单输出到 stdout
        write(STDOUT_FILENO, buf, n);
    }
    
    close(fd);
    close(fd_dummy);
    unlink(FIFO_PATH);
    
    return 0;
}

writer.c ------ 可启动多个实例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <time.h>

#define FIFO_PATH "/tmp/multi_writer.fifo"

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <message>\n", argv[0]);
        exit(1);
    }
    
    // 非阻塞打开检查:如果无读者,快速失败
    int fd = open(FIFO_PATH, O_WRONLY | O_NONBLOCK);
    if (fd == -1) {
        perror("No reader available");
        exit(1);
    }
    
    // 切回阻塞模式(或保持非阻塞并处理 EAGAIN)
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
    
    // 构造带进程 ID 和时间戳的消息
    char msg[256];
    snprintf(msg, sizeof(msg), "[%d @ %ld]: %s\n", 
             getpid(), time(NULL), argv[1]);
    
    write(fd, msg, strlen(msg));
    close(fd);
    
    return 0;
}

6. FIFO 的边界与陷阱

解决方案
FIFO 常见陷阱
EOF 轮询问题

所有写者关闭后

read() 返回 0
SIGPIPE 信号

读者关闭后写者

收到管道破裂信号
原子性限制

> PIPE_BUF 时

写入非原子
命名冲突

多个应用使用

相同路径
dummy 写端保活

或信号机制
signal(SIGPIPE, SIG_IGN)

或捕获处理
分块写入

或改用共享内存
应用专属目录

如 /run/myapp/

陷阱 现象 解决方案
EOF 轮询 所有写者关闭后,read() 返回 0,循环退出 保持 dummy 写端打开,或使用信号机制
管道破裂 读端关闭后写者收到 SIGPIPE 信号 捕获 SIGPIPE 或忽略(signal(SIGPIPE, SIG_IGN)
原子性限制 数据 > PIPE_BUF(通常 4KB/64KB)时写入非原子 大数据分块或改用共享内存
命名冲突 多个应用使用相同路径 使用应用专属目录(如 /run/myapp/

三、System V 共享内存

1.设计哲学:零拷贝的极致

共享内存是 Linux 最快的 IPC ,核心在于绕过内核的数据拷贝
性能对比
共享内存(零拷贝)
页表映射
页表映射
进程A 虚拟地址空间
同一块物理内存
进程B 虚拟地址空间
0次内核切换
0次数据拷贝
直接内存访问
传统 IPC(管道/消息队列)
进程A 用户态缓冲区
copy_to_user
内核态缓冲区
copy_from_user
进程B 用户态缓冲区
2次用户-内核态切换
2次数据拷贝
⚠️ 代价:内核不参与同步

必须由用户自行实现互斥

代价 :内核不再参与同步,必须由用户自行实现互斥

2. System V IPC 标识体系

System V 使用键(key)→ 标识符(id)两级寻址,支持内核持久化(资源不随进程退出而销毁):
内核层: IPC 对象表
系统调用层
应用层
文件路径

如 /tmp/myapp
项目ID

如 'A' (0x41)
ftok() 生成
key_t 键值

32位整数
shmget(key, size, flags)
shmid 标识符

用于后续操作
shmid_ds 结构数组
shm_perm: 权限与所有者
shm_segsz: 段大小
shm_nattch: 当前附加进程数

=0 时允许删除
semid_ds: 信号量

(通常配合共享内存)
msgid_ds: 消息队列

3. 核心 API 详解与陷阱

ftok() ------ 键生成的不确定性

c 复制代码
key_t ftok(const char *pathname, int proj_id);

陷阱pathname 指向的文件必须存在且可访问 ,否则生成相同的 key = -1

c 复制代码
// 健壮的键生成
key_t generate_key(const char *base_path, char proj) {
    // 确保文件存在
    int fd = open(base_path, O_CREAT | O_RDWR, 0666);
    close(fd);
    
    key_t key = ftok(base_path, proj);
    if (key == -1) {
        perror("ftok failed --- check if file exists");
        exit(1);
    }
    return key;
}

shmget() ------ 创建与获取的语义

c 复制代码
int shmget(key_t key, size_t size, int shmflg);

标志位组合策略
IPC_CREAT | IPC_EXCL | 0666
IPC_CREAT | 0666
0666
shmget(key, size, shmflg)
flags 组合
严格创建

存在则失败

EEXIST
获取或创建

存在则获取

不存在则创建
纯获取

必须已存在

否则 ENOENT
保证新建

适合初始化者
灵活模式

适合通用逻辑
严格依赖

适合消费者

场景 标志 行为
创建新段 `IPC_CREAT IPC_EXCL
获取或创建 `IPC_CREAT 0666`
纯获取 0666(或 0) 必须已存在,否则失败(ENOENT

关键size 在创建时生效,获取已有段时被忽略(但通常应传入 0 或实际大小)。

shmat() ------ 地址映射的细节

c 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

地址选择策略

c 复制代码
// 策略1:让内核选择地址(推荐,可移植)
char *addr = shmat(shmid, NULL, 0);

// 策略2:指定地址(仅在特殊场景使用,如需要固定地址的硬件交互)
// 地址必须页对齐,且不与现有映射冲突
char *addr = shmat(shmid, (void *)0x7f0000000000, 0);

// 策略3:只读附加(用于纯消费者进程)
char *addr = shmat(shmid, NULL, SHM_RDONLY);

返回值检查陷阱

c 复制代码
// 错误!shmat 返回 (void *)-1,不是 NULL
if (addr == NULL) { /* 错误检查 */ }

// 正确
if (addr == (void *)-1) { 
    perror("shmat");
    exit(1);
}

shmctl() ------ 生命周期管理

c 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

删除的延迟语义

c 复制代码
// 标记删除 ------ 实际销毁发生在最后一个进程 shmdt 之后
shmctl(shmid, IPC_RMID, NULL);

// 检查当前附加数
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
printf("Current attaches: %zu\n", ds.shm_nattch);

共享内存生命周期
shmget()

创建段
shmat()

进程A附加

nattch=1
shmat()

进程B附加

nattch=2
shmctl(IPC_RMID)

标记删除

但 nattch>0
shmdt()

进程A分离

nattch=1
shmdt()

进程B分离

nattch=0
实际销毁

物理内存释放

重要IPC_RMID标记删除,不是立即销毁。这允许创建者提前标记删除,而使用者继续访问直到完成。

4.完整示例:带生命周期协调的共享内存

common.h ------ 协议定义:

c 复制代码
#ifndef COMMON_H
#define COMMON_H

#include <stdint.h>
#include <pthread.h>  // 实际应使用信号量,这里简化

#define SHM_KEY_PATH "/tmp/shm_demo"
#define SHM_KEY_ID 'X'
#define SHM_SIZE (1024 * 1024)  // 1MB

// 共享内存布局:头部 + 数据区
struct shm_header {
    volatile uint32_t magic;      // 魔数,标识初始化完成
    volatile uint32_t version;    // 协议版本
    volatile uint32_t data_len;   // 当前数据长度
    volatile uint32_t ready;      // 数据就绪标志(简易同步)
    // 实际生产环境应使用 sem_t 信号量
};

#define SHM_DATA_OFFSET sizeof(struct shm_header)
#define MAGIC_NUMBER 0x53484D00  // "SHM\0"

#endif

producer.c ------ 生产者,负责创建与销毁协调:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <fcntl.h>
#include "common.h"

int main() {
    // 1. 生成键
    int tmp_fd = open(SHM_KEY_PATH, O_CREAT | O_RDWR, 0666);
    close(tmp_fd);
    key_t key = ftok(SHM_KEY_PATH, SHM_KEY_ID);
    
    // 2. 创建共享内存(独占创建检查)
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    int created = 1;
    
    if (shmid == -1 && errno == EEXIST) {
        // 已存在,获取之
        shmid = shmget(key, 0, 0666);
        created = 0;
        printf("Attached to existing shm\n");
    } else if (shmid == -1) {
        perror("shmget");
        exit(1);
    } else {
        printf("Created new shm segment\n");
    }
    
    // 3. 附加
    void *shm_base = shmat(shmid, NULL, 0);
    if (shm_base == (void *)-1) {
        perror("shmat");
        exit(1);
    }
    
    struct shm_header *hdr = (struct shm_header *)shm_base;
    char *data_area = (char *)shm_base + SHM_DATA_OFFSET;
    
    // 4. 初始化(如果是创建者)
    if (created) {
        hdr->magic = MAGIC_NUMBER;
        hdr->version = 1;
        hdr->data_len = 0;
        hdr->ready = 0;
    } else {
        // 检查魔数
        if (hdr->magic != MAGIC_NUMBER) {
            fprintf(stderr, "Invalid shared memory format\n");
            exit(1);
        }
    }
    
    // 5. 生产数据
    const char *msg = "High-performance IPC via System V shared memory";
    strncpy(data_area, msg, SHM_SIZE - SHM_DATA_OFFSET);
    hdr->data_len = strlen(msg);
    
    // 内存屏障 + 设置就绪标志(简易同步)
    __sync_synchronize();  // GCC 内置内存屏障
    hdr->ready = 1;
    
    printf("Data written, waiting for consumer...\n");
    
    // 6. 等待消费者完成(实际应使用信号量)
    sleep(10);
    
    // 7. 清理策略:如果是创建者,标记删除
    // 实际销毁延迟到最后一个附加者分离
    if (created) {
        printf("Marking shm for removal\n");
        shmctl(shmid, IPC_RMID, NULL);
    }
    
    shmdt(shm_base);
    return 0;
}

consumer.c ------ 消费者:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <fcntl.h>
#include "common.h"

int main() {
    // 1. 生成相同键
    int tmp_fd = open(SHM_KEY_PATH, O_CREAT | O_RDWR, 0666);
    close(tmp_fd);
    key_t key = ftok(SHM_KEY_PATH, SHM_KEY_ID);
    
    // 2. 获取共享内存(等待创建者)
    int shmid;
    while ((shmid = shmget(key, 0, 0666)) == -1) {
        if (errno == ENOENT) {
            printf("Waiting for producer to create shm...\n");
            sleep(1);
            continue;
        }
        perror("shmget");
        exit(1);
    }
    
    // 3. 附加(只读)
    void *shm_base = shmat(shmid, NULL, SHM_RDONLY);
    if (shm_base == (void *)-1) {
        perror("shmat");
        exit(1);
    }
    
    const struct shm_header *hdr = (struct shm_header *)shm_base;
    const char *data_area = (char *)shm_base + SHM_DATA_OFFSET;
    
    // 4. 等待数据就绪(轮询,实际应使用信号量)
    while (hdr->magic != MAGIC_NUMBER || !hdr->ready) {
        usleep(100000);  // 100ms
    }
    
    // 5. 读取数据
    printf("Received (%u bytes): %.*s\n", 
           hdr->data_len, hdr->data_len, data_area);
    
    // 6. 分离 ------ 如果是最后一个附加者,且已标记 IPC_RMID,则段被销毁
    shmdt(shm_base);
    printf("Detached from shm\n");
    
    return 0;
}

5. 同步机制:共享内存的必需伴侣

共享内存本身不提供同步,必须配合以下机制:
具体实现
推荐模式:信号量嵌入共享内存
struct shared_region {
sem_t mutex; // 互斥锁
sem_t empty; // 空槽位计数
sem_t full; // 满槽位计数
// ... 数据区
};
共享内存区域
同步机制选择
System V 信号量

semget() + semop()

适合:多进程互斥/计数
POSIX 信号量

sem_t 嵌入头部

适合:线程/进程混合
文件锁

flock/fcntl

适合:简单互斥
原子变量 + 内存屏障

_sync * 内置

适合:单生产者单消费者

机制 适用场景 集成方式
System V 信号量(sem) 多进程互斥/计数 semget() + semop(),与 shm 同 key
POSIX 信号量(sem_t) 线程/进程混合 sem_t 放在共享内存头部
文件锁(flock/fcntl) 简单互斥 额外锁文件
原子变量 + 内存屏障 单生产者单消费者 __sync_* 内置函数

6.管理命令与调试

bash 复制代码
# 查看共享内存详情
$ ipcs -m -i <shmid>

# 示例输出:
# Shared memory Segment shmid=65536
# uid=1000 gid=1000 cuid=1000 cgid=1000
# mode=0666 access_perms=0666
# bytes=1048576 lpid=12345 cpid=12344 nattch=2
#                     ↑ 最后操作者  ↑ 创建者   ↑ 当前附加数

# 强制删除(即使还有附加者 ------ 危险!)
$ ipcrm -m <shmid>

# 清理所有残留 IPC 资源(谨慎使用)
$ ipcs -m | tail -n +4 | head -n -1 | awk '{print $2}' | xargs -n1 ipcrm -m

7. System V vs POSIX 共享内存

新项目
需内核持久化

复杂键管理
POSIX 共享内存
接口: shm_open/mmap
标识: 文件名 /shm-name
持久化: shm_unlink + 引用计数
特点: 类文件操作

接口更现代简洁
System V 共享内存
接口: shmget/shmat/shmdt
标识: key_t 整数键
持久化: IPC_RMID 显式删除
特点: 内核持久

适合复杂键管理
选型建议

特性 System V(shm) POSIX(shm_open)
接口风格 shmget/shmat/shmdt shm_open/mmap(类文件操作)
标识方式 key_t 整数键 文件名(/shm-name
内核持久性 显式 IPC_RMID shm_unlink + 引用计数
可移植性 更古老,广泛支持 现代标准,更简洁
灵活性 固定大小,不可变 ftruncate 动态调整

选型建议 :新项目优先考虑 POSIX 共享内存 (接口更现代),但 System V 在需要内核持久化复杂键管理的场景仍有优势。


四、选型决策树












开始
进程是否有

亲缘关系?
数据量大小?
需要跨网络?
匿名管道

pipe()

简单快速
共享内存

零拷贝高性能
Socket

网络通信基础
需要持久化标识?
需要最高性能?
匿名管道

通过 fd 传递

临时通信
System V 共享内存

内核持久 + 极致性能
FIFO

文件路径标识

足够好用
需要复杂同步原语?
POSIX 消息队列

或自定义协议


五、性能基准参考

IPC 机制性能对比(对数坐标示意) 匿名管道 FIFO System V<br>共享内存 Unix Domain<br>Socket TCP本地<br>Socket 10 9 8 7 6 5 4 3 2 1 0 相对吞吐量

机制 吞吐量(大致) 延迟 适用数据量
匿名管道 ~1 GB/s ~10μs < 64KB 消息
FIFO ~1 GB/s ~10μs < 64KB 消息
System V 共享内存 ~10 GB/s(内存速度) ~100ns 任意(MB-GB)
Socket(Unix Domain) ~2 GB/s ~5μs 通用
Socket(TCP本地) ~1 GB/s ~50μs 跨主机

六、关键要点总结

命名管道(FIFO)

FIFO

核心要点
优势
文件路径解耦进程关系
内核自动同步流控
陷阱
打开阻塞语义复杂
EOF轮询问题
原子写入限制 PIPE_BUF
SIGPIPE信号处理
最佳实践
幂等创建处理EEXIST
dummy写端保活防EOF
应用专属目录防冲突

  • 核心优势:解耦进程关系,通过文件系统路径 rendezvous
  • 关键陷阱:打开阻塞语义、EOF 轮询、原子写入限制
  • 最佳实践:幂等创建、dummy 写端保活、应用专属目录

System V 共享内存

System V SHM

核心要点
优势
零拷贝极致性能
内核持久化资源
代价
无内置同步机制
生命周期管理复杂
陷阱
ftok键生成不确定性
IPC_RMID延迟销毁
shmat返回-1非NULL
最佳实践
头部嵌入信号量
魔数校验格式
显式管理nattch计数

  • 核心优势:零拷贝,性能极致
  • 关键陷阱:同步缺失、生命周期管理复杂、键生成不确定性
  • 最佳实践:头部嵌入信号量、IPC_RMID 延迟销毁、魔数校验

参考资源

  • 手册页man 7 pipe, man 3 mkfifo, man 2 shmget, man 2 shmat
  • 内核源码ipc/shm.c, fs/pipe.c
  • 经典著作:《Unix 环境高级编程》第 15 章(System V IPC)、《Linux/Unix 系统编程手册》第 45、48 章
相关推荐
蜜獾云2 小时前
Nginx-包教包会-入门
运维·nginx
有谁看见我的剑了?2 小时前
关于linux namespace学习
linux·运维·docker
顾喵2 小时前
ARM处理器体系结构
linux·arm
特长腿特长2 小时前
systemd 服务配置文件,xxx.service 编辑指南,自定义我们自己的服务。
linux·网络·云原生
senijusene2 小时前
i.MX6ULL 平台 Linux 字符设备驱动:LED 驱动解析
linux·运维·服务器
承渊政道2 小时前
【递归、搜索与回溯算法】(递归问题拆解与经典模型实战大秘笈)
数据结构·c++·学习·算法·macos·dfs·bfs
xuhaoyu_cpp_java2 小时前
MySql学习(一)
经验分享·学习·mysql
爱上好庆祝2 小时前
clip-path裁剪,css的滤镜,动画时间线,css的变量和函数
前端·css·学习·html·css3
上海达策TECHSONIC2 小时前
汽车零配件 SAP 转型数字化标杆 上海达策实施 SAP Business One 赋能汽车底盘转向领域
大数据·运维·人工智能·汽车·运维开发·制造