Linux专题六:进程替换详解加五种进程间通讯方式(套接字放到tcp通信编程上讲述)

一、printf 缓冲区与 write 函数差异

1. 核心区别

函数 缓冲区特性 输出效果
printf() 自带缓冲区(行缓冲 / 全缓冲) 数据先存入缓冲区,满足刷新条件(\n、fflush、缓冲区满、程序结束)才会输出到屏幕,不会立即显示
write() 无缓冲区 直接将数据写入文件描述符(如 stdout 对应屏幕),数据能够立马显示,无需等待刷新
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    // printf有缓冲区,无\n/fflush时,sleep期间不会输出
    printf("printf输出(无缓冲区刷新)");
    // write无缓冲区,会立即输出
    write(1, "write输出(立即显示)\n", strlen("write输出(立即显示)\n"));
    
    sleep(3); // 阻塞3秒,观察输出顺序
    printf("\nprintf输出(换行符触发刷新)\n");
    return 0;
}
  • 运行结果:先立即显示write输出(立即显示),3 秒后才显示printf输出(无缓冲区刷新)和后续换行后的内容。

二、进程替换(exec 系列函数)

1. 核心分类与底层关系

类型 函数名 特性说明
系统调用 execve() 所有 exec 系列函数的底层实现,必须传递环境变量,参数需指定完整路径
库函数 execl()execle() 列表型参数,参数以可变参数形式传递,以NULL结尾;execle()需手动传入环境变量
库函数 execlp()execvp() p后缀,无需指定完整路径,系统会自动在PATH环境变量中查找程序路径
库函数 execv() 矢量型参数,参数以字符串数组形式传递,数组末尾需以NULL结尾

2. 关键特性

  1. 环境变量传递 :默认情况下,exec 库函数会隐式传递当前进程的环境变量;若需修改环境变量,需使用execle()(手动传入自定义环境变量数组)。
  2. 参数命名规则
    • 前 3 个替换函数(execl()/execlp()/execle())的第二个参数是替换程序的名称,可自定义命名,但第一个参数(路径 / 程序名)必须准确。
    • execvp()的参数数组中,argv[0]通常与第一个参数(程序名)一致,属于合法且常规的传递习惯,argv[0]相当于给替换后的程序起新名字。
  3. 返回值特性 :exec 函数执行成功后,原进程内存空间被完全替换,后续代码不会执行;只有替换失败时,才会返回-1并继续执行原进程后续代码。
  4. bash 底层原理 :bash 执行命令 / 程序时,会先fork()创建子进程,再通过 exec 函数将子进程替换为目标程序,替换后的程序仍为 bash 的子进程(通过ppid可验证)。

3. 常用示例(含 fork+exec 组合)

(1)execlp(无需路径,执行 ls 命令)
cpp 复制代码
#include <unistd.h>
#include <stdio.h>

int main() {
    printf("原进程PID=%d\n", getpid());
    // 替换为ls -l命令,参数以NULL结尾
    int ret = execlp("ls", "ls", "-l", NULL);
    // 替换失败才会执行以下代码
    if (ret == -1) {
        perror("execlp替换失败");
        return 1;
    }
    printf("原进程后续代码(不会执行)\n");
    return 0;
}
(2)fork+exec 组合(父进程等待子进程替换执行)
cpp 复制代码
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid > 0) {
        // 父进程:等待子进程执行完毕
        int status;
        wait(&status);
        printf("子进程替换执行完成,父进程PID=%d\n", getpid());
    } else if (pid == 0) {
        // 子进程:替换为ps -f命令
        char* myargv[] = {"ps", "-f", NULL}; // execvp参数数组
        printf("子进程PID=%d,即将替换为ps命令\n", getpid());
        execvp("ps", myargv);
        // 替换失败处理
        perror("execvp替换失败");
        exit(1);
    } else {
        perror("fork创建子进程失败");
        return 1;
    }
    return 0;
}
(3)execle(手动传入环境变量)
cpp 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 自定义环境变量
    char* myenv[] = {"MY_ENV=test", "PATH=/usr/bin", NULL};
    // 替换为echo命令,手动传入环境变量
    execle("/usr/bin/echo", "echo", "$MY_ENV", NULL, myenv);
    perror("execle替换失败");
    return 1;
}

三、信号相关(signal、kill、SIGCHLD 等)

1. 核心概念

  • 信号本质:用于通知进程发生了某个事件(软中断),程序正常运行时一般不产生信号,不当操作(如ctrl+c)或系统事件(如子进程结束)会触发信号。
  • 进程响应方式:默认处理(如终止进程)、忽略信号、自定义处理函数。
  • 关键头文件:#include <signal.h>

2. 常用信号宏定义

信号宏定义 信号编号 触发场景 默认处理方式
SIGINT 2 键盘按下ctrl+c 终止当前进程
SIGCHLD 17 子进程结束后,系统自动向父进程发送该信号 忽略该信号
SIGPIPE - 管道读端关闭后,写端继续写入数据时触发 终止当前进程
SIGKILL 9 kill -9 pid发送的终止信号 强制终止进程(不可捕获 / 忽略)

3. signal 函数(信号处理注册)

(1)函数特性
  • 函数原型:void (*signal(int sig, void (*func)(int)))(int);
  • 第一个参数:要处理的信号(如SIGINT),用于约定 "捕获到该信号时执行对应操作"。
  • 第二个参数:
    • 函数指针:返回值为void,参数只有一个int类型(接收信号编号),函数名本身即为函数指针。
    • 特殊指令:SIG_IGN(忽略信号)、SIG_DFL(默认处理信号),用于命令内核执行对应操作。
  • 核心逻辑:类似 "设置陷阱",仅约定信号与处理方式的关联,信号未触发时,不会执行处理函数;信号触发时,进程从用户态切换到内核态,执行处理函数后返回用户态继续运行(打破代码从上到下执行的常规逻辑)。
(2)使用示例
示例 1:自定义 SIGINT 信号处理函数
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

// 自定义信号处理函数:返回值void,参数int(信号编号)
void fun_sig(int sig) {
    printf("捕获到信号:sig=%d(SIGINT)\n", sig);
}

int main() {
    // 约定:捕获SIGINT信号时,调用fun_sig函数
    signal(SIGINT, fun_sig);
    
    // 死循环,等待信号触发
    while (1) {
        printf("程序运行中,按ctrl+c触发信号\n");
        sleep(1);
    }
    return 0;
}
  • 运行结果:程序循环输出信息,按下ctrl+c不会终止进程,而是触发fun_sig函数输出捕获信息,之后继续循环。
示例 2:动态修改信号处理方式(先忽略后默认)
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

void fun_sig(int sig) {
    printf("第一次捕获信号:sig=%d,后续按ctrl+c将终止程序\n", sig);
    // 将SIGINT信号恢复为默认处理方式(将来时,第二次触发才生效)
    signal(SIGINT, SIG_DFL);
}

int main() {
    // 第一步:约定捕获SIGINT,调用fun_sig
    signal(SIGINT, fun_sig);
    
    while (1) {
        printf("程序运行中,第一次按ctrl+c忽略默认终止,第二次终止\n");
        sleep(1);
    }
    return 0;
}

4. kill 函数 / 指令(发送信号)

(1)核心区别
类型 形式 说明
系统调用 kill(pid_t pid, int sig); 头文件#include <signal.h>,向指定 PID 进程发送指定信号,成功返回 0,失败返回 - 1
终端指令 kill [信号] pid kill -9 1234(发送 SIGKILL 信号强制终止 PID=1234 的进程),内置指令无需额外程序
(2)kill 系统调用示例
cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("用法:%s <pid> <signal>\n", argv[0]);
        return 1;
    }
    
    pid_t pid = atoi(argv[1]); // 转换为进程PID
    int sig = atoi(argv[2]);   // 转换为信号编号
    
    // 向指定进程发送信号
    int ret = kill(pid, sig);
    if (ret == -1) {
        perror("kill发送信号失败");
        return 1;
    }
    
    printf("成功向PID=%d的进程发送信号%d\n", pid, sig);
    return 0;
}

5. SIGCHLD 信号(子进程结束信号)

(1)核心作用
  • 子进程结束后,系统自动向父进程发送SIGCHLD信号,父进程默认忽略该信号,导致子进程成为僵死进程。
  • 通过注册SIGCHLD信号的处理函数,在函数中调用wait()/waitpid(),可自动回收子进程资源,避免僵死进程。
(2)示例:自动回收子进程
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>

// SIGCHLD信号处理函数:回收子进程资源
void recycle_child(int sig) {
    // 循环回收所有退出的子进程,避免遗漏
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        printf("子进程已结束,资源已回收\n");
    }
}

int main() {
    // 注册SIGCHLD信号处理函数
    signal(SIGCHLD, recycle_child);
    
    pid_t pid = fork();
    if (pid > 0) {
        // 父进程:持续运行
        while (1) {
            printf("父进程运行中,PID=%d\n", getpid());
            sleep(2);
        }
    } else if (pid == 0) {
        // 子进程:运行5秒后退出
        printf("子进程运行中,PID=%d,5秒后退出\n", getpid());
        sleep(5);
        exit(0);
    } else {
        perror("fork失败");
        return 1;
    }
    return 0;
}

四、目录与用户信息操作

1. 目录操作(dirent.h)

(1)核心函数
函数 功能说明
opendir() 打开目录流,返回DIR*结构体指针(失败返回 NULL),头文件#include <dirent.h>
readdir() 读取目录项,每次调用返回下一个目录项的struct dirent*指针,遍历完毕返回 NULL
closedir() 关闭目录流,释放资源
(2)示例:遍历目录内容
cpp 复制代码
#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>

int main() {
    // 打开当前目录
    DIR* dir = opendir(".");
    if (dir == NULL) {
        perror("opendir打开目录失败");
        return 1;
    }
    
    // 遍历目录项
    struct dirent* entry;
    while ((entry = readdir(dir)) != NULL) {
        printf("目录项:%s\n", entry->d_name);
    }
    
    // 关闭目录流
    closedir(dir);
    return 0;
}

2. 用户信息操作(pwd.h)

(1)核心函数
函数 功能说明
getuid() 获取启动当前进程的用户 ID(无参数,返回uid_t类型),头文件#include <pwd.h>
getpwuid() 根据用户 ID 查询用户详细信息,返回struct passwd*结构体指针(包含用户名、家目录等)
(2)示例:获取用户信息
cpp 复制代码
#include <stdio.h>
#include <pwd.h>
#include <unistd.h>

int main() {
    // 获取当前用户ID
    uid_t uid = getuid();
    printf("当前用户ID:%u\n", uid);
    
    // 获取用户详细信息
    struct passwd* pw = getpwuid(uid);
    if (pw == NULL) {
        perror("getpwuid查询用户信息失败");
        return 1;
    }
    
    printf("用户名:%s\n", pw->pw_name);
    printf("家目录:%s\n", pw->pw_dir);
    printf("登录shell:%s\n", pw->pw_shell);
    return 0;
}

3. 获取当前工作目录(unistd.h)

(1)函数说明
  • 函数:getcwd(char* buf, size_t size);

  • 功能:获取当前进程的绝对工作目录,存储到buf缓冲区中,size为缓冲区大小。

  • 示例:

    cpp 复制代码
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main() {
        char buf[1024];
        // 获取当前工作目录
        if (getcwd(buf, sizeof(buf)) == NULL) {
            perror("getcwd获取目录失败");
            return 1;
        }
        printf("当前工作目录:%s\n", buf);
        return 0;
    }

4. 文件信息获取(sys/stat.h)

(1)函数说明
  • 函数:stat(const char* pathname, struct stat* statbuf);

  • 功能:获取文件 / 目录的详细信息(大小、权限、类型等),存储到struct stat结构体中。

  • 示例:

    cpp 复制代码
    #include <stdio.h>
    #include <sys/stat.h>
    #include <unistd.h>
    
    int main() {
        struct stat st;
        // 获取当前目录下a.txt的信息
        if (stat("a.txt", &st) == -1) {
            perror("stat获取文件信息失败");
            return 1;
        }
        
        printf("文件大小:%ld 字节\n", st.st_size);
        printf("文件权限:%o\n", st.st_mode & 0777); // 屏蔽高位,显示八进制权限
        printf("文件最后访问时间:%ld\n", st.st_atime);
        return 0;
    }

    五、进程间通信(IPC)****(都是重点)

    1. 基础前置概念

    (1)文件描述符(FD)
  • 本质:操作系统为打开的文件 / 管道 /socket 等对象分配的非负整数编号,用于标识和操作这些对象,是程序操作底层资源的句柄。

(2)阻塞概念
  • 程序执行到某行代码时暂停执行,直到特定操作完成或条件满足(如wait()read()阻塞、sleep()),阻塞期间无法执行后续代码,处于 "卡住" 状态。
(3)程序异常终止
  • 若程序未运行到结尾却突然终止,大概率是触发了信号(如SIGINTSIGPIPE),信号默认处理方式为终止进程。
(4)通信方式分类
通信方式 特性说明
单工 仅单向通信(A→B,B 无法向 A 发送),如打印机(电脑→打印机)
半双工 双向通信,但不能同时发送(如管道、对讲机)
全双工 双向通信,可同时发送(如网络套接字、电话)

2. IPC 分类

常见进程间通信方式:管道(有名 / 无名)、信号量、共享内存、消息队列、套接字。

3. 管道通信(核心:半双工通信,数据存储在内存中)

(1)管道核心特性
  • 通信要求:双方进程需同时运行(时间意义上的同时),否则管道会阻塞。
  • 阻塞规则:
    1. 管道为空时,read()会阻塞,直到有数据写入或写端关闭。
    2. 管道为满时,write()会阻塞,直到有数据读取或读端关闭。
  • 通信方式:半双工(双方可互相通信,但不能同时发送数据,类似写信)。
  • 管道关闭:一方关闭管道后,另一方会感知到(如读端收到read()返回 0),并自动结束相关操作。
(2)有名管道(命名管道)
核心特性
  • 存在形式:以文件形式存在于文件系统中(无后缀名),本质是内核中的数据结构。
  • 适用范围:可用于任意两个进程间通信(无需父子关系)。
  • 创建方式:终端指令mkfifo 管道名(如mkfifo myfifo)。
  • 头文件:#include <fcntl.h>(管道本质是文件,需用文件操作函数打开 / 读写)。
示例:有名管道通信(写端 + 读端)
写端程序(fifo_write.c)
cpp 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    // 1. 创建有名管道(若已存在则无需重复创建)
    if (mkfifo("myfifo", 0644) == -1) {
        perror("mkfifo创建管道失败");
        // 若管道已存在,忽略该错误
        if (errno != EEXIST) return 1;
    }
    
    // 2. 以只写方式打开管道
    int fd = open("myfifo", O_WRONLY);
    if (fd == -1) {
        perror("open打开管道失败");
        return 1;
    }
    
    // 3. 向管道写入数据
    const char* msg = "Hello 有名管道通信!";
    write(fd, msg, strlen(msg));
    printf("写端:已写入数据:%s\n", msg);
    
    // 4. 关闭管道
    close(fd);
    return 0;
}

读端程序(fifo_read.c)

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

int main() {
    // 1. 以只读方式打开管道(需先创建管道或写端已创建)
    int fd = open("myfifo", O_RDONLY);
    if (fd == -1) {
        perror("open打开管道失败");
        return 1;
    }
    
    // 2. 从管道读取数据
    char buf[1024] = {0};
    ssize_t len = read(fd, buf, sizeof(buf) - 1);
    if (len == -1) {
        perror("read读取管道失败");
        close(fd);
        return 1;
    } else if (len == 0) {
        printf("读端:管道写端已关闭\n");
        close(fd);
        return 0;
    }
    
    printf("读端:已读取数据:%s\n", buf);
    
    // 3. 关闭管道
    close(fd);
    return 0;
}
  • 运行方式:打开两个终端,分别运行./fifo_write./fifo_read,实现跨进程通信。
(3)无名管道
核心特性
  • 存在形式:仅存在于内核中,无文件形式,是内核内部的数据结构。
  • 适用范围:仅能用于父子进程(或兄弟进程)间通信。
  • 创建方式:调用pipe()函数,无需手动创建文件。
  • 函数原型:int pipe(int fd[2]);fd[0]为读端描述符,fd[1]为写端描述符,成功返回 0,失败返回 - 1)。
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>

int main() {
    // 1. 创建无名管道
    int fd[2];
    if (pipe(fd) == -1) {
        perror("pipe创建管道失败");
        return 1;
    }
    
    // 2. fork创建父子进程
    pid_t pid = fork();
    if (pid > 0) {
        // 父进程:关闭读端,向管道写数据
        close(fd[0]);
        
        const char* msg = "Hello 无名管道通信!";
        write(fd[1], msg, strlen(msg));
        printf("父进程:已写入数据:%s\n", msg);
        
        close(fd[1]);
        wait(NULL); // 等待子进程
    } else if (pid == 0) {
        // 子进程:关闭写端,从管道读数据
        close(fd[1]);
        
        char buf[1024] = {0};
        ssize_t len = read(fd[0], buf, sizeof(buf) - 1);
        if (len > 0) {
            printf("子进程:已读取数据:%s\n", buf);
        }
        
        close(fd[0]);
        exit(0);
    } else {
        perror("fork失败");
        return 1;
    }
    return 0;
}
(4)管道异常处理
  • 写端关闭,读端继续读取:read()返回 0,标识管道已关闭。
  • 读端关闭,写端继续写入:触发SIGPIPE信号,默认终止进程。

4. 消息队列

(1)核心概念
  • 本质:存储消息的队列(数据结构),消息以结构体形式存储,支持不同类型消息的分类读取。
  • 适用范围:可用于任意两个进程间通信,数据存储在内核中。
  • 头文件:#include <sys/msg.h>
  • 查看 / 删除指令:
    • ipcs:查看所有 IPC 资源(消息队列、信号量、共享内存)。
    • ipcs -q:仅查看消息队列。
    • ipcrm -q <msgid>:删除指定标识符的消息队列。
(2)核心函数
1. msgget(创建 / 获取消息队列)
  • 函数原型:int msgget(key_t key, int msgflg);
  • 参数说明:
    • key:关键字(可自定义,如(key_t)1234),用于标识消息队列。
    • msgflg:标志位(IPC_CREAT | 0600:不存在则创建,存在则返回标识符,同时指定权限)。
  • 返回值:成功返回消息队列标识符(msgid),失败返回 - 1。
2. msgsnd(向消息队列添加消息)
  • 函数原型:int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);
  • 参数说明:
    • msqid:消息队列标识符(msgget返回值)。
    • msgp:消息结构体指针(第一个成员必须是long type(消息类型),后续成员自定义数据)。
    • msgsz:消息数据大小(不包含long type的大小)。
    • msgflg:标志位(设为 0,阻塞等待发送)。
  • 返回值:成功返回 0,失败返回 - 1。
3. msgrcv(从消息队列读取消息)
  • 函数原型:ssize_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg);
  • 参数说明:
    • msqid:消息队列标识符。
    • msgp:接收消息的结构体指针。
    • msgsz:接收消息的数据大小(不包含long type)。
    • msgtyp:消息类型筛选:
      • 0:接收队列中第一个消息(不区分类型)。
      • 0:接收类型为msgtyp的第一个消息。

      • <0:接收类型小于或等于该值绝对值的第一个消息。
    • msgflg:标志位(设为 0,阻塞等待接收)。
  • 返回值:成功返回读取的字节数,失败返回 - 1。
4. msgctl(控制消息队列,如删除)
  • 函数原型:int msgctl(int msqid, int cmd, struct msqid_ds* buf);
  • 参数说明:
    • msqid:消息队列标识符。
    • cmd:操作命令(IPC_RMID:立即删除消息队列)。
    • buf:队列属性结构体指针(删除时可设为 NULL)。
  • 返回值:成功返回 0,失败返回 - 1。
(3)示例:消息队列通信(发送端 + 接收端)
消息结构体定义(共用)
cpp 复制代码
// 消息结构体:第一个成员必须是long type
struct msg_buf {
    long msg_type; // 消息类型
    char msg_data[32]; // 消息数据
};
发送端程序(msg_send.c)
cpp 复制代码
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>

struct msg_buf {
    long msg_type;
    char msg_data[32];
};

int main() {
    // 1. 创建/获取消息队列
    int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
    if (msgid == -1) {
        perror("msgget创建消息队列失败");
        return 1;
    }
    
    // 2. 构造消息
    struct msg_buf msg;
    msg.msg_type = 1; // 消息类型设为1
    strcpy(msg.msg_data, "Hello 消息队列!");
    
    // 3. 发送消息
    if (msgsnd(msgid, &msg, sizeof(msg.msg_data), 0) == -1) {
        perror("msgsnd发送消息失败");
        return 1;
    }
    
    printf("发送端:已发送消息,类型=%ld,内容=%s\n", msg.msg_type, msg.msg_data);
    return 0;
}
接收端程序(msg_recv.c)
cpp 复制代码
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>

struct msg_buf {
    long msg_type;
    char msg_data[32];
};

int main() {
    // 1. 获取消息队列
    int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
    if (msgid == -1) {
        perror("msgget获取消息队列失败");
        return 1;
    }
    
    // 2. 接收消息(接收类型为1的消息)
    struct msg_buf msg;
    ssize_t len = msgrcv(msgid, &msg, sizeof(msg.msg_data), 1, 0);
    if (len == -1) {
        perror("msgrcv接收消息失败");
        return 1;
    }
    
    printf("接收端:已接收消息,类型=%ld,内容=%s,长度=%ld\n", msg.msg_type, msg.msg_data, len);
    
    // 3. 删除消息队列
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl删除消息队列失败");
        return 1;
    }
    printf("接收端:消息队列已删除\n");
    return 0;
}

5. 信号量(同步互斥工具,解决临界资源竞争问题)

(1)核心概念
1. 基础定义
  • 信号量本质 :特殊的非负整数变量,仅支持+1(V 操作)和-1(P 操作)两种原子操作,用于控制进程对临界资源的访问。
  • P 操作(获取资源):信号量值减 1,若减后值 < 0,进程阻塞等待;若≥0,可访问临界资源(类似公共厕所锁门)。
  • V 操作(释放资源):信号量值加 1,若加后值≤0,唤醒阻塞的进程;若 > 0,无额外操作(类似公共厕所开门)。
  • 临界资源:同一时刻仅允许一个进程访问的资源(如公共厕所、共享内存)。
  • 临界区:访问临界资源的代码段(进程中操作临界资源的部分)。
  • 默认初始值:通常为 1(实现互斥,保证同一时刻只有一个进程访问临界资源)。
2. 关键关联
  • 信号量是进程间通信方式之一:进程通过访问和修改信号量的值,判断临界资源是否可用,实现进程间的 "交流"(类似红绿灯指示过马路)。
  • 信号量集:多个信号量的集合(类似数组),每个信号量有索引(下标从 0 开始),semid为信号量集的内核内部标识符。
  • key 与 semid 的关系key是用户指定的 "外部标识"(固定值,如(key_t)1234),semid是内核动态分配的 "内部标识"(同一 key 在不同环境下 semid 可能不同)。
3. 联合体(union)说明
  • 本质:特殊数据类型,允许不同数据类型共享同一段内存空间,用于节省内存,主要为信号量控制操作传递参数。
  • 用途 :单个信号量的初始值需通过联合体存储,传递给semctl()函数完成初始化。
(2)核心函数(头文件:#include <sys/sem.h>
函数名 功能说明
semget() 创建 / 获取信号量集原型:int semget(key_t key, int nsems, int semflg);参数:- key:外部标识- nsems:信号量集中信号量的个数- semflg:标志位(`IPC_CREAT IPC_EXCL 0600:创建全新信号量集,存在则失败;IPC_CREAT 0600:不存在则创建,存在则返回现有标识)<br>返回值:成功返回semid`(信号量集标识符),失败返回 - 1
semop() 操作信号量(执行 P/V 操作)原型:int semop(int semid, struct sembuf *sops, size_t nsops);参数:- semid:信号量集标识符- sopssembuf结构体指针(描述单个信号量操作)- nsops:操作次数(sops若为数组,指定数组长度)返回值:成功返回 0,失败返回 - 1
semctl() 控制信号量(初始化 / 删除)原型:int semctl(int semid, int semnum, int cmd, ...);参数:- semid:信号量集标识符- semnum:信号量在集中的索引(下标)- cmd:操作命令(SETVAL:设置信号量值;IPC_RMID:删除信号量集)- 可选参数:联合体(初始化时传递信号量初始值)返回值:成功返回非负整数,失败返回 - 1
关键结构体:sembuf(描述单个信号量操作)
cpp 复制代码
struct sembuf {
    short sem_num; // 信号量在集中的索引(下标)
    short sem_op;  // 操作类型:-1(P操作)、1(V操作)
    short sem_flg; // 标志位(通常设为0,阻塞等待;IPC_NOWAIT:非阻塞)
};
(3)信号量函数封装(sem_tool.h + sem_tool.c,VSCode 实现)
1. 头文件(sem_tool.h)
cpp 复制代码
#ifndef SEM_TOOL_H
#define SEM_TOOL_H

#include <sys/sem.h>

// 信号量集标识符(全局变量,方便封装函数访问)
extern int semid;
// 信号量外部标识(可自定义)
#define SEM_KEY (key_t)1234

// 联合体:用于设置信号量初始值
union semun {
    int val; // 单个信号量的初始值
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};

// 信号量初始化(创建信号量集并设置初始值)
void sem_init();

// P操作(获取资源,信号量减1)
void sem_p(int sem_index);

// V操作(释放资源,信号量加1)
void sem_v(int sem_index);

// 信号量集销毁(删除信号量集)
void sem_destroy();

#endif
2. 实现文件(sem_tool.c)
cpp 复制代码
#include "sem_tool.h"
#include <stdio.h>
#include <stdlib.h>

int semid; // 全局信号量集标识符

// 信号量初始化
void sem_init() {
    // 创建包含2个信号量的信号量集(可根据需求修改nsems)
    semid = semget(SEM_KEY, 2, IPC_CREAT | IPC_EXCL | 0666);
    if (semid == -1) {
        // 若信号量集已存在,获取现有信号量集
        semid = semget(SEM_KEY, 2, 0666);
        if (semid == -1) {
            perror("semget创建/获取信号量集失败");
            exit(1);
        }
        printf("信号量集已存在,获取成功,semid=%d\n", semid);
    } else {
        printf("信号量集创建成功,semid=%d\n", semid);
        // 初始化信号量0为1(允许第一个进程访问)
        union semun sem_union0;
        sem_union0.val = 1;
        if (semctl(semid, 0, SETVAL, sem_union0) == -1) {
            perror("semctl初始化信号量0失败");
            exit(1);
        }
        // 初始化信号量1为0(阻塞进程直到有数据)
        union semun sem_union1;
        sem_union1.val = 0;
        if (semctl(semid, 1, SETVAL, sem_union1) == -1) {
            perror("semctl初始化信号量1失败");
            exit(1);
        }
        printf("信号量0(初始值1)、信号量1(初始值0)初始化成功\n");
    }
}

// P操作(根据信号量索引执行减1操作)
void sem_p(int sem_index) {
    struct sembuf sem_buf;
    sem_buf.sem_num = sem_index; // 信号量索引
    sem_buf.sem_op = -1;         // P操作:减1
    sem_buf.sem_flg = 0;         // 阻塞等待
    if (semop(semid, &sem_buf, 1) == -1) { // 第三个参数:执行1次操作
        perror("semop P操作失败");
        exit(1);
    }
    printf("信号量%d P操作执行成功\n", sem_index);
}

// V操作(根据信号量索引执行加1操作)
void sem_v(int sem_index) {
    struct sembuf sem_buf;
    sem_buf.sem_num = sem_index; // 信号量索引
    sem_buf.sem_op = 1;          // V操作:加1
    sem_buf.sem_flg = 0;         // 默认标志
    if (semop(semid, &sem_buf, 1) == -1) { // 第三个参数:执行1次操作
        perror("semop V操作失败");
        exit(1);
    }
    printf("信号量%d V操作执行成功\n", sem_index);
}

// 信号量集销毁(删除整个信号量集)
void sem_destroy() {
    if (semctl(semid, 0, IPC_RMID) == -1) { // semnum参数在删除时可忽略,设为0即可
        perror("semctl删除信号量集失败");
        exit(1);
    }
    printf("信号量集(semid=%d)已成功删除\n", semid);
}
3. 测试程序(sem_test.c)
cpp 复制代码
#include "sem_tool.h"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <stdlib.h> // rand函数头文件

// rand函数说明:随机生成0到RAND_MAX之间的非负整数,用于模拟随机业务场景
// 示例:生成随机休眠时间,模拟不同进程访问临界资源的时机差异
int get_random_sleep_time() {
    return rand() % 3 + 1; // 生成1-3秒的随机休眠时间
}

// 临界区函数(模拟访问临界资源)
void critical_area(int pid) {
    printf("进程%d:进入临界区,开始访问临界资源\n", pid);
    int sleep_time = get_random_sleep_time();
    sleep(sleep_time); // 模拟临界资源操作耗时(随机时间)
    printf("进程%d:退出临界区,释放临界资源(耗时%d秒)\n", pid, sleep_time);
}

int main() {
    // 初始化信号量集
    sem_init();
    
    // 创建子进程
    pid_t pid = fork();
    if (pid > 0) {
        // 父进程:P操作获取资源 -> 访问临界区 -> V操作释放资源
        printf("父进程(PID=%d):准备获取临界资源\n", getpid());
        sem_p(0); // 操作信号量0(初始值1)
        critical_area(getpid());
        sem_v(0); // 释放信号量0
        wait(NULL); // 等待子进程结束
        // 销毁信号量集
        sem_destroy();
    } else if (pid == 0) {
        // 子进程:P操作获取资源 -> 访问临界区 -> V操作释放资源
        printf("子进程(PID=%d):准备获取临界资源\n", getpid());
        sem_p(0); // 操作信号量0(父进程释放后才能获取)
        critical_area(getpid());
        sem_v(0); // 释放信号量0
        exit(0);
    } else {
        perror("fork创建子进程失败");
        exit(1);
    }
    return 0;
}
(4)相关指令
指令 功能说明
ipcs 查看所有 IPC 资源(信号量、共享内存、消息队列)
ipcs -s 仅查看信号量集的详细信息(包含 semid、key、信号量个数等)
ipcrm -s <semid> 删除指定 semid 的信号量集

6. 共享内存(最快的 IPC 方式,临界资源之一)

(1)核心概念
1. 本质与特性
  • 本质:多个进程映射到同一块物理内存区域,该区域在各个进程的逻辑地址空间中可见,进程可直接读写该内存(无需数据拷贝,速度最快)。
  • 存在问题:多个进程同时访问共享内存会导致数据不一致(如同时写入不同数据),需通过信号量或互斥锁实现同步互斥。
  • 通信搭配:通常与两个信号量配合使用(一个控制写入、一个控制读取),实现 "写进程写入时读进程阻塞,读进程读取时写进程阻塞"。
2. 关键说明
  • 共享内存是临界资源的一种,需通过同步机制保证访问安全。
  • 映射:将物理共享内存转换为进程的逻辑地址,通过shmat()实现;解除映射:将共享内存从进程逻辑地址空间分离,通过shmdt()实现。
3. 标志位说明
  • 本质:二进制位或一组二进制位,用于表示特定状态或条件,控制程序流程、表示操作结果或系统状态。
  • 或位运算符(|) :用于将多个标志位组合成一个新的标志位(如IPC_CREAT|0600IPC_CREAT|IPC_EXCL|0666),实现多个条件的同时生效。
  • 常用标志位
    • IPC_CREAT:若共享内存 / 信号量不存在则创建,存在则返回现有标识符。
    • IPC_EXCL:与IPC_CREAT配合使用,创建全新资源,若资源已存在则函数调用失败。
    • 0600/0666:文件权限位,指定资源的读写执行权限(与文件权限规则一致)。
4. rand 函数说明
  • 功能 :随机生成整数,默认返回0RAND_MAX(系统定义的最大随机数,通常为2147483647)之间的非负整数。
  • 头文件#include <stdlib.h>
  • 用途:在共享内存 / 信号量测试中,模拟随机的业务耗时、访问时机差异,更贴近实际应用场景。
  • 示例int random_num = rand() % 5; // 生成0-4之间的随机整数
(2)核心函数(头文件:#include <sys/shm.h>
函数名 功能说明
shmget() 创建 / 获取共享内存段原型:int shmget(key_t key, size_t size, int shmflg);参数:- key:外部标识- size:共享内存大小(字节)- shmflg:标志位(`IPC_CREAT IPC_EXCL 0600:创建全新共享内存,存在则失败)<br>返回值:成功返回shmid`(共享内存标识符),失败返回 - 1
shmat() 映射共享内存(将共享内存连接到进程逻辑地址空间)原型:void *shmat(int shmid, const void *shmaddr, int shmflg);参数:- shmid:共享内存标识符- shmaddr:指定映射地址(通常设为 NULL,由系统自动分配)- shmflg:标志位(通常设为 0,可读可写)返回值:成功返回共享内存起始逻辑地址(指针),失败返回(void*)-1
shmdt() 解除共享内存映射(将共享内存从进程逻辑地址空间分离)原型:int shmdt(const void *shmaddr);参数:shmaddrshmat()返回的共享内存起始地址返回值:成功返回 0,失败返回 - 1
shmctl() 控制共享内存(删除共享内存段等)原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数:- shmid:共享内存标识符- cmd:操作命令(IPC_RMID:删除共享内存段)- buf:共享内存属性结构体指针(删除时设为 NULL)返回值:成功返回 0,失败返回 - 1
(3)示例:共享内存 + 信号量实现进程间同步通信
1. 共享内存工具头文件(shm_tool.h)
cpp 复制代码
#ifndef SHM_TOOL_H
#define SHM_TOOL_H

#include <sys/shm.h>

#define SHM_KEY (key_t)4321 // 共享内存外部标识
#define SHM_SIZE 1024       // 共享内存大小(1KB)

// 创建/获取共享内存
int shm_create();

// 映射共享内存
void* shm_attach(int shmid);

// 解除共享内存映射
int shm_detach(void* shm_addr);

// 删除共享内存
int shm_remove(int shmid);

#endif
2. 共享内存工具实现文件(shm_tool.c)
cpp 复制代码
#include "shm_tool.h"
#include <stdio.h>
#include <stdlib.h>

// 创建/获取共享内存
int shm_create() {
    // 标志位组合:IPC_CREAT(不存在则创建)| 0666(读写权限)
    int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget创建/获取共享内存失败");
        exit(1);
    }
    printf("共享内存创建/获取成功,shmid=%d\n", shmid);
    return shmid;
}

// 映射共享内存
void* shm_attach(int shmid) {
    void* shm_addr = shmat(shmid, NULL, 0); // shmaddr=NULL由系统分配地址,shmflg=0可读可写
    if (shm_addr == (void*)-1) {
        perror("shmat映射共享内存失败");
        exit(1);
    }
    printf("共享内存映射成功,起始地址:%p\n", shm_addr);
    return shm_addr;
}

// 解除共享内存映射
int shm_detach(void* shm_addr) {
    int ret = shmdt(shm_addr);
    if (ret == -1) {
        perror("shmdt解除共享内存映射失败");
        return -1;
    }
    printf("共享内存解除映射成功\n");
    return 0;
}

// 删除共享内存
int shm_remove(int shmid) {
    int ret = shmctl(shmid, IPC_RMID, NULL); // IPC_RMID:删除共享内存段
    if (ret == -1) {
        perror("shmctl删除共享内存失败");
        return -1;
    }
    printf("共享内存(shmid=%d)已删除\n", shmid);
    return 0;
}
3. 写进程程序(shm_write.c)
cpp 复制代码
#include "sem_tool.h"
#include "shm_tool.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdlib.h> // rand函数头文件

// 生成随机字符串,模拟向共享内存写入的动态数据
void generate_random_data(char* buf, int max_len) {
    const char* chars = "0123456789abcdefghijklmnopqrstuvwxyz";
    int len = rand() % (max_len - 1) + 1; // 生成1到max_len-1的随机长度
    for (int i = 0; i < len; i++) {
        buf[i] = chars[rand() % strlen(chars)];
    }
    buf[len] = '\0'; // 字符串结束符
}

int main() {
    // 1. 初始化信号量集(2个信号量:0控制写入,1控制读取)
    sem_init();
    
    // 2. 创建/获取共享内存并映射
    int shmid = shm_create();
    void* shm_addr = shm_attach(shmid);
    
    // 3. P操作获取写入权限(信号量0)
    printf("写进程:准备写入数据到共享内存\n");
    sem_p(0);
    
    // 4. 向共享内存写入数据(临界区操作,写入随机字符串)
    char send_buf[SHM_SIZE] = {0};
    generate_random_data(send_buf, SHM_SIZE);
    strcpy((char*)shm_addr, send_buf);
    printf("写进程:已写入随机数据:%s\n", send_buf);
    
    // 5. V操作释放读取权限(信号量1,唤醒读进程)
    sem_v(1);
    
    // 6. 等待读进程读取完成(模拟业务耗时,随机休眠1-3秒)
    int sleep_time = rand() % 3 + 1;
    printf("写进程:等待读进程读取数据(休眠%d秒)\n", sleep_time);
    sleep(sleep_time);
    
    // 7. 解除共享内存映射
    shm_detach(shm_addr);
    
    // 8. 销毁信号量集(此处可由读进程删除共享内存,避免提前删除)
    sem_destroy();
    
    return 0;
}
4. 读进程程序(shm_read.c)
cpp 复制代码
#include "sem_tool.h"
#include "shm_tool.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdlib.h> // rand函数头文件

int main() {
    // 1. 初始化信号量集(与写进程使用同一key,获取现有信号量集)
    sem_init();
    
    // 2. 创建/获取共享内存并映射
    int shmid = shm_create();
    void* shm_addr = shm_attach(shmid);
    
    // 3. P操作获取读取权限(信号量1,写进程写入后唤醒)
    printf("读进程:等待读取共享内存数据\n");
    sem_p(1);
    
    // 4. 从共享内存读取数据(临界区操作)
    char buf[SHM_SIZE] = {0};
    strcpy(buf, (char*)shm_addr);
    printf("读进程:已读取数据:%s\n", buf);
    
    // 5. V操作释放写入权限(信号量0,允许写进程再次写入)
    sem_v(0);
    
    // 6. 模拟业务处理耗时(随机休眠1-2秒)
    int sleep_time = rand() % 2 + 1;
    printf("读进程:处理读取到的数据(休眠%d秒)\n", sleep_time);
    sleep(sleep_time);
    
    // 7. 解除共享内存映射
    shm_detach(shm_addr);
    
    // 8. 删除共享内存
    shm_remove(shmid);
    
    return 0;
}
(4)编译与运行说明(VSCode 环境)
1. 环境准备
  • 将所有相关文件放在同一目录下:sem_tool.hsem_tool.cshm_tool.hshm_tool.cshm_write.cshm_read.c
  • 确保 VSCode 已配置 C/C++ 编译环境(安装 GCC 编译器、C/C++ 插件)。
2. 编译命令
(5)相关指令
指令 功能说明
ipcs 查看所有 IPC 资源(包含共享内存)
ipcs -m 仅查看共享内存的详细信息(shmid、key、大小等)
ipcrm -m <shmid> 删除指定 shmid 的共享内存段

六、补充基础知识点

1. binary 目录

  • binary通常指存放可执行二进制文件的目录,系统命令(lscpmv等)存放在/bin/usr/binbin目录下。
相关推荐
开压路机2 小时前
Linux的基本指令
linux·服务器
lifewange2 小时前
linux管理服务的命令有哪些
linux·运维·服务器
大聪明-PLUS2 小时前
我们如何分析原生应用程序(C++、Windows、Linux)的内存消耗?
linux·嵌入式·arm·smarc
麒qiqi3 小时前
进程间通信(IPC):管道通信全解析
linux·运维·服务器
无奈笑天下3 小时前
银河麒麟V10虚拟机安装vmtools报错:/bin/bash解释器错误, 权限不够
linux·运维·服务器·开发语言·经验分享·bash
wdfk_prog4 小时前
[Linux]学习笔记系列 -- [fs]kernfs
linux·笔记·学习
代码游侠4 小时前
学习笔记——IO多路复用技术
linux·运维·数据库·笔记·网络协议·学习
比奇堡派星星4 小时前
Linux Hotplug 机制详解
linux·开发语言·驱动开发
m0_485614675 小时前
Linux-容器基础2
linux·运维·服务器