一、核心逻辑的伪代码解释 1.1 主程序逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 主程序开始: 打开文件"1.txt"用于读写 如果文件打开失败: 输出错误信息并退出程序 向文件中写入"nonono"字符串 如果写入失败: 关闭文件 输出错误信息并退出程序 强制将缓冲区数据写入磁盘 分配能存储3个workerData_t结构体的内存空间 如果内存分配失败: 关闭文件 输出错误信息并退出程序 调用MakeWorker函数创建3个工作进程 如果创建失败: 释放已分配的内存 关闭文件 输出错误信息并退出程序 对于每个工作进程: 等待该进程执行结束 释放内存空间 关闭文件 正常退出程序
主程序首先尝试打开文件并写入内容,确保数据持久化。接着分配内存存储工作进程信息,调用MakeWorker函数创建子进程,最后等待所有子进程结束,释放资源并退出。
1.2 工作进程创建逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 MakeWorker函数(工作进程数量, 存储工作进程信息的数组): 如果传入的参数不合法(数组为空或进程数量小于等于0): 返回-1表示失败 循环(从0到工作进程数量-1): 调用fork( )系统调用创建新进程 获取返回的pid值 如果pid小于0: 输出fork失败的错误信息 返回-1表示失败 否则如果pid等于0(当前为子进程): 进入无限循环: 休眠1秒 退出子进程(实际不会执行到这里,仅作保险) 否则(当前为父进程): 将子进程的pid存储到信息数组中 将该子进程的状态设置为FREE 打印该子进程的索引和pid信息 返回0表示成功
MakeWorker
函数负责创建指定数量的工作进程。通过fork系统调用生成子进程,在父进程中记录子进程的 PID 和状态;子进程则进入休眠循环,模拟等待任务的状态。
二、程序功能概述 本程序主要实现以下功能:
创建文本文件并写入指定内容
批量创建多个子进程作为工作进程
记录每个工作进程的 PID 和状态信息
确保主进程等待所有子进程结束后再退出
早期服务器实现
三、完整代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <errno.h> #include <sys/wait.h> // 定义进程状态枚举 enum { FREE, // 空闲状态 BUSY // 忙碌状态 }; // 工作进程数据结构 typedef struct workerData_s { pid_t pid; // 进程ID int status; // 进程状态 } workerData_t; // 函数声明 int MakeWorker(const int workernum, workerData_t *workaddr); int main(int argc, char *argv[]) { // 打开文件并进行写操作 FILE *fp = fopen("1.txt", "w+"); if (fp == NULL) { perror("fopen failed"); return -1; } // 使用fwrite写入数据 size_t written = fwrite("nonono", 1, 6, fp); if (written != 6) { perror("fwrite failed"); fclose(fp); return -1; } // 确保数据写入磁盘 fflush(fp); // 分配工作进程数据结构内存 workerData_t *workaddr = (workerData_t*)calloc(3, sizeof(workerData_t)); if (workaddr == NULL) { perror("calloc failed"); fclose(fp); return -1; } // 创建3个工作进程 int ret = MakeWorker(3, workaddr); if (ret == -1) { fprintf(stderr, "MakeWorker failed\n"); free(workaddr); fclose(fp); return -1; } // 等待所有子进程结束,避免僵尸进程 for (int i = 0; i < 3; i++) { if (workaddr[i].pid > 0) { waitpid(workaddr[i].pid, NULL, 0); } } // 释放资源 free(workaddr); fclose(fp); return 0; } // 创建指定数量的工作进程 int MakeWorker(const int workernum, workerData_t *workaddr) { // 参数合法性检查 if (workaddr == NULL || workernum <= 0) { return -1; } for (int i = 0; i < workernum; ++i) { pid_t pid = fork( ); if (pid < 0) { // 处理fork失败的情况 perror("fork failed"); return -1; } else if (pid == 0) { // 子进程:进入循环等待 while (1) { sleep(1); } exit(EXIT_SUCCESS); // 确保子进程退出 } else { // 父进程:记录子进程信息 workaddr[i].pid = pid; workaddr[i].status = FREE; printf("i == %d pid == %d\n", i, workaddr[i].pid); } } return 0; }
四、关键技术点解析 4.1 进程创建机制 程序中使用fork( )系统调用来创建子进程,这是多进程编程的核心:
fork( )调用后会产生两个几乎完全相同的进程:
需要注意的是,fork( )函数创建的子进程是父进程的一个副本,它会复制父进程的数据段、堆栈段等,但子进程有自己独立的地址空间。在实际应用中,这种复制会带来一定的开销,因此在创建大量进程时需要谨慎考虑。
4.2 数据结构设计 程序中定义了workerData_t
结构体来管理工作进程信息:
1 2 3 4 typedef struct workerData_s { pid_t pid; // 进程ID,用于唯一标识一个进程 int status; // 进程状态,取值为FREE或BUSY } workerData_t;
通过这个结构体,父进程可以方便地记录和管理所有子进程的状态。在实际的多进程应用中,还可以根据需求扩展该结构体,比如添加进程创建时间、进程所处理的任务信息等,以便更好地对进程进行监控和管理。
4.3 资源管理 程序中对文件和内存资源进行了严格管理:
在大型程序中,资源管理尤为重要。如果资源管理不当,可能会导致程序运行缓慢、崩溃甚至系统不稳定等问题。因此,开发者需要养成良好的资源管理习惯,确保每一个分配的资源都能被正确释放。
4.4 僵尸进程预防 为了避免子进程成为僵尸进程,父进程使用waitpid( )
等待所有子进程结束:
1 waitpid(workaddr[i].pid, NULL, 0);
waitpid( )
会暂停父进程的执行,直到指定的子进程结束,从而确保子进程的资源被正确回收。僵尸进程是指已经终止但尚未被父进程回收的进程,它们会占用系统资源,如进程 ID 等。如果系统中存在大量的僵尸进程,可能会导致新的进程无法创建。除了使用waitpid( )
函数外,还可以通过信号机制来处理子进程的终止,比如注册SIGCHLD信号的处理函数,在信号处理函数中回收子进程资源。
五、程序执行流程
初始化阶段 :打开文件并写入数据,为工作进程信息分配内存空间。在这个阶段,需要对各种操作进行错误检查,确保程序能够正常启动。
进程创建阶段 :通过循环调用fork( )创建多个子进程,子进程进入休眠循环,父进程记录子进程信息。在创建子进程时,要注意处理fork( )函数可能返回的错误,避免因进程创建失败而影响整个程序的运行。
等待阶段 :父进程逐个等待所有子进程结束。这一阶段的主要目的是回收子进程资源,防止僵尸进程的产生。
清理阶段 :释放已分配的内存,关闭文件,程序正常退出。在程序退出前,确保所有资源都被正确释放,是保证程序稳定性的重要环节。
六、程序扩展与优化方向 6.1 进程池概念引入 当前程序中,工作进程的数量是固定的,在实际应用中,我们可以将其扩展为进程池。进程池是一组预先创建的进程,它们可以重复使用来处理多个任务,而不是为每个任务创建一个新进程。这样可以减少进程创建和销毁的开销,提高程序的性能。
进程池的基本工作原理是:
要实现进程池,需要在父进程和子进程之间建立通信机制,比如使用管道、消息队列等,以便父进程向子进程分配任务,子进程向父进程反馈任务处理结果。
6.2 子进程任务处理扩展 当前程序中的子进程只是进入休眠循环,并没有实际的任务处理逻辑。在实际应用中,可以根据具体需求为子进程添加任务处理功能。例如,子进程可以从文件中读取数据进行处理,或者接收网络请求并进行响应等。
以下是一个简单的子进程任务处理扩展示例,子进程从文件中读取数据并打印:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 else if (pid == 0) { // 子进程:处理任务 FILE *fp = fopen("1.txt", "r"); if (fp == NULL) { perror("fopen failed in child process"); exit(EXIT_FAILURE); } char buf[1024]; size_t nread; while ((nread = fread(buf, 1, sizeof(buf), fp)) > 0) { printf("Child process %d read: %.*s\n", getpid( ), (int)nread, buf); } fclose(fp); exit(EXIT_SUCCESS); }