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)
- 执行清理工作(刷新缓冲区、调用atexit函数等)
- 将status传递给操作系统,由父进程的wait或waitpid接收。
- 终止进程,它不需要返回值,因为执行完后进程就结束了
_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和其他系统资源
- 获取子进程的退出状态信息
- 彻底消除僵尸状态