Linux 进程(终止、创建、僵尸进程、多进程与信号)

1. 进程终止

1.1 八种终止进程的情况

有8种方式可以中止进程,其中 5 种为正常终止,它们是:

  1. 在main()函数用return返回;
  2. 在任意函数中调用exit()函数;
  3. 在任意函数中调用_exit()或_Exit()函数;
  4. 最后一个线程从其启动例程(线程主函数)用return返回;
  5. 在最后一个线程中调用pthread_exit()返回;

异常终止有3种方式,它们是:

  1. 调用abort()函数中止;
  2. 接收到一个信号;
  3. 最后一个线程对取消请求做出响应。

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 资源释放的问题

  1. retun 表示函数返回,会调用局部对象的析构函数,main() 函数中的 return 还会调用全局对象的析构函数。
  2. exit()表示终止进程不会调用局部对象的析构函数,只调用全局对象的析构函数。
  3. 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()函数的返回值比较麻烦。

  1. 如果执行的程序不存在,system()函数返回非0;
  2. 如果执行程序成功,并且被执行的程序终止状态是0,system()函数返回0;
  3. 如果执行程序成功,并且被执行的程序终止状态不是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. 如果执行程序失败则直接返回 -1,失败原因存于 errno 中。
  2. 新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈。
  3. 如果执行成功则函数不会返回,当在主程序中成功调用exec后,被调用的程序将取代调用者程序,也就是说,exec函数之后的代码都不会被执行。
  4. 在实际开发中,最常用的是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()的两种用法

  1. 父进程复制自己,然后,父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用fork(),=让子进程处理些请求,而父进程则继续等待下一个连接请求。(这个例子可以参考3.3)
  2. 进程要执行另一个程序。这种用法在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);
  }
    
}

这段代码就会产生一个僵尸进程

  • 僵尸进程的避免:

    1. 子进程退出的时候,内核会向父进程发头SIGCHLD信号,如果父进程用 signal(SIGCHLD,SIG_IGN) 知内核,表示自己对子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构。
signal(SIGCHLD,SIG_IGN) // 在main()最前面加上这一行代码,表示忽略子进程退出的信号
    1. 父进程通过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);
}
  • 29
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值