进程(Process)详解
进程(Process)详解
一、基本定义
- 概念
- 进程是计算机中程序的一次动态执行实例,包含程序代码、数据及运行状态,是操作系统进行资源分配和调度的基本单位。
- 与静态的“程序”不同,进程是动态实体,随程序运行而创建,终止后消失。
- 核心属性
- **唯一标识符(PID)**:每个进程分配唯一的进程ID(如Linux中的
pid_t
类型)。 - **父子关系(PPID)**:子进程由父进程创建(如通过
fork()
),继承父进程资源。
- **唯一标识符(PID)**:每个进程分配唯一的进程ID(如Linux中的
二、进程的生命周期
- 创建
- 通过系统调用(如
fork()
)创建子进程,子进程复制父进程的代码、数据及上下文。 - 示例:Linux中
fork()
返回子进程PID,父子进程并发执行。
- 通过系统调用(如
- 运行
- 进程通过CPU时间片轮转执行,状态包括运行、就绪、阻塞等。
- 终止
- 进程可通过
exit()
主动终止,或由系统强制终止(如kill
命令)。 - 终止后资源由父进程回收(
wait()
),否则成为僵尸进程
- 进程可通过
三、进程与线程的区别
文件描述符:
当我们执行open()等系统调用时,内核会创建一个新的struct file,这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将struct file维护在文件描述符表中,最后将文件描述符返回给应用程序。我们可以通过后者对文件执行它所支持的各种函数操作,而这些函数的函数指针都维护在struct file_operations数据结构中。文件描述符实质上是底层数据结构struct file的一个引用或者句柄,它为用户提供了操作底层文件的入口。
1.system函数用法:
#include <stdlib.h>
int system(const char *command);参数:command 是要执行的命令字符串,例如 "dir"(Windows)或 "ls"(Linux)
返回值:
0:命令执行成功(部分系统可能返回子进程的退出状态码)
-1:调用失败(如无法创建子进程)
其他非零值:命令执行失败或子进程异常退出
2.工作原理:
调用 system() 时,程序会暂停当前进程,创建子进程执行命令,并等待子进程结束;
命令通过操作系统的命令处理器(如 cmd.exe 或 /bin/sh)执行
3.常见用法示例:
使用system函数生成子进程:
main函数:
fork:创建子进程
一、fork():创建子进程
1.基本功能
- fork() 是 Linux/Unix 系统调用,用于创建子进程。调用后,父进程和子进程同时执行后续代码,形成两个独立的执行流
- 返回值:
- 父进程:返回子进程的 PID(正整数)
- 子进程:返回 0
失败:返回 -1(如系统资源不足)
2.工作原理
- 子进程复制父进程的代码段、数据段和堆栈段,但拥有独立的地址空间
- 父子进程共享文件描述符等资源
- 示例:
#include <stdio.h>
#include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程 PID=%d\n", getpid()); // 子进程输出 } else if (pid > 0) { printf("父进程 PID=%d,子进程 PID=%d\n", getpid(), pid); // 父进程输出 } else { perror("fork失败"); } return 0;
}
3.pid_t:进程标识符类型
- 定义:pid_t 是 C 语言中表示进程 ID 的数据类型,本质为有符号整数(如 int 或 long),用于存储进程或父进程的 PID
- 用途:在系统调用(如 fork()、getpid())中标识进程
4.getpid() 与 getppid():获取进程标识符
1.getpid()
- 返回当前进程的 PID
- 示例:
printf("当前进程 PID=%d\n", getpid()); // 输出当前进程 PID
**2.getppid()
**
- 返回当前进程的父进程 PID(PPID)36。
- 示例:
printf("父进程 PID=%d\n", getppid()); // 输出父进程 PID
综合示例:
#include <stdio.h>
#include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:PID=%d,PPID=%d\n", getpid(), getppid()); } else if (pid > 0) { printf("父进程:PID=%d,子进程 PID=%d\n", getpid(), pid); sleep(1); // 等待子进程执行完毕 } else { perror("fork失败"); } return 0;
}
父进程:PID=7234,子进程 PID=7235
子进程:PID=7235,PPID=7234
(若父进程先终止,子进程的 PPID 可能变为 1,即由 init 接管)
五、关键注意事项
1.父子进程独立性
- 修改父子进程中的变量互不影响(因地址空间独立)
2.资源回收
- 子进程终止后需由父进程调用 wait() 或 waitpid() 回收资源,否则会形成僵尸进程
3.跨平台差异
- fork() 在 Windows 中不可用,需使用其他方法(如 CreateProcess)
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>int main(int argc, char const *argv[])
{// fork之前// 打开一个文件int fd = open("io.txt",O_CREAT | O_WRONLY | O_APPEND ,0644);if (fd == -1){perror("open");exit(EXIT_FAILURE);}char buffer[1024];//缓冲区存放写出的数据pid_t pid = fork();if (pid < 0){perror("fork");exit(EXIT_FAILURE);}else if (pid == 0){// 子进程代码strcpy(buffer,"这是子进程写入的数据!\n");}else {// 父进程代码sleep(1);strcpy(buffer,"这是父进程写入的数据!\n");}// 父子进程都要执行的代码ssize_t bytes_write = write(fd,buffer,strlen(buffer));if (bytes_write == -1){perror("write");close(fd);exit(EXIT_FAILURE);}printf("写入数据成功\n");// 使用完毕之后关闭close(fd);if (pid == 0){printf("子进程写入完毕,并释放文件描述符\n");}else{printf("父进程写入完毕,并释放文件描述符\n");}return 0;
}
execve
一、函数定义与基础特性
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
功能:将当前进程的代码段、数据段和堆栈完全替换为filename指定程序的镜像,进程ID保持不变,新程序从main()函数开始执行
返回值:
-
执行成功时,无返回值(原进程的代码已被覆盖);
-
执行失败时返回-1并设置errno(如路径错误、权限不足等)
参数说明:
-
filename
要执行的文件路径,可以是绝对路径(如 /bin/ls)或相对路径(如 ./script.sh)。若文件是脚本,需在首行指定解释器(如 #!/bin/bash)。 -
argv[]
参数数组,表示传递给新程序的命令行参数。
• argv[0] 通常为程序名称(如 “ls”),但可自定义。
• 数组必须以 NULL 结尾,否则会导致未定义行为。
3.envp[]
环境变量数组,格式为 “变量名=值” 的字符串(如 “PATH=/bin”),同样以 NULL 结尾。若为 NULL,则继 承 当前进程的环境变量。
二、返回值与执行机制
- 返回值
• 成功时无返回值,原进程的代码段、数据段、堆栈等被新程序完全替换,后续代码不再执行。
• 失败返回 -1,并设置 errno 标识错误类型(需包含 <errno.h> 查看具体错误)。 - 执行流程
• 通过 fork() 创建子进程后调用 execve,是常见用法(如 fork-exec 模型)。
• 新程序加载时,内核会解析其格式(如 ELF),并初始化内存映射、堆栈、环境变量等。
三、错误类型(部分常见错误)
• EACCES:文件不可执行(权限不足或文件系统挂载为 noexec)。
• ENOENT:文件不存在。
• ENOMEM:内存不足。
• E2BIG:参数或环境变量数组过长。
• EFAULT:无效指针地址。
四、与其他 exec 函数的区别
execve 是底层系统调用,而其他 exec 函数(如 execl, execvp)是库函数,主要差异在参数传递方式与环境变量处理:
五、代码示例
#include <unistd.h>
int main() {char *argv[] = { "ls", "-l", "/etc/passwd", NULL };char *envp[] = { "PATH=/bin", NULL };execve("/bin/ls", argv, envp);// 若执行失败,以下代码才会运行perror("execve failed");return -1;
}
重要例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(int argc, char *argv[])
{if (argc < 2) {printf("参数不够,上不了二楼.\n");return 1; // 当没有传入参数时,应返回非零值表示错误}printf("我是%s %d,我跟海哥上二楼啦!\n", argv[1], getpid());return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{/*exec系列函数 父进程跳转进入一个新进程推荐使用execvechar *__path: 需要执行程序的完整路径名char *const __argv[]: 指向字符串数组的指针 需要传入多个参数(1) 需要执行的程序命令(同*__path)(2) 执行程序需要传入的参数(3) 最后一个参数必须是NULLchar *const __envp[]: 指向字符串数组的指针 需要传入多个环境变量参数(1) 环境变量参数 固定格式 key=value(2) 最后一个参数必须是NULLreturn: 成功就回不来了 下面的代码都没有意义失败返回-1int execve (const char *__path, char *const __argv[], char *const __envp[])*/char *name = "banzhang";printf("我是%s %d,我现在在一楼\n",name,getpid());// 参数没填写够也能完成跳转,错误信息会在新程序中// char *argv[] = {"/home/fangzhixiang/CProject/erlou",NULL};char *args[] = {"/home/fangzhixiang/CProject/erlou",name,NULL};// 环境变量可以不传// char *envp[] = {NULL};char *envs[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin",NULL};int re = execve(args[0],args,envs);if (re == -1){printf("你没机会上二楼\n");return -1;}return 0;
}
这两段代码通过execve系统调用联系在一起,形成一个进程替换的过程,具体过程如下:
-
第二段代码通过
execve
执行第一段代码编译后的可执行程序(路径为/home/fangzhixiang/CProject/erlou
)。 -
char *args[] = {"/home/fangzhixiang/CProject/erlou", name, NULL}; execve(args[0], args, envs);
-
args[0] 是第一段代码的可执行文件路径。
-
args[1] 是传递给第一段代码的参数 name(值为 “banzhang”)。
-
-
参数传递:
- 第二段代码通过 args 数组向第一段代码传递参数:
- argv[0]: 可执行文件路径(固定,表示程序名)。
- argv[1]: 字符串 “banzhang”(即 name 的值)。
- ◦ 第一段代码检查 argc >= 2,确保参数足够(argv[1] 必须存在)。
- 第二段代码通过 args 数组向第一段代码传递参数:
-
环境变量:
- 第二段代码通过 envs 设置 PATH 环境变量,但第一段代码未使用环境变量,因此不影响逻辑。
-
进程替换与输出
-
进程替换:
-
execve 会完全替换当前进程的代码和数据,但保留原进程的 PID。
-
◦第二段代码调用 execve 后,原进程的代码被替换为第一段代码的代码,但进程 PID 不变。
-
输出结果:
- 第二段代码先打印:
我是banzhang ,我现在在一楼
2.成功调用 execve 后,第一段代码执行并打印:我是banzhang <PID>,我跟海哥上二楼啦!- 注意:两段代码输出的 <PID> 相同,因为是同一个进程。
流程图:
第二段代码执行↓
打印 "我是banzhang <PID>,我现在在一楼"↓
调用 execve 执行第一段代码├─ 成功 → 替换进程,执行第一段代码 → 打印 "我是banzhang <PID>,我跟海哥上二楼啦!"└─ 失败 → 打印 "你没机会上二楼"
execve+fork
测试例子:可以fork和exec共同使用,实现场景老学员推荐新学员在二楼学习,自己保持不变。
创建fork_execve_test.c,写入以下内容。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>int main(int argc, char const *argv[])
{ //邀请之前char *name="老学员";printf("%s%d在一楼精进\n",name,getpid());__pid_t pid = fork();//创建子进程//邀请新学员if (pid == -1){printf("邀请新学员失败!\n");}else if (pid == 0){// 新学员在这里char *newName = "ergou";//args[]参数数组,传给新程序的参数char *args[] = {"/home/fangzhixiang/CProject/erlou",newName,NULL};char *envs[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin",NULL};int re = execve(argv[0],args,envs);if (re == -1){printf("新学员上二楼失败\n");return 1;}}else{// 老学员在这里//sleep(1);printf("老学员%d邀请完%d之后还是在一楼学习\n",getpid(),pid);}return 0;
}
waitpid
Linux中父进程除了可以启动子进程,还要负责回收子进程的状态。如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵尸进程——即程序执行完成,但是进程没有完全结束,其内核中PCB结构体(下文介绍)没有释放。在上面的例子中,父进程在子进程结束前就结束了,那么其子进程的回收工作就交给了父进程的父进程的父进程(省略若干父进程)。
本节通过系统调用waitpid在父进程中等待子进程完成并执行回收工作。
#include <sys/types.h>
#include <sys/wait.h>/** 等待子进程的终止并获取子进程的退出状态
* 功能简单 没有选择
*/
pid_t wait(int *wstatus);
/*** 功能灵活 可以设置不同的模式 可以等待特定的子进程* * pid: 等待的模式* (1) 小于-1 例如 -1 * pgid,则等待进程组ID等于pgid的所有进程终止* (2) 等于-1 会等待任何子进程终止,并返回最先终止的那个子进程的进程ID -> 儿孙都算* (3) 等于0 等待同一进程组中任何子进程终止(但不包括组领导进程) -> 只算儿子* (4) 大于0 仅等待指定进程ID的子进程终止* wstatus: 整数指针,子进程返回的状态码会保存到该int* options: 选项的值是以下常量之一或多个的按位或(OR)运算的结果;二进制对应选项,可多选:* (1) WNOHANG 如果没有子进程终止,也立即返回;用于查看子进程状态而非等待* (2) WUNTRACED 收到子进程处于收到信号停止的状态,也返回。* (3) WCONTINUED(自Linux 2.6.10起)如果通过发送SIGCONT信号恢复了一个已停止的子进程,则也返回。* return: (1) 成功等到子进程停止 返回pid* (2) 没等到并且没有设置WNOHANG 一直等* (3) 没等到设置WNOHANG 返回0* (4) 出错返回-1*/
pid_t waitpid(pid_t pid, int *wstatus, int options);/*更加全面的子进程监控和状态报告
*/
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
示例:
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{// fork之前int subprocess_status;printf("老学员在校区\n");pid_t pid = fork();if (pid < 0){perror("fork");return 1;}else if (pid == 0){// 新学员char *args[] = {"/usr/bin/ping","-c","50","www.atguigu.com",NULL};char *envs[] = {NULL};printf("新学员%d联系海哥10次\n",getpid());int exR = execve(args[0],args,envs);if (exR < 0){perror("execve");return 1;}}else {// 老学员 getpid()父进程 pid 子进程printf("老学员%d等待新学员%d联系\n",getpid(),pid);//waitpid 第二个参数需要传入int*指针,用于接收子进程状态信息waitpid(pid,&subprocess_status,0);}printf("老学员等待新学员联系完成\n");return 0;
}
进程树
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>int main(int argc, char const *argv[])
{char *name="老学员";printf("%s%d在一楼精进\n",name,getpid());__pid_t pid = fork();if (pid == -1){printf("邀请新学员失败!\n");}else if (pid == 0){// 新学员在这里char *newName = "ergou";char *argv[] = {"/home/atguigu/process_test/erlou",newName,NULL};char *envp[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin",NULL};int re = execve(argv[0],argv,envp);if (re == -1){printf("新学员上二楼失败\n");return 1;}}else{// 老学员在这里printf("老学员%d邀请完%d之后还是在一楼学习\n",getpid(),pid);// 等待新学员二楼结束 手动输入一个字母结束等待//这里用fgetc来阻塞,手动控制父进程的等待,等待用户输入一个字符来结束父进程char bye = fgetc(stdin);}return 0;
}
这里用fgetc来阻塞,手动控制父进程的等待,等待用户输入一个字符来结束父进程
匿名管道
有名管道
1.库函数
#include <sys/types.h>
#include <sys/stat.h>/*** @brief 用于创建有名管道。该函数可以创建一个路径为pathname的FIFO专用文件,mode指定了FIFO的权限,FIFO的权限和它绑定的文件是一致的。FIFO和pipe唯一的区别在于创建方式的差异。一旦创建了FIFO专用文件,任何进程都可以像操作文件一样打开FIFO,执行读写操作。* * @param pathname 有名管道绑定的文件路径* @param mode 有名管道绑定文件的权限* @return int */
int mkfifo(const char *pathname, mode_t mode);
发送端:
这一段代码主要基于linux系统的有名管道(FIFO)发送端程序。主要功能是从标准输入读取数据并通过有名管道发送给接收端。
#include <fcntl.h> //文件控制相关函数(如open mkfifo)
#include <unistd.h>//通用unix函数
#include <stdio.h>//标准I/O函数
#include <sys/stat.h>//文件状态相关函数(如mkfifo)
#include <stdlib.h>//标准库函数(如exit)
#include <string.h>//字符串操作函数(如strerror)
#include <errno.h>//错误码定义int main()
{int fd;//1.管道创建与初始化char *pipe_path = "/tmp/myfifo";//有名管道的文件路径// 创建有名管道,权限设置为 0664if (mkfifo(pipe_path, 0664) != 0){perror("mkfifo failed");//打印错误信息(如“mkfifo failed”)if (errno != 17)//管道已存在,则忽略管道已存在{exit(EXIT_FAILURE);}}// 2.打开有名管道用于写入fd = open(pipe_path, O_WRONLY);//以只写的方式打开FIFO,会阻塞直到进程以读模式打开另一端,确保(读写配对)if (fd == -1){perror("open failed");//打开失败时打印错误(如权限问题)exit(EXIT_FAILURE);}char write_buf[100];//缓冲区,用于存储从标准输入读取的数据ssize_t read_num;//记录实际读取的字节数//fd:输入源的文件描述符,使用STDIN_FILENO(值为0) 表示标准输入(如键盘//每次用户读取100字节,存入。用户输入(EOF),返回0//3.数据写入循环//数据来源://写入逻辑:while ((read_num = read(STDIN_FILENO, write_buf, 100)) > 0) {//将缓冲区的数据写入管道,管道的写入是阻塞的,若管道已满(缓冲区存满),会阻塞直到接收端读取数据write(fd, write_buf, read_num);}if (read_num < 0) { //读取标准输入时发生错误perror("read");printf("命令行数据读取异常,退出");close(fd); //关闭文件描述符exit(EXIT_FAILURE);}printf("发送管道退出,进程终止\n");close(fd); //关闭管道文件描述符return 0;
}
程序逻辑总结:
- 创建管道:确保管道存在(允许重复创建时忽略 “文件已存在” 错误)。
- 打开管道:以只写模式打开,等待接收端连接(阻塞直到有读端打开)。
- 数据传输:从标准输入读取数据,通过管道发送给接收端,直到用户输入结束(EOF)或发生错误。
- 清理资源:关闭管道,退出程序。
有名管道的特性:
-
**文件系统存在:**管道以文件形式存在,但内容存于内核缓冲区,不占用磁盘空间。/tmp/myfifo
-
阻塞机制:
写端打开时,若没有读端打开,会阻塞。O_WRONLYopen
读端关闭后,写端的writeSIGPIPE会触发信号(默认导致程序终止,此处未处理)
接收端:
1.程序的作用是作为有名管道的读端,持续读取数据并输出到标准输出。
2.需要确保有名管道已经存在(通过mkfifo创建),否则open会失败。
3.打开管道时使用O_RDONLY,可能会阻塞直到写端打开。
4.循环读取数据,处理可能的错误,但未处理EINTR。
5.正确使用write输出原始字节数据到标准输出。
6.程序结束时关闭文件描述符。
#include <fcntl.h> // 文件操作相关函数(如 open)
#include <unistd.h> // 包含 read、write、close 等系统调用
#include <stdio.h> // 标准 I/O 函数(如 printf)
#include <stdlib.h> // 包含 exit 等程序控制函数
#include <string.h> // 字符串操作函数(此处未直接使用,但头文件保留)int main() {int fd; // 文件描述符,用于操作管道char *pipe_path = "/tmp/myfifo"; // 有名管道的文件路径(需与写端一致)// 以只读模式打开有名管道用于读取//阻塞特性:若此时写端未以O_WRONLY打开管道,会阻塞(卡住不动),直到写端成功打开管道(体现有名管道的同步机制)fd = open(pipe_path, O_RDONLY);if (fd == -1) {perror("open failed");exit(EXIT_FAILURE);}char read_buff[100];//缓冲区,每次最多读取100字节数ssize_t read_num;//记录实际读取的字节数(可能小于缓冲区大小)//循环读取管道数据,直到写端关闭通道(read 返回 0)while ((read_num = read(fd, read_buff, 100)) > 0) {//read函数://返回值:// >0:成功读取字节数据(正常读取)。read_num// =0:写端关闭管道,无数据可读(正常结束条件)// <0:读取发生错误(如管道被删除,文件描述符失效)//write函数//STDOUT_FILENO是标准输出的文件符(值为1),将读取到的数据直接打印到屏幕write(STDOUT_FILENO, read_buff, read_num);//将数据输出到屏幕}//错误处理与资源释放//read_num = 0:写端正常关闭通道,读端退出循环,不视为错误//read_num < 0:发生错误(如管道 文件被删除),打印错误信息并退出if (read_num < 0) { //处理读取数据(非正常关闭)perror("read");//打印错误原因printf("管道数据读取异常,退出");exit(EXIT_FAILURE);//终止程序}printf("接收管道退出,进程终止\n");close(fd); //关闭文件描述符return 0;
}
有名管道读端核心特性:
- 阻塞机制:
- 打开时阻塞: 若写端未打开,会一直等待(直到写端调用 )。
open(O_RDONLY)``open(O_WRONLY)
- 读取时阻塞:管道为空且写端未关闭时, 会阻塞; 若写端关闭, 返回 0,不再阻塞。
read``read
- 打开时阻塞: 若写端未打开,会一直等待(直到写端调用 )。
- 数据流向:
- 有名管道是单向的(读端只能读,写端只能写),若需双向通信,需创建两个管道。
- 文件系统存在:
- 管道以文件形式存在(如 ),但内容存储在内核缓冲区,不占用磁盘空间。
/tmp/myfifo
- 管道以文件形式存在(如 ),但内容存储在内核缓冲区,不占用磁盘空间。
- 权限依赖:
- 读端需有管道的读权限(由写端创建时的权限掩码和 决定)。
umask
- 读端需有管道的读权限(由写端创建时的权限掩码和 决定)。
共享内存:
(1)shm_open()和shum_unlink()
shm_open可以开启一块内存共享对象,我们可以像使用一般文件描述符一般使用这块内存对象。
#include <sys/mman.h> /*** const char *name: 这是共享内存对象的名称,直接写一个文件名称,本身会保存在 /dev/shm 。名称必须是唯一的,以便不同进程可以定位同一个共享内存段。* 命名规则:必须是以正斜杠/开头,以\0结尾的字符串,中间可以包含若干字符,但不能有正斜杠* int oflag: 打开模式 二进制可拼接* (1) O_CREAT:如果不存在则创建新的共享内存对象* (2) O_EXCL:当与 O_CREAT 一起使用时,如果共享内存对象已经存在,则返回错误(避免覆盖现有对象)* (3) O_RDONLY:以只读方式打开* (4) O_RDWR:以读写方式打开* (5) O_TRUNC 用于截断现有对象至0长度(只有在打开模式中包含 O_RDWR 时才有效)。* mode_t mode: 当创建新共享内存对象时使用的权限位,类似于文件的权限模式,一般0644即可* return: 成功执行,它将返回一个新的描述符;发生错误,返回值为 -1
*/
int shm_open(const char *name, int oflag, mode_t mode);/*** * 删除一个先前由 shm_open() 创建的命名共享内存对象。尽管这个函数被称为“unlink”,但它并没有真正删除共享内存段本身,而是移除了与共享内存对象关联的名称,使得通过该名称无法再打开共享内存。当所有已打开该共享内存段的进程关闭它们的描述符后,系统才会真正释放共享内存资源** char *name: 要删除的共享内存对象名称* return: 成功返回0 失败返回-1*/
int shm_unlink(const char *name);
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库(含exit等)
#include <unistd.h> // Unix标准函数(含fork、sleep等)
#include <fcntl.h> // 文件控制(含O_CREAT、O_RDWR等标志)
#include <sys/mman.h> // 内存映射相关函数(mmap、munmap等)
#include <sys/wait.h> // 进程等待(wait函数)
#include <string.h> // 字符串操作(strcpy等)int main() {char *share; // 指向共享内存的指针pid_t pid; // 存储fork返回的进程IDchar shmName[100] = {0}; // 共享内存对象名称sprintf(shmName, "/letter%d", getpid()); // 生成唯一名称(包含当前进程PID)// 共享内存对象的文件标识符int fd;fd = shm_open(shmName, O_CREAT | O_RDWR, 0644);if (fd < 0){perror("共享内存对象开启失败!\n");exit(EXIT_FAILURE);}// 将该区域扩充为100字节长度//ftruncate缩放的文件描述符,可以通过shm_open()开启的内存对象,truncate必须是已存在的文件//int truncate(const char *path, off_t length);//int ftruncate(int fd, off_t length);ftruncate(fd, 100);// 以读写方式映射该区域到内存,并开启父子共享标签 偏移量选择0从头开始//share保存的是共享内存的起始地址,*share解引用,该处地址的值//MAP_SHARED 实现数据共享share = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);// 注意:不是p == NULL 映射失败返回的是((void *) -1)if (share == MAP_FAILED){perror("共享内存对象映射到内存失败!\n");exit(EXIT_FAILURE);}// 映射区建立完毕,关闭读取连接 注意不是删除close(fd);// 创建子进程pid = fork();if (pid == 0){// 子进程写入数据作为回信//同步问题:子进程无需等待父进程,直接执行写入并退出strcpy(share, "你是个好人!\n");printf("新学员%d完成回信!\n", getpid());}else{// 等待回信sleep(1);printf("老学员%d看到新学员%d回信的内容: %s", getpid(),pid,share);// 等到子进程运行结束//当传入参数status为NULL时,及调用wait(NULL),表示父进程不关心子进程的退出状态,仅等待子进程结束回收资源//回收资源,避免僵尸进程。wait(NULL);// 释放映射区//munmap函数 参数:映射地址和大小,成功返回0,失败返回-1。int ret = munmap(share, 100);if (ret == -1){perror("munmap");exit(EXIT_FAILURE);}}// 删除共享内存对象shm_unlink(shmName);//名称从系统中删除,内容立即不可访问return 0;
}
1.定义消息结构
2.生产者(发送消息)
#include <stdio.h> // 标准输入输出(用于printf/scanf等)
#include <stdlib.h> // 标准库(用于exit等)
#include <string.h> // 字符串操作(用于strcpy等)
#include <sys/msg.h> // 消息队列相关系统调用(msgget/msgsnd/msgrcv/msgctl)
#include "msg_struct.h" // 自定义头文件,包含消息结构体定义
int main() {//ftok根据给定的路径名("msgq.txt")和项目标识符('M',任意非零字符)生成一个系统范围内唯一的key。key_t key = ftok("msgq.txt", 'M'); //获取消息队列//通过ftok生成的键值,定位唯一的消息队列int msqid = msgget(key, 0666 | IPC_CREAT);if (msqid == -1) { perror("msgget failed"); exit(1); }//定义消息内容//消息结构体格式:必须以long mtype开头struct msgbuf msg;msg.mtype = 1; // 类型1//mtext:消息内容 ,此处100字节//strcpy将字符串复制到mtext中,需确保缓冲区足够大,避免溢出strcpy(msg.mtext, "Hello from producer!");//msqid 消息队列ID(由msgget返回)//&msg: 消息 结构体指针,包含类型和内容//sizeof(msg.mtext):实际数据的长度//flag 0 默认值 阻塞发送//IPC_NOWAIT 非阻塞发送if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {perror("msgsnd failed");exit(1);}printf("Message sent: %s\n", msg.mtext);return 0;
}
3.消费者(接收指定类型的消息)
流程:
- **获取队列:**通过与生产者相同的 key 获取消息队列 ID,确保操作同一队列。
- **接收指定类型消息:**使用 msgrcv 阻塞等待类型为 1 的消息,解析后打印内容。
- **清理队列:**作为最后一个消费者,调用 msgctl 标记队列删除(异步生效,不影响当前接收)。
- **错误处理:**每个系统调用后检查返回值,避免未定义行为(如队列已删除时继续操作)
#include <stdio.h> //标准输入输出
#include <stdlib.h>//标准库
#include <sys/msg.h>//消息队列调用
#include "msg_struct.h"//自定义消息队列头文件 int main() { key_t key = ftok("msgq.txt", 'M'); //仅获取已有队列 int msqid = msgget(key, 0666); if (msqid == -1) { perror("msgget failed"); exit(1); } struct msgbuf msg;//定义的接收消息 的结构体msg // 接收类型为1的消息 // &msg 接收消息的结构体指针,用于存储读取的信息 // sizeof() 接收数据的缓冲区大小 // 1 接收消息的类型:仅获取mtype == 1 的消息 // 0 阻塞标志:无符合条件的消息阻塞,直到消息到达 if (msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0) == -1) { perror("msgrcv failed"); exit(1); } printf("Received message: %s\n", msg.mtext); // 删除队列(仅在最后一个消费者使用时执行) if (msgctl(msqid, IPC_RMID, NULL) == -1) { perror("msgctl(IPC_RMID) failed"); } return 0;
}
父子进程间通信测试例程
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{//1.创建消息队列(消息队列初始化)struct mq_attr attr;//消息队列的容量attr.mq_maxmsg = 10;//单条消息最大的字节数attr.mq_msgsize = 100;attr.mq_flags = 0;//不使用异步通知attr.mq_curmsgs = 0;//当前消息数(创建时由系统自动管理,无需手动设置)//2.消息队列创建/打开char *mq_name = "/father_son_mq";//&attr 队列属性mqd_t mqdes = mq_open(mq_name,O_RDWR | O_CREAT,0664,&attr);//mqd_t是POSIX定义的专用类型,通常类似于整型if (mqdes == (mqd_t)-1){perror("mq_open");exit(EXIT_FAILURE);}// 创建父子进程pid_t pid = fork();if (pid < 0){perror("fork");exit(EXIT_FAILURE);}if (pid == 0){// 子进程 等待接收消息队列中的信息char read_buf[100];//接收缓冲区(与mq_attr.mq_msgsize一致)struct timespec time_info;//超时时间结构体,由系统定义/*struct timespec {time_t tv_sec; 秒(time_t 是长整型,通常为 longlong tv_nsec; 纳秒(范围:0 ≤ tv_nsec < 1000000*/for (size_t i = 0; i < 10; i++){// 清空接收数据的缓冲区memset(read_buf,0,100);//用0填充缓冲区(长度100字节)// 设置接收数据的等待时间clock_gettime(0,&time_info);time_info.tv_sec += 15;//设置15秒后超时// 接收消息队列的数据 打印到控制台
/* 带超时的消息接收 结构体ssize_t recv_len = mq_timedreceive(mqdes, // 消息队列描述符(由mq_open返回)read_buf, // 接收缓冲区100, // 缓冲区大小(必须≥mq_attr.mq_msgsize,否则截断数据)NULL, // 消息优先级(NULL表示忽略,不获取优先级)&time_info // 超时时间(NULL表示阻塞,直到有消息到达)
);函数作用:从消息队列中接收一条消息,若队列中无消息且未超时,则阻塞,若超时则返回-1*/if (mq_timedreceive(mqdes,read_buf,100,NULL,&time_info) == -1){perror("mq_timedreceive");}printf("子进程接收到数据:%s\n",read_buf);}}else {// 父进程 发送消息到消息队列中char send_buf[100];//发送消息的缓冲区struct timespec time_info;//超时时间结构体//循环发送消息for (size_t i = 0; i < 10; i++){// 清空处理bufmemset(send_buf,0,100);//sprintf(send_buf,"父进程的第%d次发送消息\n",(int)(i+1));// 获取当前的具体时间clock_gettime(0,&time_info);time_info.tv_sec += 5;// 发送消息if (mq_timedsend(mqdes,send_buf,strlen(send_buf),0,&time_info) == -1){perror("mq_timedsend");}printf("父进程发送一条消息,休眠1s\n");sleep(1);}}// 最终不管是父进程还是子进程都需要释放消息队列的引用close(mqdes);// 清除消息队列只需要执行一次if (pid > 0){mq_unlink(mq_name);}return 0;
}
消息队列<mqueue.h>
是在 POSIX 消息队列编程中使用的一种数据类型,下面为你详细介绍:
定义和用途
mqd_t
类型用于表示消息队列描述符。在操作系统里,消息队列是一种用于进程间通信(IPC)的机制,它允许不同的进程通过发送和接收消息来交换数据。mqd_t
就类似于文件描述符,当你打开一个消息队列时,系统会返回一个 mqd_t
类型的值,后续对这个消息队列的操作(像发送消息、接收消息、关闭消息队列等)都要通过这个描述符来进行。
头文件和相关函数
要使用 mqd_t
类型,需要包含 <mqueue.h>
头文件。下面是一些和 mqd_t
相关的常用函数:
mq_open
:
用于打开或创建一个消息队列,返回一个 mqd_t
类型的消息队列描述符
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
name
:消息队列的名称。oflag
:打开标志,如O_RDONLY
(只读)、O_WRONLY
(只写)、O_RDWR
(读写)等。mode
:当创建新的消息队列时,指定其权限。attr
:可选参数,用于指定消息队列的属性。
mq_send
:
向指定的消息队列发送消息,需要传入 mqd_t
类型的消息队列描述符。
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);
mqdes
:消息队列描述符。msg_ptr
:指向要发送的消息的指针。msg_len
:消息的长度。msg_prio
:消息的优先级。
mq_receive
:
从指定的消息队列接收消息,同样需要传入 mqd_t
类型的消息队列描述符。
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);
mqdes
:消息队列描述符。msg_ptr
:用于存储接收到的消息的缓冲区指针。msg_len
:缓冲区的长度。msg_prio
:用于存储接收到的消息的优先级。
mq_close
:
关闭一个打开的消息队列,传入 mqd_t
类型的消息队列描述符。
int mq_close(mqd_t mqdes);
mqdes
:消息队列描述符。
mq_unlink
:
从系统中删除一个消息队列,使用消息队列的名称,不过操作前要先关闭对应的 mqd_t
描述符。
隐藏过程
int mq_unlink(const char *name);
name
:消息队列的名称。
示例代码
下面是一个简单的使用 mqd_t
的示例代码,展示了如何创建、发送和接收消息队列中的消息:
#include <stdio.h>
#include <mqueue.h>
#include <string.h>
#include <stdlib.h>#define QUEUE_NAME "/test_queue"
#define MAX_MSG_SIZE 1024int main() {mqd_t mqdes;char msg[MAX_MSG_SIZE];struct mq_attr attr;// 初始化消息队列属性attr.mq_flags = 0;attr.mq_maxmsg = 10;attr.mq_msgsize = MAX_MSG_SIZE;attr.mq_curmsgs = 0;// 打开或创建消息队列mqdes = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0666, &attr);if (mqdes == (mqd_t)-1) {perror("mq_open");exit(1);}// 发送消息const char *send_msg = "Hello, message queue!";if (mq_send(mqdes, send_msg, strlen(send_msg), 0) == -1) {perror("mq_send");mq_close(mqdes);mq_unlink(QUEUE_NAME);exit(1);}// 接收消息ssize_t bytes_received = mq_receive(mqdes, msg, MAX_MSG_SIZE, NULL);if (bytes_received == -1) {perror("mq_receive");} else {msg[bytes_received] = '\0';printf("Received message: %s\n", msg);}// 关闭和删除消息队列mq_close(mqdes);mq_unlink(QUEUE_NAME);return 0;
}
mq_timedsend
函数
是 POSIX 消息队列 API 中的一个重要函数,用于向指定的消息队列发送消息,并允许设置超时时间。下面从函数原型、参数说明、返回值、使用示例以及注意事项几个方面详细介绍该函数。
函数原型
#include <mqueue.h>
#include <time.h>int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len,unsigned int msg_prio, const struct timespec *abs_timeout);
参数说明
mqdes
:消息队列描述符,是通过mq_open
函数打开或创建消息队列时返回的mqd_t
类型的值。后续对该消息队列的操作(如发送消息)都要借助这个描述符。msg_ptr
:指向要发送的消息的指针。消息内容会从该指针所指向的内存地址开始读取。msg_len
:要发送的消息的长度(以字节为单位),必须小于或等于创建消息队列时所指定的mq_msgsize
属性值。msg_prio
:消息的优先级,是一个无符号整数。优先级较高的消息会优先被接收。取值范围通常是0
到系统支持的最大优先级值。abs_timeout
:指向struct timespec
结构体的指针,用于指定发送操作的绝对超时时间。若在该时间之前消息无法发送,函数将返回错误。struct timespec
结构体定义如下:
隐藏过程
struct timespec {time_t tv_sec; /* 秒 */long tv_nsec; /* 纳秒 */
};
返回值
-
成功:返回
0
,表示消息已成功发送到消息队列。 -
失败:返回-1
以指示具体的错误原因。常见的
EAGAIN
:消息队列已满,且消息队列以非阻塞模式打开。ETIMEDOUT
:在指定的超时时间内,消息未能成功发送到消息队列。EBADF
:mqdes
不是一个有效的消息队列描述符。EMSGSIZE
:消息长度msg_len
超过了消息队列的最大消息大小。EINTR
:在操作过程中被信号中断。
信号
#include <stdio.h>
#include <stdlib.h>
#include <signal.h> //提供了信号处理相关的函数和宏定义,如signal函数和SIGINT信号常量
#include <unistd.h>// 定义信号处理函数
//函数接受一个整数参数 signum,表示接收到的信号编号。
void sigint_handler(int signum) {printf("\n收到%d信号,停止程序!\n",signum);exit(signum);
}
int main() {// 注册SIGINT信号处理函数 收到ctrl+c信号之后不执行默认的函数,而是执行新的注册函数//signal 函数用于注册信号处理函数。它接受两个参数:第一个参数是要捕获的信号编号,这里使用 SIGINT 表示 Ctrl+C 信号;第二个参数是信号处理函数的指针,即 sigint_handler。if (signal(SIGINT, sigint_handler) == SIG_ERR) {perror("注册新的信号处理函数失败\n");return 1;}// 无限循环等待信号while (1) {sleep(1);printf("你好,在吗?\n");}return 0;
}
小结:这段代码的核心逻辑是注册一个自定义的信号处理函数来处理 Ctrl+C
信号,避免程序默认终止。在程序运行期间,每隔 1 秒输出一条提示信息,直到用户按下 Ctrl+C
触发信号处理函数,程序输出提示信息并终止。
ct timespec 结构体的指针,用于指定发送操作的绝对超时时间。若在该时间之前消息无法发送,函数将返回错误。
struct timespec` 结构体定义如下: