【Linux】进程控制

进程创建

  • 一、进程创建
    • 1.fork函数初识
    • 2.fork函数的两个返回值
    • 3.写时拷贝
    • 4.fork常规用法
  • 二、进程终止
    • 1.进程退出场景
    • 2.进程退出方法
      • 正常终止:
        • exit函数:
        • _exit函数:
        • 区别:
      • 异常退出:
  • 三、进程等待
    • 1.进程等待的必要性
    • 2.进程等待的方法
        • wait方法:
        • waitpid方法:
        • 获取子进程status:
    • 3.阻塞与非阻塞等待
    • 4.总结
  • 四、进程程序替换
    • 1.替换原理
    • 2.进程替换操作
        • exec 系列函数
        • exec 系列函数的使用


一、进程创建

1.fork函数初识

linux中,fork()函数非常重要,它从当前进程中创建一个新的进程。新进程为子进程,原始进程为父进程。

#include 
pit_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1

2.fork函数的两个返回值

当一个进程调用fork之后,就有两个二进制代码相同的进程,自然也会被return两次。
具体的参考大佬的博客:进程控制
【Linux】进程控制_第1张图片
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。但谁先执行完全由调度器决定。

3.写时拷贝

通常,父子代码共享。
1)修改内容之前,父子进程的数据段+代码段默认共享;
2)修改内容之后,谁写入,谁发生写时拷贝,即写入的数据开辟新的空间。
【Linux】进程控制_第2张图片

4.fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段;例如,父进程等待客户端请求,生成子进程来处理请求。前面使用的 fork都属于这种情况。
  • 一个进程要创建子进程来执行一个不同的程序;例如子进程从 fork 返回后,调用 exec 系列函数。即下面进程替换内容。

二、进程终止

1.进程退出场景

退出码0代表进程运行结果正确
退出码非0代表运行结果错误

  • 代码运行完毕且结果正确 – 退出码为0;
  • 代码运行完毕且结果不正确 – 退出码为非0;
  • 代码异常终止 – 退出码无意义。

2.进程退出方法

正常终止:

可以通过echo $?查看最近一个进程的退出码
1.从main返回
2.调用exit
3.调用_exit

exit函数:
#include 
    
void exit(int status);

status:status 定义了进程的终止状态,父进程通过wait来获取该值
    
函数功能:终止进程

_exit函数:
#include 
    
void _exit(int status);

status:status 定义了进程的终止状态,父进程通过wait来获取该值
    
函数功能:终止进程

区别:
  • exit()是库函数,_exit()是系统调用, 库函数是对系统调用封装的一些接口。即exit 的底层是 _exit 函数,exit 是 _exit 的封装。
  • exit 在终止程序后会刷新缓冲区,而 _exit 终止程序后不会刷新缓冲区。
  • exit 的底层是 _exit,而 _exit 并不会刷新缓冲区,即缓冲区不在操作系统内部,而是在用户空间。

异常退出:

Ctrl C 终止进程

三、进程等待

1.进程等待的必要性

解决僵尸进程问题。父进程通过进程等待的方式,回收子进程资源,获取子进程信息。
进程的退出信息是存放在子进程的 task_struct 中的,所以进程等待的本质就是从子进程 task_struct 中读取退出信息,然后保存到相应变量中去。

2.进程等待的方法

wait方法:
#include 
#include 

pid_t wait(int *status);

返回值:成功返回被等待进程的pid,失败返回-1;

status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;

wait的使用:
【Linux】进程控制_第3张图片

【Linux】进程控制_第4张图片
【Linux】进程控制_第5张图片

开始时父子进程都处于休眠状态,之后子进程运行5s退出,此时由于父进程还要休眠5s,所以没有对子进程进行进程等待,所以子进程变成僵尸状态Z。5s过后,父进程使用 wait 系统调用对子进程进行进程等待,所以子进程由僵尸状态变为彻底死亡状态。

waitpid方法:
#include 
#include 

pid_t waitpid(pid_t pid, int *status, int options);

返回值:成功返回被等待进程的pid,失败返回-1;

pid:pid=-1,等待任意一个子进程,与wait等效;pid>0.等待其进程id与pid相等的子进程
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;
options:等待方式,options=0,阻塞等待;options=WNOHANG,非阻塞等待

waitpid的使用:
【Linux】进程控制_第6张图片

【Linux】进程控制_第7张图片

【Linux】进程控制_第8张图片

waitpid 可以传递 id 来指定等待特定的子进程,也可以指定 options 来指明等待方式。

获取子进程status:
  • wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息 。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低16比特位):
    【Linux】进程控制_第9张图片
    status 低两个字节的内容被分成了两部分 – 第一个字节前七位表示退出信号/终止信号,最后一位表示 core dump 标志;第二个字节表示退出状态,退出状态即代表进程退出时的退出码;
    对正常退出的程序来说,退出信号和 core dump 标志都为0,退出状态等于退出码;对于异常终止的程序来说,退出信号为不同终止原因对应的数字,退出状态未用,无意义。
    因此 status读取为:
printf("exit signal:%d,  exit code:%d \n", (status & 0x7f), (status >> 8 & 0xff)); 

其中,status 按位与上 0x7f 表示保留低七位,其余九位全部置为0,从而得到退出信号;
status 右移8位得到退出状态,再按位与上 0xff 是为了防止右移时高位补1的情况;

Linux 提供了 WIFEXITED 和 WEXITSTATUS 宏 来帮助我们获取 status 中的退出状态和退出信号

该部分参考自:添加链接描述

3.阻塞与非阻塞等待

阻塞式等待即当父进程执行到 waitpid 函数时,如果子进程还没有退出,父进程就只能阻塞在 waitpid 函数,直到子进程退出,父进程通过 waitpid 读取退出信息后才能接着执行后面的语句;

而非阻塞式等待则不同,当父进程执行到 waitpid 函数时,如果子进程未退出,父进程会直接读取子进程的状态并返回,然后接着执行后面的语句,不会等待子进程退出。

轮询
轮询是指父进程在非阻塞式状态的前提下,以循环方式不断的对子进程进行进程等待,直到子进程退出。

原文链接:https://blog.csdn.net/m0_62391199/article/details/128033352

4.总结

  • 为了读取子进程的退出结果以及回收子进程资源,需要进行进程等待。
  • 进程等待的本质是父进程从子进程 task_struct中读取退出信息,然后保存到 status 中。
  • 可以通过 wait 和 waitpid 系统调用进行进程等待。 status参数是一个输出型参数,父进程通过 wait/waitpid 函数将子进程的退出信息写入到 status 中。
  • status 以位图方式存储,包括退出状态和退出信号,若退出信号不为0,则退出状态无效。
  • 可以使用系统提供的宏 WIFEXITED 和WEXITSTATUS 来分别获取 status 中的退出状态和退出信号。
  • 进程等待的方式分为阻塞式等待与非阻塞式等待,阻塞式等待用0来标识,非阻塞式等待用宏 WNOHANG 来标识。
  • 由于非阻塞式等待不会等待子进程退出,所以我们需要以轮询的方式来不断获取子进程的退出信息。

四、进程程序替换

1.替换原理

进程程序替换是指父进程用 fork 创建子进程后,子进程通过调用 exec 系列函数来执行另一个程序;当进程调用某一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,然后从新程序的启动例程开始执行;

但是原进程的 task_struct 和 mm_struct 以及进程 id 都不会改变,页表可能会变;所以调用 exec 并不会创建新进程,而是让原进程去执行另外一个程序的代码和数据

程序替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序,即
1、程序替换成功后,运行完新程序,依然会运行原有的代码
2、程序替换成功后,原进程没有退出,使用原进程运行新程序

2.进程替换操作

exec 系列函数

实现进程程序替换的系统调用函数就一个:execve,其他一系列的 exec 库函数都是为了满足不同的替换场景而对 execve 系统调用进行的封装;主要认识六个 exec 库函数。

#include `
int execl(const char *path, const char *arg, ...);
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[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

这些函数调用成功则加载新的程序并启动代码开始执行,不再返回;如果调用出错则返回-1。

这些 函数一旦调用成功,就代表着原程序的代码和数据已经被新程序替换掉了,原程序后续的语句都不会再被执行了,所以 exec 调用成功后没有返回值,因为该返回值没有机会被使用;只有 exec 调用失败,原程序可以继续往下执行时,exec 返回值才会被使用。

exec 系列函数的使用

执行程序就两个步骤 :
一是找到该可执行程序;二是指定程序执行的方式。
对于 exec 函数来说,“p” 和非 “P” 用来找到程序,“l” “v” 用来指定程序执行方式;“e” 用来指定环境变量。

(1)execl && execlp
ls指令为例

execl("/usr/bin/ls", "ls", "-a", "-l", NULL);  
execlp("ls", "ls", "-a", "-l", NULL);  //函数带有 “p”,可以不带路径参数

可以看到,exec无非就两个参数:
第一个参数是路径参数。注意带 “p” 的 exec 函数可以不带路径的前提是被替换程序处于PATH环境变量中。
第二个参数是如何执行程序。即Linux 命令行中该程序如何执行就如何传参,要注意的是,exec 中需要对不同选项进行分割,即每一个选项都单独分为一个字符串,并且最后一个可变参数设置为 NULL,表示传参完毕。

进程程序替换时如果想要让不同类型文件表现为不同颜色的话,需要显示传递 “–color=auto” 选项。

实例如下:

#include 
#include 
#include 
#include 
#include 

int main()
{
  pid_t pid=fork();
  if(pid==-1)
  {
    perror("fork"),exit(-1);
  }
  else if(pid==0)
  {
    printf("child process is running... pid:%d\n ", getpid());

    int ret = execl("/usr/bin/ls", "ls", "-l", "-a", "--color=auto", NULL);
    if(ret == -1)
    {
      printf("process exec fail....\n");
      exit(1);
    }

    printf("child process is done... pid:%d\n ", getpid());
    return 0;
  }
  else{
    int status = 0;
    pid_t ret = waitpid(pid, &status, 0);  //进程等待
    if(ret == -1)
    {
      perror("waitpid");
      return 1;
    }
    else
    { 
      printf("exit single:%d, exit code:%d\n", (status & 0x7f),(status >> 8 & 0xff));
    }
  }
  return 0;
}

【Linux】进程控制_第10张图片
可以看到在命令行上使用 “ls -a -l” 和使用进程程序替换得到的结果一致。

(2)execv && execvp
“v” 代表参数采用数组的形式传递 – argv 是一个指针数组,数组里面的每一个元素都是指针,每一个指针都指向一个参数 (字符串),同样最后一个元素指向 NULL,代表参数传递完毕;
(3)execle && execvpe
“e” 代表环境变量 – 和 argv 一样,envp 也是一个指针数组,数组里面的每个元素都是一个指针,指向一个环境变量 (字符串),我们可以显式初始化 envp 来传递我们自定义的环境变量,但是这也代表着我们放弃了系统环境变量

你可能感兴趣的:(linux,bash,服务器)