Linux 进程
1. 进程终止
1.1 八种终止进程的情况
有8种方式可以中止进程,其中 5 种为正常终止,它们是:
- 在main()函数用return返回;
- 在任意函数中调用exit()函数;
- 在任意函数中调用_exit()或_Exit()函数;
- 最后一个线程从其启动例程(线程主函数)用return返回;
- 在最后一个线程中调用pthread_exit()返回;
异常终止有3种方式,它们是:
- 调用abort()函数中止;
- 接收到一个信号;
- 最后一个线程对取消请求做出响应。
1.2 进程终止的状态
在main()函数中,return的返回值即终止状态,如果没有return语句或调用exit(),那么该进程的终止状态是0。
在Shell中,查看进程终止的状态:echo $?
正常终止进程的3个函数( exit() 和 _Exit() 是由 ISO C 说明的,_exit() 是由 POSIX 说明的 )。
void exit(int status);
void _exit(int status);
void _Exit(int status);
status也是进程终止的状态。
如果进程被异常终止,终止状态为非0。 服务程序的调度、日志和监控
1.3 资源释放的问题
- retun 表示函数返回,会调用局部对象的析构函数,main() 函数中的 return 还会调用全局对象的析构函数。
- exit()表示终止进程,不会调用局部对象的析构函数,只调用全局对象的析构函数。
- exit()会执行清理工作,然后退出,_exit() 和 _Exit() 直接退出,不会执行任何清理工作。
例程:
#include <iostream>
#include <unistd.h>
using namespace std;
struct AA
{
string name;// 用于区分不同的对象
AA(const string & str):name(str){
}
~AA(){
cout << name << "调用了析构函数\n" ;
}
};
AA a1("对象a1"); // 全局变量
int main(int argc,char *argv[])
{
AA a2("对象a2"); // 局部变量
return 0;
}
- return 时候,两个对象都调用了析构函数
- 用exit(0)时候:只调用了全局变量的析构函数
1.4 进程的终止函数
进程可以用atexit() 函数登记终止函数(最多32个),这些函数将由exit()自动调用。
int atexit(void (*function)(void));
exit()调用终止函数的顺序与登记时相反。 进程退出前的收尾工作
#include <iostream>
#include <unistd.h>
using namespace std;
void func1(){
cout << "调用了func1()" << endl;
}
void func2(){
cout << "调用了func2()" << endl;
}
int main(int argc,char *argv[])
{
atexit(func1); // 登记第一个进程终止函数
atexit(func2); // 登记第二个进程终止函数
return 0;
// exit(0);
//_exit(1);
}
- 第一种情况:return 0
- 第二种情况:exit(1) , 没有变化
- 第三种情况:_exit(1): _exit()和_Exit()直接退出,不会执行任何清理工作
2. 调用可执行程序
Linux提供了system()函数和exec函数族,在C++程序中,可以执行其它的程序(二进制文件、操作系统命令或Shell脚本)。
2.1 system()函数
system()函数提供了一种简单的执行程序的方法,把需要执行的程序和参数用一个字符串传给system()函数就行了。
函数的声明:
int system(const char * string); // 一般调用全路径
system()函数的返回值比较麻烦。
- 如果执行的程序不存在,system()函数返回非0;
- 如果执行程序成功,并且被执行的程序终止状态是0,system()函数返回0;
- 如果执行程序成功,并且被执行的程序终止状态不是0,system()函数返回非0。
2.2 exec函数族
exec函数族提供了另一种在进程中调用程序(二进制文件或Shell脚本)的方法。
exec函数族的声明如下:
int execl(const char *path, const char *arg, ...); // 重点1
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,...,char * const envp[]);
int execv(const char *path, char *const argv[]); // 重点2
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
注意:
- 如果执行程序失败则直接返回 -1,失败原因存于 errno 中。
- 新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈。
- 如果执行成功则函数不会返回,当在主程序中成功调用exec后,被调用的程序将取代调用者程序,也就是说,exec函数之后的代码都不会被执行。
- 在实际开发中,最常用的是execl()和execv(),其它的极少使用。
3. 创建进程
3.1 Linux的0、1和2号进程
整个linux系统全部的进程是一个树形结构。
0号进程(系统进程)是所有进程的祖先,它创建了1号和2号进程。
1号进程(systemd)负责执行内核的初始化工作和进行系统配置。
2号进程(kthreadd)负责所有内核线程的调度和管理。
用pstree
命令可以查看进程树(yum -y install psmisc)
pstree-p 进程编号
3.2 进程标识
每个进程都有一个非负整数表示的唯一的进程ID。虽然是唯一的,但是进程ID可以复用。
当一个进程终止后,其进程ID就成了复用的候选者。
Linux采用延迟复用算法,让新建进程的 ID 不同于最近终止的进程所使用的 ID 。这样防止了新进程被误认为是使用了同一ID的某个已终止的进程。
pid_t getpid(void); // 获取当前进程的ID。
pid_t getppid(void); // 获取父进程的ID。
3.3 fork()函数
一个现有的进程可以调用 fork()函数 创建一个新的进程。
pid_t fork(void);
由fork()创建的新进程被称为子进程。子进是父进程的副本,父进程和子进程都从调用fork()之后的代码开始执行。
#include <iostream>
#include <unistd.h>
using namespace std;
int main(int argc,char *argv[])
{
int bh = 12;
string message = "这是一个进程";
fork();
cout << "程序执行编号:"<< bh << message << endl;
sleep(20);
cout << "程序结束" << endl;
//return 0;
}
- 执行代码,发现main里的内容,fork()后的cout都被执行了两次
- 运行的同时用命令观察进程:
ps -ef |grep demo
fork()创建了子进程,7415是父进程,7416是子进程
fork()函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是子进程的进程ID。
- 定义一个变量存储fork()的返回值,观察是否不同
#include <iostream>
#include <unistd.h>
using namespace std;
int main(int argc,char *argv[])
{
int bh = 12;
string message = "这是一个进程";
pid_t pid = fork(); // 定义一个变量存储fork()的返回值
cout << "pid = " << pid << endl;
cout << "程序执行编号:"<< bh << message << endl;
}
- 9392是父进程显示的,0是子进程显示的
- 所以我们可以利用这个fork()返回值不同的性质,让父进程和子进程执行不同的内容
例:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(int argc,char *argv[])
{
int bh = 12;
string message = "这是一个进程";
pid_t pid = fork();
if (pid > 0){
// 父进程执行这段代码
cout << "pid = " << pid <<"执行父进程代码" << endl;
}else{
// 子进程执行这段代码
cout << "pid = " << pid <<"执行子进程代码" << endl;
}
}
- 子进程获得了父进程数据空间、堆和栈的副本(注意:子进程拥有的是副本,不是和父进程共享)。
演示:
更新代码,输出 &bh 和 &message
#include <iostream>
#include <unistd.h>
using namespace std;
int main(int argc,char *argv[])
{
int bh = 12;
string message = "这是一个进程";
pid_t pid = fork();
if (pid > 0){
// 父进程执行这段代码
cout << "pid = " << pid <<"执行父进程代码" << endl;
cout << "&bh = " << &bh <<" &message = " << &message << endl;
}else{
// 子进程执行这段代码
cout << "pid = " << pid <<"执行子进程代码" << endl;
cout << "&bh = " << &bh <<" &message = " << &message << endl;
}
}
地址值相同,这里的地址值是虚拟地址,而不是物理地址,
如果让父类延迟一秒再运行(sleep(1)),在这个过程中先运行子进程,并添加修改子进程变量的代码,会发现子进程中的值被修改了,而父进程中的变量值还是最开始的初始化值
这就说明物理地址是不同的,他们是两个变量
fork()之后,父进程和子进程的执行顺序是不确定的。
3.4 fork()的两种用法
- 父进程复制自己,然后,父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用fork(),=让子进程处理些请求,而父进程则继续等待下一个连接请求。(这个例子可以参考3.3)
- 进程要执行另一个程序。这种用法在Shell中很常见,子进程从fork()返回后立即调用exec。
例:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(int argc,char *argv[])
{
pid_t pid = fork();
if (pid > 0){
// 父进程执行这段代码
while(true){
sleep(1);
cout << "pid = " << pid <<"执行父进程代码" << endl;
}
}else{
// 子进程执行这段代码
sleep(10);
cout << "pid = " << pid <<"执行子进程代码" << endl;
execl("/bin/ls", "/bin/ls","-lt","/tmp",0); // 取代子进程
cout << "子进程执行任务结束" << endl;
}
}
可以看到,最开始的子进程被 execl("/bin/ls", "/bin/ls","-lt","/tmp",0);
这行子进程打断了,这个子进程结束后也没有再回到最开始的子进程中(说法可能不太准确)
3.5 共享文件
fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一个文件偏移量。
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出可能会相互混合。
3.6 vfork()函数
vfork()函数的调用和返回值与fork()相同,但两者的语义不同。
vfork()函数用于创建一个新进程,而该新进程的目的是exec一个新程序,它不复制父进程的地址空间,因为子进程会立即调用exec,于是也就不会使用父进程的地址空间。如果子进程使用了父进程的地址空间,可能会带来未知的结果。
vfork()和fork()的另一个区别是:vfork()保证子进程先运行,在子进程调用exec或exit()之后父进程才恢复运行。
4. 僵尸进程
如果父进程比子进程先退出,子进程将被1号进程托管(这也是一种让程序在后台运行的方法)。
如果子进程比父进程先退出,而父进程没有处理子进程退出的信息,那么,子进程将成为僵尸进程。
- 僵尸进程有什么危害?
内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用CPU时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程。
例:
#include <iostream>
#include <unistd.h>
#include <fstream>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main(int argc,char *argv[])
{
pid_t pid = fork();
if (pid == 0) return 0;
while(true){
cout << "父进程运行中" << endl;
sleep(1);
}
}
这段代码就会产生一个僵尸进程
-
僵尸进程的避免:
-
- 子进程退出的时候,内核会向父进程发头SIGCHLD信号,如果父进程用 signal(SIGCHLD,SIG_IGN) 知内核,表示自己对子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构。
signal(SIGCHLD,SIG_IGN) // 在main()最前面加上这一行代码,表示忽略子进程退出的信号
-
- 父进程通过wait()/waitpid() 等函数等待子进程结束,在子进程退出之前,父进程将被阻塞待。
pid_t wait(int *stat_loc); // 最常用,下面的三个是第一个的补充和完善,但比较复杂,接收参数是子进程的编号
pid_t waitpid(pid_t pid, int *stat_loc, int options);
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
返回值是子进程的编号。
stat_loc是子进程终止的信息:
a)如果是正常终止,宏**WIFEXITED(stat_loc)**返回真,宏**WEXITSTATUS(stat_loc)**可获取终止状态;
b)如果是异常终止,宏**WTERMSIG(stat_loc)**可获取终止进程的信号。
#include <iostream>
#include <unistd.h>
#include <fstream>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main(int argc,char *argv[])
{
pid_t pid = fork();
if (pid > 0){
// 父进程的流程
int sts;
pid_t pid = wait(&sts);
cout << "已终止的子进程编号是:" << endl;
if(WIFEXITED(sts)){
cout << "子进程是正常退出的,退出的状态是:" << WEXITSTATUS(sts) << endl;
}else{
cout << "子进程是异常退出的,退出的状态是:" << WTERMSIG(sts) << endl;
}
}else{
// 子进程的流程
sleep(5);
// int *p = 0; *p = 10; // 这种空指针的写法会造成内存泄漏
exit(0);
}
}
3)如果父进程很忙,可以捕获SIGCHLD信号,在信号处理函数中调用wait()/waitpid()。
例:
#include <iostream>
#include <unistd.h>
#include <fstream>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void func(int sig){ // 子进程退出的信号处理函数
int sts;
pid_t pid = wait(&sts);
cout << "已终止的子进程编号是:" << endl;
if(WIFEXITED(sts)){
cout << "子进程是正常退出的,退出的状态是:" << WEXITSTATUS(sts) << endl;
}else{
cout << "子进程是异常退出的,退出的状态是:" << WTERMSIG(sts) << endl;
}
}
int main(int argc,char *argv[])
{
pid_t pid = fork();
if (pid > 0){
// 父进程的流程
while (true){
cout << "父进程执行中..." << endl;
sleep(1);
}
}else{
// 子进程的流程
sleep(5);
int *p = 0; *p = 10;
exit(1);
}
}
去其他窗口查看后台,只有父进程,没有出现僵尸进程,成功避免僵尸进程出现
5. 多线程与信号
在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出,如果父进程收到退出信号,则应该先向全部的子进程发送退出信号,然后自己再退出。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void FathEXIT(int sig); // 父进程的信号处理函数。
void ChldEXIT(int sig); // 子进程的信号处理函数。
int main()
{
// 忽略全部的信号,不希望被打扰。
for (int ii=1;ii<=64;ii++) signal(ii,SIG_IGN);
// 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
// 但请不要用 "kill -9 +进程号" 强行终止
signal(SIGTERM,FathEXIT); signal(SIGINT,FathEXIT); // SIGTERM 15 SIGINT 2
while (true)
{
if (fork()>0) // 父进程的流程。
{
sleep(5); continue;
}
else // 子进程的流程。
{
// 子进程需要重新设置信号。
signal(SIGTERM,ChldEXIT); // 子进程的退出函数与父进程不一样。
signal(SIGINT ,SIG_IGN); // 子进程不需要捕获SIGINT信号。
while (true)
{
cout << "子进程" << getpid() << "正在运行中。\n"; sleep(3); continue;
}
}
}
}
// 父进程的信号处理函数。
void FathEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "父进程退出,sig=" << sig << endl;
kill(0,SIGTERM); // 向全部的子进程发送15的信号,通知它们退出。
// 在这里增加释放资源的代码(全局的资源)。
exit(0);
}
// 子进程的信号处理函数。
void ChldEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "子进程" << getpid() << "退出,sig=" << sig << endl;
// 在这里增加释放资源的代码(只释放子进程的资源)。
exit(0);
}