前言

在 Linux 开发中,进程是资源分配最小单位,我们经常需要创建子进程、等待子进程退出、在进程中执行新程序,同时线程作为轻量级执行单元也是必学知识点。本文从waitpid开始,讲解进程回收、exec 函数族、环境变量,再过渡到 pthread 线程基础,包含函数原型、参数解析、使用场景、注意事项,适合嵌入式 / Linux C 开发入门学习。

阅读前提:已经掌握 fork 创建子进程、wait 函数基础用法。

一、waitpid 函数:回收指定子进程

1. 函数原型

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);

2. 参数说明

  1. pid:指定要回收的子进程 PID

    表格

    pid 取值含义
    >0回收 PID 等于该值的指定子进程
    0回收调用进程组内任意子进程
    -1回收任意子进程,等价于 wait ()
    <‑1回收指定进程组内的任意子进程
  2. wstatus:保存子进程退出状态的地址,传入NULL代表不关心退出状态。 可以使用宏解析 wstatus:
  • WIFEXITED(wstatus):判断子进程是否正常退出
  • WEXITSTATUS(wstatus):获取子进程exit()返回值,仅正常退出时有效
  • WIFSIGNALED(wstatus):判断子进程是否被信号杀死
  • WTERMSIG(wstatus):获取杀死子进程的信号编号,仅被信号终止时有效
  1. options:回收选项,控制阻塞 / 非阻塞行为
  • 0阻塞回收。子进程没有退出,父进程就阻塞挂起,直到子进程结束。
  • WNOHANG非阻塞回收。如果子进程还没有结束,waitpid 立刻返回0,父进程不会卡住。

3. 返回值

  • 大于 0:成功,返回被回收子进程的 PID
  • 0:使用WNOHANG时,子进程还未退出
  • -1:出错,比如没有子进程

4.wait 和 waitpid 对比

  1. wait():只能阻塞等待任意一个子进程退出;
  2. waitpid():可以指定回收某个子进程,支持非阻塞,灵活性更高。

💡核心用途:解决僵尸进程。子进程执行完毕,父进程没有回收 PCB 资源就会产生僵尸进程,waitpid可以选择阻塞或者轮询非阻塞回收子进程。

简单示例

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid = fork();
    if(pid == 0){
        //子进程
        sleep(2);
        printf("子进程%d即将退出\n",getpid());
        exit(5);
    }else if(pid >0){
        int status;
        //非阻塞轮询回收子进程
        while(waitpid(pid,&status,WNOHANG) == 0)
        {
            printf("子进程还没结束,父进程做别的工作\n");
            sleep(1);
        }
        if(WIFEXITED(status)){
            printf("子进程正常退出,退出码:%d\n",WEXITSTATUS(status));
        }
    }
    return 0;
}

二、exec 函数族:进程内部执行全新程序

1. 什么是 exec?

fork()创建子进程后,子进程复制父进程完整代码。如果想要子进程运行一个完全不一样的可执行程序,就使用exec 函数族

重点:exec 调用成功之后,原来进程的代码段、数据段全部被新程序覆盖,exec 之后的代码不会执行;只有调用失败,才会返回‑1 继续向下运行。

2. 函数原型一览

extern char **environ;

//l:参数以可变参数列表传入,必须以NULL结尾
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);

//v:参数传入字符串指针数组
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
后缀含义记忆

表格

后缀含义
llist,参数列表,可变参数,末尾必须传(char*)NULL
vvector,字符串指针数组传参
ppath,会从环境变量PATH路径下搜索程序,不用写全路径
eenvironment,可以自定义传给新程序的环境变量

注意:第一个参数是要执行程序名;第二个开始是命令行参数,第一个参数必须是程序本身名字,最后必须以 NULL 哨兵结尾。

示例:execlp 使用

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("准备调用exec执行ls\n");
    //p会去PATH中找ls,不需要写/bin/ls
    execlp("ls","ls","-l",NULL);
    //只有exec失败才会走到这里
    perror("execlp fail");
    return 0;
}

开发常用套路:fork 创建子进程,子进程调用 exec 执行外部程序,父进程负责 waitpid 回收。

三、环境变量 setenv

环境变量是进程运行时保存配置信息的变量,子进程默认继承父进程全部环境变量。

函数原型

int setenv(const char *name, const char *value, int overwrite);

参数:

  1. name:环境变量名字
  2. value:环境变量值
  3. overwrite
    • 1:如果该环境变量已经存在,覆盖旧值
    • 0:已经存在则不做修改

返回值:成功返回 0,失败返回‑1。

extern char **environ;可以直接拿到当前进程全部环境变量表。

使用示例

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    setenv("MY_VAR","hello_csdn",1);
    printf("MY_VAR = %s\n",getenv("MY_VAR"));
    return 0;
}

四、线程基础 pthread

进程:操作系统资源分配最小单元;线程:操作系统任务调度最小单元。 一个进程内部可以创建多个线程,所有线程共享进程的堆、全局变量、静态变量、代码段;每个线程拥有自己独立栈空间(默认 8MB)

  • 线程局部变量(函数内局部变量)存放在线程私有栈,线程之间互不干扰;
  • 全局 / 静态 / 堆上的数据多线程共享,会带来线程安全、竞态问题。

表格

进程 API对应线程 API
fork () 创建子进程pthread_create () 创建线程
exit () 结束进程pthread_exit () 结束线程
wait ()/waitpid () 回收子进程pthread_join () 回收线程

⚠️编译线程代码时,必须链接 pthread 库gcc demo.c -o demo -pthread

1.pthread_create 创建线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

参数解析:

  1. thread:输出参数,保存新线程 TID(线程 ID)
  2. attr:线程属性,栈大小、分离属性、优先级;填NULL使用默认属性
  3. start_routine:线程执行函数,函数格式固定:void* fun(void* arg)
  4. arg:传给线程函数的参数,无参数传NULL

返回值:成功返回 0;失败返回错误编号。

2.pthread_exit 线程退出

void pthread_exit(void *retval);
  • 在线程函数内部调用,结束当前线程;
  • retval是线程退出返回值,可以被pthread_join获取;

注意:主线程调用pthread_exit()只会结束主线程,不会终止整个进程;如果主线程 return/exit (),整个进程和所有线程全部销毁。

3.pthread_join 阻塞回收线程

int pthread_join(pthread_t thread, void **retval);
  1. 作用:阻塞等待指定线程结束,回收线程资源;
  2. thread:要回收线程 TID;
  3. retval:接收线程pthread_exit()传出返回值,不需要则填 NULL;
  4. 特点:阻塞函数,线程没执行完,调用者就卡住等待。

完整线程 Demo

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

//线程函数,固定格式
void *thread_func(void *arg)
{
    pthread_t tid = pthread_self();
    printf("子线程(TID:%lu)开始执行\n",tid);
    sleep(2);
    printf("子线程即将退出\n");
    pthread_exit((void*)100); //线程返回值
}

int main(void)
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,thread_func,NULL);
    if(ret != 0){
        perror("pthread_create");
        return -1;
    }
    void *res;
    //阻塞等待子线程结束
    pthread_join(tid,&res);
    printf("主线程:子线程退出,返回值:%ld\n",(long)res);
    return 0;
}

编译运行命令:

gcc pthread_demo.c -o pthread_demo -pthread
./pthread_demo

五、拓展作业思路(文档原题作业)

需求:创建 4 个线程

  1. 采集线程:打印采集线程 TID,每 1s 打印正在采集;
  2. 存储线程:打印存储线程 TID,每 2s 打印正在存储;
  3. 显示线程:打印显示线程 TID,每 5s 打印正在显示;
  4. 日志线程:打印日志线程 TID,每 10s 打印正在日志记录;
  5. 主线程读取终端输入字符:
    • a/A:采集线程开启 / 停止打印
    • b/B:存储线程开启 / 停止打印
    • c/C:显示线程开启 / 停止打印
    • d/D:日志线程开启 / 停止打印

实现思路提示:

  1. 设置 4 个全局 flag 变量,控制每个线程是否打印输出;
  2. 4 个线程内部循环 sleep,flag 为 1 就打印,flag 为 0 就跳过输出;
  3. 主线程循环getchar()读取键盘字符,修改对应 flag 的值;
  4. 主线程调用pthread_join等待 4 个子线程。

注意:多线程修改全局共享变量,实际项目中需要加互斥锁,避免竞态。

六、总结

  1. waitpid可以指定回收子进程,支持非阻塞 WNOHANG,用来处理僵尸进程;
  2. exec 族会覆盖当前进程地址空间,成功之后后续代码不运行;后缀 l/v/p/e 记住参数传递方式;
  3. setenv 用来修改进程环境变量,子进程默认继承父进程环境变量;
  4. 线程是调度最小单元,共享进程大部分资源,每个线程私有栈;编译必须加‑pthread
  5. pthread_create创建线程,pthread_exit退出线程,pthread_join阻塞回收线程。

本篇内容来自 Linux+C 开发学习笔记,适合嵌入式软件工程师入门,如有错误欢迎评论区指正。

更多推荐