【Linux 封神之路】进程进阶实战:fork/vfork/exec 函数族 + 作业实现(含僵尸进程解决方案)

【Linux 封神之路】进程进阶实战:fork/vfork/exec 函数族 + 作业实现(含僵尸进程解决方案)

大家好,我是专注 Linux 技术分享的小杨。上一篇我们吃透了进程基础概念、状态和fork创建进程的核心用法。今天就进入进程进阶实战 ------ 解锁vforkwaitwaitpidexec函数族等核心 API,拆解父子进程同步、进程替换、僵尸进程回收等关键场景,最后手把手实现资料中的实战作业(定时生成日志文件 + 自动清理旧文件),帮你彻底掌握 Linux 进程编程的核心技能!

一、进程核心函数进阶:从创建到退出全流程

上一篇我们重点讲了fork,这次补充剩余的核心进程函数,覆盖 "进程创建→同步等待→进程替换→退出" 全生命周期,每个函数都附实战场景和代码示例。

1. vfork:父子进程 "串行执行" 的创建方式

vforkfork功能类似,但核心差异在于执行顺序和资源共享,适合需要子进程先完成特定任务(如初始化)的场景。

函数详解

c

运行

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

pid_t vfork(void);
  • 功能 :创建子进程,父进程会阻塞直到子进程退出或执行exec函数族(子进程优先执行);

  • 参数:无;

  • 返回值 :与fork一致(父进程返回子进程 PID,子进程返回 0,失败返回 - 1);

  • 核心差异(与 fork 对比)

    特性 fork vfork
    执行顺序 父子进程并发竞争执行 子进程先执行,父进程阻塞
    资源分配 子进程复制父进程资源(独立内存空间) 子进程与父进程共享内存空间
    适用场景 父子进程并行执行 子进程需先完成初始化 / 任务,再让父进程执行
实战代码:vfork 创建子进程(子进程先执行)

c

运行

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    printf("父进程启动,PID:%d\n", getpid());

    pid_t pid = vfork();
    if (pid == -1) {
        perror("vfork failed");
        exit(1);
    }

    // 子进程(优先执行)
    if (pid == 0) {
        printf("子进程执行,PID:%d,父进程PID:%d\n", getpid(), getppid());
        sleep(2); // 子进程执行2秒任务
        printf("子进程执行完毕,即将退出\n");
        exit(0); // 子进程必须调用exit退出,否则会导致父进程异常
    }

    // 父进程(子进程退出后才执行)
    printf("父进程恢复执行,子进程PID:%d\n", pid);
    return 0;
}
运行结果

plaintext

复制代码
父进程启动,PID:1234
子进程执行,PID:1235,父进程PID:1234
子进程执行完毕,即将退出
父进程恢复执行,子进程PID:1235
  • 关键注意 :vfork 创建的子进程必须调用exit退出,否则会继续执行父进程的代码(共享内存空间),导致程序异常。

2. wait/waitpid:回收子进程资源(避免僵尸进程)

waitwaitpid是解决僵尸进程的核心函数,负责阻塞父进程,等待子进程退出并回收其 PID 和退出状态。

(1)wait 函数:简单回收任意子进程

c

运行

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

pid_t wait(int *wstatus);
  • 功能:阻塞父进程,等待任意一个子进程退出,回收其资源;
  • 参数wstatus:存储子进程退出状态(传 NULL 表示不关注);
  • 返回值:成功返回退出子进程的 PID,失败返回 - 1;
  • 适用场景:父进程只有一个子进程,无需指定回收对象。
(2)waitpid 函数:精准回收指定子进程(推荐)

c

运行

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

pid_t waitpid(pid_t pid, int *wstatus, int options);
  • 功能 :比wait更灵活,可指定回收的子进程、是否阻塞;
  • 核心参数
    • pid:指定回收的子进程 PID(-1表示回收任意子进程,与wait一致);
    • wstatus:存储子进程退出状态;
    • options0表示阻塞等待(与wait一致),WNOHANG表示非阻塞(子进程未退出时立即返回 0);
  • 返回值:成功返回退出子进程的 PID,子进程未退出返回 0,失败返回 - 1;
  • 适用场景:父进程有多个子进程,需精准回收或非阻塞回收。
实战代码:waitpid 非阻塞回收子进程

c

运行

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        exit(1);
    }

    if (pid == 0) {
        // 子进程执行3秒任务
        printf("子进程PID:%d,开始执行任务\n", getpid());
        sleep(3);
        printf("子进程执行完毕,退出\n");
        exit(0);
    }

    // 父进程非阻塞回收子进程
    int status;
    while (1) {
        pid_t ret = waitpid(pid, &status, WNOHANG);
        if (ret == -1) {
            perror("waitpid failed");
            break;
        } else if (ret == 0) {
            // 子进程未退出,父进程可执行其他任务
            printf("父进程:子进程未退出,继续等待...\n");
            sleep(1);
        } else {
            // 子进程已回收
            printf("父进程:成功回收子进程PID:%d\n", ret);
            break;
        }
    }

    return 0;
}

3. exit/_exit:进程退出(清理资源差异)

进程退出的两个核心函数,差异在于是否清理标准 IO 缓冲区。

函数详解

c

运行

复制代码
// exit:清理缓冲区后退出(推荐)
#include <stdlib.h>
void exit(int status);

// _exit:直接退出,不清理缓冲区
#include <unistd.h>
void _exit(int status);
  • 参数status:退出状态码(0 表示正常退出,非 0 表示异常退出);

  • 核心差异

    • exit:退出前会刷新标准 IO 缓冲区(如printf未换行的内容会输出),清理进程资源;
    • _exit:直接终止进程,不刷新缓冲区,资源由内核回收;
  • 实战对比

    c

    运行

    复制代码
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main() {
        printf("使用exit退出(会刷新缓冲区)"); // 无换行符
        exit(0); // 输出完整字符串后退出
    }

    c

    运行

    复制代码
    #include <stdio.h>
    #include <unistd.h>
    
    int main() {
        printf("使用_exit退出(不刷新缓冲区)"); // 无换行符
        _exit(0); // 缓冲区内容未输出,直接退出
    }

4. exec 函数族:进程替换(启动新程序)

exec函数族的核心功能是 "替换当前进程的代码段和数据段",启动一个新程序运行,原进程的 PID 保持不变(相当于 "换魂不换壳"),常与vfork配合使用。

常用函数:execl 与 execlp

c

运行

复制代码
// execl:需指定程序完整路径
#include <unistd.h>
int execl(const char *pathname, const char *arg, ... /* (char *) NULL */);

// execlp:从环境变量PATH中查找程序(无需完整路径)
#include <unistd.h>
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
  • 核心参数
    • pathname(execl):程序完整路径(如/bin/ls);
    • file(execlp):程序名称(如ls,从 PATH 中查找);
    • arg:程序运行参数(第一个参数为程序名,后续为运行参数,结尾必须以NULL终止);
  • 返回值:成功时不会返回(程序已替换),失败返回 - 1;
  • 实战代码:execlp 启动 ls 命令

c

运行

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    printf("当前进程PID:%d,即将启动ls命令\n", getpid());

    // 用execlp启动ls命令(-l参数,结尾必须加NULL)
    int ret = execlp("ls", "ls", "-l", NULL);
    if (ret == -1) {
        perror("execlp failed");
        exit(1);
    }

    // 以下代码不会执行(进程已被ls替换)
    printf("这行代码不会输出\n");
    return 0;
}
运行结果

plaintext

复制代码
当前进程PID:1236,即将启动ls命令
总用量 16
-rwxr-xr-x 1 zcy zcy 8960 2月  2 15:30 a.out
-rw-r--r-- 1 zcy zcy  780 2月  2 15:29 test.c

二、实战作业:定时生成日志文件(进程 + 文件操作综合)

结合资料中的作业要求,实现一个 "定时生成日志 + 自动清理旧文件" 的程序,综合运用进程、文件操作、时间编程等知识点:

作业要求

  1. 创建log目录,日志文件以当前时间命名;
  2. 日志文件中记录 "当前是第 N 个日志文件";
  3. 每 3 秒生成一个日志文件,log目录仅保留最新 10 个,超过则删除最早的;
  4. 父进程持续打印当前时间,子进程负责生成日志和清理文件。

完整实现代码

c

运行

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <sys/wait.h>

#define LOG_DIR "log"
#define MAX_LOG_NUM 10  // 最多保留10个日志文件

// 函数:创建log目录
void create_log_dir() {
    struct stat st;
    if (stat(LOG_DIR, &st) == -1) {
        // 目录不存在,创建目录(权限0755)
        if (mkdir(LOG_DIR, 0755) == -1) {
            perror("mkdir log failed");
            exit(1);
        }
        printf("创建log目录成功\n");
    }
}

// 函数:获取log目录下的日志文件数量及最早的文件名称
int get_log_count_and_oldest(char *oldest_file) {
    DIR *dir = opendir(LOG_DIR);
    if (!dir) {
        perror("opendir log failed");
        exit(1);
    }

    struct dirent *entry;
    struct stat st;
    int count = 0;
    time_t oldest_time = time(NULL); // 初始化为当前时间
    char file_path[256];

    while ((entry = readdir(dir)) != NULL) {
        // 跳过.和..
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        // 拼接文件完整路径
        snprintf(file_path, sizeof(file_path), "%s/%s", LOG_DIR, entry->d_name);
        if (stat(file_path, &st) == 0) {
            // 仅统计普通文件(日志文件)
            if (S_ISREG(st.st_mode)) {
                count++;
                // 记录最早的文件
                if (st.st_ctime < oldest_time) {
                    oldest_time = st.st_ctime;
                    strcpy(oldest_file, file_path);
                }
            }
        }
    }

    closedir(dir);
    return count;
}

// 函数:生成日志文件
void generate_log_file(int log_num) {
    // 获取当前时间,格式化文件名(如log/2026-02-02_15-30-00.log)
    time_t now = time(NULL);
    struct tm *lt = localtime(&now);
    char log_name[128];
    snprintf(log_name, sizeof(log_name), "%s/%04d-%02d-%02d_%02d-%02d-%02d.log",
             LOG_DIR, lt->tm_year + 1900, lt->tm_mon + 1, lt->tm_mday,
             lt->tm_hour, lt->tm_min, lt->tm_sec);

    // 创建并写入日志
    FILE *fp = fopen(log_name, "w");
    if (!fp) {
        perror("fopen log failed");
        return;
    }
    fprintf(fp, "当前是第%d个日志文件\n", log_num);
    fclose(fp);
    printf("生成日志文件:%s\n", log_name);
}

// 函数:清理最早的日志文件
void delete_oldest_log(const char *oldest_file) {
    if (remove(oldest_file) == 0) {
        printf("删除最早日志文件:%s\n", oldest_file);
    } else {
        perror("remove oldest log failed");
    }
}

int main() {
    // 创建log目录
    create_log_dir();

    // 创建子进程:子进程生成日志,父进程打印当前时间
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        exit(1);
    }

    // 子进程:每3秒生成日志,清理旧文件
    if (pid == 0) {
        int log_num = 1;
        char oldest_file[256];
        while (1) {
            // 生成日志文件
            generate_log_file(log_num);
            log_num++;

            // 检查日志文件数量,超过10个则删除最早的
            int count = get_log_count_and_oldest(oldest_file);
            if (count > MAX_LOG_NUM) {
                delete_oldest_log(oldest_file);
            }

            // 每3秒生成一个
            sleep(3);
        }
    }

    // 父进程:持续打印当前时间
    if (pid > 0) {
        while (1) {
            time_t now = time(NULL);
            char time_str[64];
            strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&now));
            printf("父进程 - 当前时间:%s\n", time_str);
            sleep(1);
        }

        // 回收子进程(实际不会执行,父进程是死循环)
        wait(NULL);
    }

    return 0;
}

代码核心逻辑

  1. 目录创建create_log_dir函数检查log目录是否存在,不存在则创建;
  2. 日志生成generate_log_file函数以 "年 - 月 - 日_时 - 分 - 秒.log" 命名日志,写入文件序号;
  3. 文件清理get_log_count_and_oldest函数遍历log目录,统计文件数量并记录最早的文件,超过 10 个则调用delete_oldest_log删除;
  4. 进程分工:子进程循环生成日志(每 3 秒一次),父进程持续打印当前时间(每 1 秒一次),父子进程并发执行。

编译与运行

bash

运行

复制代码
# 编译
gcc log_generator.c -o log_generator
# 运行
./log_generator

运行效果

plaintext

复制代码
创建log目录成功
父进程 - 当前时间:2026-02-02 15:35:00
生成日志文件:log/2026-02-02_15-35-00.log
父进程 - 当前时间:2026-02-02 15:35:01
父进程 - 当前时间:2026-02-02 15:35:02
父进程 - 当前时间:2026-02-02 15:35:03
生成日志文件:log/2026-02-02_15-35-03.log
父进程 - 当前时间:2026-02-02 15:35:04
...
# 生成11个文件后,删除最早的
删除最早日志文件:log/2026-02-02_15-35-00.log

三、课前分享函数:strcspn 与 strstr(字符串处理高频)

资料中提到的两个字符串处理函数,是面试和开发中的高频工具,补充其用法:

1. strcspn:查找字符首次出现位置

c

运行

复制代码
#include <string.h>
size_t strcspn(const char *s1, const char *s2);
  • 功能 :计算s1中 "不包含s2中任何字符" 的最长前缀长度(即s2中字符在s1中首次出现的位置);

  • 返回值 :前缀长度(也是s2字符首次出现的索引);

  • 实战示例

    c

    运行

    复制代码
    #include <stdio.h>
    #include <string.h>
    
    int main() {
        char s1[] = "abcdefg";
        char s2[] = "d";
        size_t len = strcspn(s1, s2);
        printf("'d'在s1中首次出现的索引:%ld\n", len); // 输出3(a(0)、b(1)、c(2)、d(3))
        return 0;
    }

2. strstr:查找子串

c

运行

复制代码
#include <string.h>
char *strstr(const char *haystack, const char *needle);
  • 功能 :在haystack字符串中查找needle子串,找到返回子串首地址,未找到返回 NULL;

  • 实战示例

    c

    运行

    复制代码
    #include <stdio.h>
    #include <string.h>
    
    int main() {
        char s[] = "hello linux world";
        char sub[] = "linux";
        char *pos = strstr(s, sub);
        if (pos) {
            printf("找到子串:%s\n", pos); // 输出"linux world"
        } else {
            printf("未找到子串\n");
        }
        return 0;
    }

四、总结:进程进阶核心要点

  1. 进程创建fork(并发)和vfork(串行)按需选择,vfork子进程必须调用exit退出;
  2. 资源回收waitpidwait更灵活,非阻塞模式适合父进程执行其他任务,避免僵尸进程;
  3. 进程替换exec函数族替换进程代码,execlp无需完整路径(从 PATH 查找),结尾必须加NULL
  4. 综合实战:进程 + 文件操作 + 时间编程的组合是嵌入式 / 服务器开发的常见场景,需掌握目录遍历、文件属性获取、定时任务等技能;
  5. 字符串工具strcspnstrstr是字符串处理的高频函数,简化字符查找和子串匹配逻辑。

掌握这些进程进阶技能后,你就能应对 Linux 多任务开发、并发编程的核心场景。下一篇我们会学习进程间通信(IPC),解决父子进程 / 无关进程的数据交互问题,敬请关注!

相关推荐
fengfuyao9852 小时前
基于MATLAB/Simulink的车辆自适应巡航控制(ACC)实现
开发语言·matlab
海盗12342 小时前
WPF上位机组件开发-设备状态运行图基础版
开发语言·c#·wpf
看我干嘛!2 小时前
python第四次作业
开发语言·python
Coder_preston2 小时前
Java集合框架详解
java·开发语言
多多*2 小时前
2026年最新 测试开发工程师相关 Linux相关知识点
java·开发语言·javascript·算法·spring·java-ee·maven
mi20062 小时前
银河麒麟上tabby和electerm两款终端工具比较
linux·运维
muyan92 小时前
统信uos-server-20-1070e-arm64-20250704-1310 安装mysql-5.7.44
linux·mysql·yum·rpm·uos·统信
muyan92 小时前
浅吐槽一下统信uos linux
linux·运维·国产化·uos·统信·去ioe
LaoWaiHang2 小时前
Linux基础知识14:文件使用权限信息
linux