进程的状态码

1. exit()和_exit()函数

1.1 缓冲区刷新

  • 缓冲区满了,系统强制刷新。
  • 程序正常结束:由exit()或main()函数正常返回(两者是一样的,在main()函数中,return()会替换为exit()),在普通函数return返回不会触发缓冲区的刷新。
  • 行缓存:当设置了换行符,会进行缓冲区刷新。
  • 关闭文件:先将数据刷新到文件再关闭。

1.2 exit()函数

定义

C 库函数 void exit(int status) 立即终止调用进程。任何属于该进程的打开的文件描述符都会被关闭,该进程的子进程由进程 1 继承,初始化,且会向父进程发送一个 SIGCHLD 信号,父进程会默认忽略该信号,导致子进程变为僵尸进程。

status为0,表示正常退出,status>0,表示异常退出。

复制代码
int main(int argc,char* argv[],char* env[])
{
    pid_t id=fork();
    if(id==0)
    {
        cout<<"我是子进程,pid = "<<getpid()<<" ppid() = "<<getppid();
        exit(0);
    }
    cout<<"我是父进程,pid = "<<getpid()<<" ppid() = "<<getppid();
    return 0;
}

现象:子进程通过exit()函数刷新缓冲区,父进程通过return函数刷新缓冲区。

1.3 _exit()函数

定义

_exit(int status)是一个系统调用,它直接请求内核立即终止当前进程。

复制代码
int main(int argc,char* argv[],char* env[])
{
    pid_t id=fork();
    if(id==0)
    {
        cout<<"我是子进程,pid = "<<getpid()<<" ppid() = "<<getppid();
        _exit(0);
    }
    cout<<"我是父进程,pid = "<<getpid()<<" ppid() = "<<getppid();
    return 0;
}

现象:我们没有换行符(没有行缓存),再进程结束时,父进程(main()函数内)通过return(exit())成功刷新了缓存区,但子进程的数据没有被刷新。

1.4 exit和_exit区别

void exit(int status)

  1. 执行清理工作(刷新缓冲区、调用atexit函数等)
  2. 将status传递给操作系统,由父进程的wait或waitpid接收。
  3. 终止进程,它不需要返回值,因为执行完后进程就结束了

_exit:立即终止一个进程,直接进入内核,不做任何清理工作。

1.5 main函数中的返回值

|-------|-----------|----------|
| 特性 | return 1; | exit(1); |
| 程序终止 | 是 | 是 |
| 返回状态码 | 1 | 1 |
| 清理工作 | 执行 | 执行 |
| 效果 | 相同 | 相同 |

技术细节:在 main 函数中,return 语句实际上会被编译器转换为对 exit 的调用,所以最终效果是一样的。

1.6 _exit()使用场景

最主要的场景是用fork()创建子进程

为什么在子进程里用 _exit

  • 避免缓冲区的重复刷新:在 fork() 之后,子进程会继承父进程的所有I/O缓冲区。如果子进程调用了_exit(),它会刷新并关闭这些缓冲区。当父进程后来也调用 exit() 时,它会尝试再次刷新并关闭同一个缓冲区,这可能会导致数据混乱或程序崩溃。
  • 避免调用父进程的退出处理函数:子进程也会继承父进程通过 atexit() 注册的函数。如果子进程调用了 exit(),它会执行这些原本为父进程准备的清理函数,这很可能是不正确甚至危险的。 因此,在子进程中,通常直接使用 _exit 来立即退出,避免干扰父进程的状态。

当子进程使用exit()退出

结果可能和我们预期的一样,但当父进程尝试关闭一个已经被子进程关闭了的文件描述符时,行为是未定义的。在某程序崩溃。输出混乱(同一段数据被输出两次)。没有任何问题(取决于操作系统如何管理文件描述符),但这种不确定性是致命的。

2. 退出状态码

2.1 定义

本身是一个数字,不同数值有对应的状态,用于在父进程回收子进程是告诉父进程执行的结果。

2.2 查看状态码

  • 使用echo $?:查看最近某个进程的退出状态码。

    int main(int argc,char* argv[],char* env[])
    {
    pid_t id=fork();
    if(id==0)
    {
    cout<<"我是子进程,pid = "<<getpid()<<" ppid() = "<<getppid();
    _exit(2);
    }
    cout<<"我是父进程,pid = "<<getpid()<<" ppid() = "<<getppid();
    return 3;
    }

  • 使用strerror查看所有退出状态码对应的问题。

    int main(int argc,char* argv[],char* env[])
    {
    int i=0;
    while(i<200)
    {
    cout<<i<<"对应的问题是: "<<strerror(i++)<<endl;
    }
    return 0;
    }

可以看到一共有134个退出状态码,1--->133代表了异常退出状态。

3. wait和waitpid函数

父进程需要获取子进程的退出状态需要调用wait或waitpid函数。

3.1 函数参数

wait和waitpid,都有一个status参数 ,该参数是一个输出型参数 ,由操作系统填充。 如果传递NULL,表示不关心子进程的退出状态信息。 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程 。 status不能简单的当作整形来看待,可以当作位图来看待。

3.2 wait函数

直接获取子进程的退出状态码

复制代码
int main(int argc,char* argv[],char* env[])
{
    pid_t id=fork();
    if(id==0)
    {
        sleep(3);
        cout<<"我是子进程: "<<getpid()<<endl;
        _exit(0);
    }
    int* status;
    wait(status);
    cout<<"我是父进程: "<<getpid()<<endl;
}

子进程先等待3秒,父进程直接执行,但子进程先打印,说明wait会阻塞式等待子进程。

3.3 waitpid函数

指定等待子进程的pid,输出型参数statue获取退出状态码,options指定等待的类型。

pid: 指定要等待的子进程。

  • pid > 0: 等待指定 PID 的子进程。
  • pid == 0: 等待当前进程组中的任意子进程。
  • pid == -1: 等待任意子进程(等同于 wait)。
  • pid < -1: 等待指定进程组 ID 等于pid绝对值的任意子进程。

options: 控制函数行为的选项。

  • options=0:堵塞状态,要等到waitpid调用成功并且子函数退出,父进程开始进行
  • WNOHANG(wait no hang):非阻塞模式,如果没有子进程状态变化,立即返回。
  • WUNTRACED:返回因信号暂停的子进程状态。
  • WCONTINUED:返回因信号恢复的子进程状态。

返回值

  • ret>0:wait成功,子进程退出
  • ret==0 wait成功, 子进程没有退出(父进程不等待,直接返回)
  • ret==-1 waitpid函数调用失败

代码测试

我们使用轮询的方式,当子进程没有退出的时候,父进程可以先解决其他的事情。

复制代码
int main(int argc, char *argv[], char *env[])
{
    pid_t id = fork();
    if (id == 0)
    {
        sleep(3);
        cout << "我是子进程: " << getpid() << endl;
        _exit(0);
    }
    int *status;
    while (1)
    {
        int ret=waitpid(id, status, WNOHANG);
        if(ret>0)
        {
            cout<<"等待成功,子进程结束"<<endl;
            break;
        }
        else if(ret==0)
        {
            cout<<"等待成功,子进程没有结束,做点其他事"<<endl;
        }
        else
        {
            cout<<"等待失败"<<endl;
            break;
        }
        sleep(1);
    }
}

4. status解析

status一共有16位,高8位是退出码,低7位是终止信号(当程序被kill时设置),当终止信号不为0时,退出码才有效。

代码获取status

复制代码
int main(int argc, char *argv[], char *env[])
{
    pid_t id=fork();
    if(id==0)
    {
        _exit(3);
    }
    int status=0;
    waitpid(id,&status,0);
    cout<<"status: "<<status<<"  exit_code: "<<(status>>8)<<"  sign_code: "<<(status&0x7f)<<endl;
}

运行异常,返回sign_code,exit_code=0,此时退出码没有意义,初始化为0。

代码实现除0错误

复制代码
int main(int argc, char *argv[], char *env[])
{
    pid_t id=fork();
    if(id==0)
    {
        int b=0;
        cout<<2/b<<endl;  //-------------->子进程终止
        _exit(3);
    }
    int status=0;
    waitpid(id,&status,0);
    cout<<"status: "<<status<<"  exit_code: "<<(status>>8)<<"  sign_code: "<<(status&0x7f)<<endl;
}

SIGFPE: Signal Float-point Exception 浮点异常信号。

可以看到,设置了sign_code = 8,exit_code默认为0。

5. 进程等待

5.1 内核组织结构体

当子进程结束运行会进入僵尸状态,并将 exit_code和sign_code保存在PCB中,父进程等待的过程就是将子进程中的exit_code和sign_code 保存在自己的status中。

5.2 为什么需要进程等待

父进程结束后,僵尸进程会被系统自动清理,但如果父进程需要长期运行,为了避免内存泄漏,就需要进程等待除去僵尸进程。

  • 从进程表中移除僵尸进程条目
  • 释放进程ID和其他系统资源
  • 获取子进程的退出状态信息
  • 彻底消除僵尸状态
相关推荐
打小就很皮...1 小时前
基于 Python + LangChain + RAG 的知识检索系统实战
前端·langchain·embedding·rag
顾温1 小时前
default——C#/C++
java·c++·c#
BJ-Giser1 小时前
Cesium 烟雾粒子特效
前端·可视化·cesium
空中海1 小时前
02 ArkTS 语言与工程规范
java·前端·spring
楚国的小隐士1 小时前
在AI时代,如何从0接手一个项目?
java·ai·大模型·编程·ai编程·自闭症·自闭症谱系障碍·神经多样性
YJlio1 小时前
7.4.5 Windows 11 企业网络连接与网络重置实战:远程访问、本地策略与故障恢复
前端·chrome·windows·python·edge·机器人·django
散峰而望1 小时前
【算法竞赛】C/C++ 的输入输出你真的玩会了吗?
c语言·开发语言·数据结构·c++·算法·github
躺不平的理查德1 小时前
时间复杂度与空间复杂度备忘录
数据结构·算法
yaki_ya1 小时前
yaki-C语言:从概念基础到内存解析---数组(array)完全指南
java·c语言·算法