【Linux】---进程控制(创建、终止、等待、替换)

news2024/11/26 8:30:28

文章目录

  • 进程创建
    • fork()
  • 进程退出
    • 进程退出场景
    • 进程退出方法
      • 退出码
      • exit、_exit
  • 进程等待
    • 进程等待的方法
    • wait
    • waitpid
    • 阻塞和非阻塞
  • 进程替换
    • 替换的原理
    • 替换所用到的函数
      • execl
      • execlp
      • execle
  • 简易的shell

进程创建

fork()

fork函数在之前的文章中也已经提到过了。其主要作用是从已存在的进程中创建一个新的进程,也就是新建的进程为子进程,原进程为父进程

当一个进程调用fork函数后,内核会做几件事:

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝给子进程
  3. 添加子进程到系统进程列表中
  4. fork返回后,开始调度器调度

下面来看看进程创建的一段简单代码

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main(){
    pid_t id = fork();
    
    if(id == 0){
        printf("I am child process, pid = %d, ppid = %d\n", getpid(), getppid());
    }
    
    printf("I am parent process, pid = %d, ppid = %d\n", getpid(), getppid());
    return 0;
}

image-20221128143753830

当其返回值为0时,说明创建出了子进程。

进程退出

进程退出场景

进程退出总共会有三种情况,也就是我们平常写代码执行的时候也是会遇到这三种情况:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码遇到异常终止执行

进程退出方法

对于进程退出而言,可以有两种方法退出。一种就是正常的程序运行完毕终止执行,另一种就是程序遇到异常信号终止运行。

那么现在有一个问题,我们平常写代码的时候为什么总是会带上一个 return 0 呢?这里就涉及到一个知识点—退出码

退出码

其实return 0这个0并没有什么特殊的意思,返回的是0就代表着程序执行正常退出,非0就是程序有错误,每一个非0的退出码都代表着不同的错误信息。可以通过程序看看

#include<stdio.h>
#include<string.h>

int main(){
	for(int i = 0; i < 20; i++)
		printf("%d: %s\n", i, strerror(i));
	
	return 0;
}

image-20221128145109320

exit、_exit

那么除了return可以返回退出码退出程序外,exit和**_exit**也是可以的。不过这两者之间还是会有所区别的。

  1. exit是库函数,_exit是系统调用
  2. exit会刷新缓冲区,_exit不会

进程等待

进程等待是非常重要的。之前在进程状态里面谈到了一种状态—僵尸状态。这种状态是非常危险的,会造成内存泄漏。并且一旦进程变成了僵尸状态,那么即使使用kill -9都无法将其杀死。

所以父进程想要获取子进程的任务完成的程度如何,就必须通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法

进程等待有两种方法:1、阻塞等待;2、非阻塞等待。可以使用两个函数去实现:wait和waitpid

image-20221128150111166

wait

wait等待成功会返回被等待的进程的pid,失败则返回-1,下面来一段代码感受一下

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>

int main(){
  pid_t id = fork();

  if(id == 0){
    int cnt = 5;
    while(cnt--){
      printf("I am child process, pid = %d\n", getpid());
      sleep(1);
    }
    exit(1);
  }

  pid_t ret = wait(NULL);
  printf("%d\n", ret);

  return 0;
}

image-20221128151017389

可以看到进程最后打印出的是子进程的pid,说明等待成功了。

waitpid

waitpid 相对于 wait 来说能够获取的信息就更多了,可以获取子进程的退出码和子进程返回的状态。

如果子进程是正常终止,那么返回的状态为0,如果收到了异常信号终止则非0

但是这里还要注意的是,waitpid 返回的子进程的数据是有自己的存储方式的。例如 waitpid 返回了一个变量 status 那么这个变量的**高八位为退出状态,低八位为终止信号。

image-20221128151748607

如果进程是被信号所杀,则退出状态就没有用到,终止信号根据实际。如果进程正常终止,则退出状态根据实际,终止信号为0.

所以当waitpid 返回了一个值,我们想要获取终止信号就得用这个值 & 0x7f;获取退出状态就得用这个变量 向右移动8位再 & 0xff

来一段代码感受一下

#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>

int main(){
  pid_t id = fork();
  assert(id != -1);

  if(id == 0){
    int cnt = 5;
    while(cnt){
      printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
      sleep(1);
    }

    exit(0);
  }

  int status = 0;
  while(1){
    pid_t ret = waitpid(id, &status, 0);
    if(ret == 0){
      //子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
      //waitpid调用成功 && 子进程没有退出
      printf("wait done, but child is running....., parent running other things\n");
    }
        
    else if(ret > 0){
      //waitpid调用成功 && 子进程已退出
      printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
      break;
    }

    else{
      //waitpid调用失败
      printf("waitpid call failed\n");
      break;
    }

    sleep(1);
  }

  return 0;
}

image-20221128160417922

下面再来看看当子进程收到异常信号时退出 waitpid 返回的结果。现在假设一个野指针的情况

#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>

int main(){
  pid_t id = fork();
  assert(id != -1);

  if(id == 0){
    int cnt = 5;
    while(cnt){
      printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
      sleep(1);
    }
	int* p;
	*p = 100;
	
    exit(0);
  }

  int status = 0;
  while(1){
    pid_t ret = waitpid(id, &status, 0);
    if(ret == 0){
      //子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
      //waitpid调用成功 && 子进程没有退出
      printf("wait done, but child is running....., parent running other things\n");
    }
        
    else if(ret > 0){
      //waitpid调用成功 && 子进程已退出
      printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
      break;
    }

    else{
      //waitpid调用失败
      printf("waitpid call failed\n");
      break;
    }

    sleep(1);
  }

  return 0;
}

image-20221128160758879

可以看到,此时waitpid接收到了异常信号退出并返回异常信号的值,可以通过kill -9查看对应的异常信息

阻塞和非阻塞

上面提到了等待可以分为阻塞等待和非阻塞等待,那么这两种有什么区别呢。

通俗点理解

阻塞等待就是父进程在等待子进程退出时并不会再去做其他的事情

非阻塞等待则是父进程在等待时如果他检测到子进程还没有退出,那它就退出检测去做自己的事情。做完自己的事情后又会检测,直至检测到子进程退出

在 waitpid 中可以通过传入 WNOHANG 表示非阻塞等待,传入0则表示阻塞等待。具体来看一段代码感受一下

#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>

#define N 10
typedef void (*func_t)();//函数指针
func_t handlerTask[N];

void task1(){
  printf("task1\n");
}

void task2(){
  printf("task2\n");
}

void task3(){
  printf("task3\n");
}

void loadTask(){
  memset(handlerTask, 0, sizeof(handlerTask));
  handlerTask[0] = task1;
  handlerTask[1] = task2;
  handlerTask[2] = task3;
}

int main(){
  pid_t id = fork();
  assert(id != -1);

  if(id == 0){
    int cnt = 5;
    while(cnt){
      printf("child running pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
      sleep(1);
    }

    exit(0);
  }

  loadTask();

  int status = 0;
  while(1){
    pid_t ret = waitpid(id, &status, WNOHANG);//WNOHANG: 非阻塞-> 子进程没有退出,父进程检测时立即退出
    if(ret == 0){
      //子进程没有退出,waitpid没有等待失败,仅仅是检测到子进程没退出
      //waitpid调用成功 && 子进程没有退出
      printf("wait done, but child is running....., parent running other things\n");
      for(int i = 0; handlerTask[i] != NULL; i++)
        handlerTask[i]();

    }

    else if(ret > 0){
      //waitpid调用成功 && 子进程已退出
      printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xff, (status & 0x7f));
      break;
    }

    else{
      //waitpid调用失败
      printf("waitpid call failed\n");
      break;
    }

    sleep(1);
  }

  return 0;
}

image-20221128161320762

可以看到父进程在等待的同时还会去执行指定的任务。等父进程检测到子进程还没有退出时,它就会退出等待去做他的事情。

进程替换

替换的原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

简单理解:替换只是把代码和数据换掉而已,并没有换进程去执行。我们可以通过替换去用A程序执行B程序。

替换所用到的函数

一般来说 实现替换有六种函数选择

#include <unistd.h>`
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 execve(const char *path, char *const argv[], char *const envp[]);

这六个函数各有不同的参数,因此实现的方法不同,但是都是为了替换。

execl

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>


int main(){ 
    printf("process running ...\n");

    //.c -> exe -> load -> process -> 运行 -> 执行所写代码
    printf("process is running ....\n");

    //load -> exe
    //第一个参数是告诉系统要执行谁,第二个是要怎么执行
    execl("/usr/bin/ls", "ls", NULL);// all exec* end of NULL

    printf("process running done...\n");

    return 0;
}

execl需要传入替换程序的地址。

image-20221128162635610

替换完成后,当我们执行程序就会有替换程序的效果了。要注意:当execl调用成功后,之后的语句就不再执行了。还可以在execl里面传入选项,已完成更全面的功能实现

 execl("/usr/bin/ls", "ls", "-a", "-l", NULL);// all exec* end of NULL

execlp

execlp相较于execl是不需要传入地址的,它会自动在环境变量里找。只需要传入需要替换的程序即可

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>


int main(){ 
    printf("process running ...\n");

    //.c -> exe -> load -> process -> 运行 -> 执行所写代码
    printf("process is running ....\n");

    //load -> exe
    execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);

    printf("process running done...\n");

    return 0;
}

image-20221128163246360

execle

exec* 的函数不仅可以替换系统中的程序,也可以替换我们自己的程序。现在我写一个程序,程序的主要功能是打印环境变量。然后我再用另一个程序替换

mybin.c

#include<stdio.h>
#include<stdlib.h>

int main(){
  printf("这是另一个C程序\n");
  printf("PATH: %s\n", getenv("PATH"));
  printf("PWD: %s\n", getenv("PWD"));
  printf("MYENV: %s\n", getenv("MYENV"));

  return 0;
}

myexec.c

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>


int main(){ 
  printf("process running ...\n");
  pid_t id = fork();
  assert(id != -1);

  if(id == 0){
    sleep(1);
    char* const envp_[] = {(char*)"MYENV=1234565", NULL};
    execle("./mybin", "mybin", NULL, envp_);

    exit(1);
  }

  return 0;
}

image-20221128163826589

如果我们自定义了环境变量,那么系统本身的环境变量就不会点出来了。

剩下的几个函数都是一样的道理,这里就不说了,可以自行查文档实现

image-20221128164255263

简易的shell

前面讲完了进程的控制,包括:创建、终止、等待、替换。那么结合这些知识我们就可以自己去写一个简单的shell外壳了。

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#include<assert.h>

#define NUM 1024
#define OPT_NUM 100
char lineCommand[NUM];
char *myargv[OPT_NUM];//指针数组
int  lastCode = 0;
int  lastSig = 0;

int main(){
  while(1){
    //输出提示符
    printf("用户名@主机名 当前路径# ");
    fflush(stdout);
  
    //获取输入,输入结束要有'\n'
    char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
    assert(s != NULL);
  
    (void)s;
    //清除最后一个'\n'
    lineCommand[strlen(lineCommand) - 1] = 0;
  
    //字符串切割
    myargv[0] = strtok(lineCommand, " ");
    int i = 1;
  
    //将颜色选项放入ls命令中
    if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
      myargv[i++] = (char*)"--color=auto";
  
    //没有子串的话,strtok返回NULL
    while(myargv[i++] = strtok(NULL, " "))
      ;
  
    //cd命令不会创建子进程,就让shell自己执行对应命令,执行系统接口
    if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){
              if(myargv[1] != NULL) 
                chdir(myargv[1]);
              continue;
    }
  
    //执行命令
    pid_t id = fork();
    assert(id != -1);
  
    if(id == 0){
      execvp(myargv[0], myargv);
      exit(1);
    }
  
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    assert(ret > 0);
    (void) ret;
  
    lastCode = ((status>>8) & 0xFF);
    lastSig = (status & 0x7F);
  
  }

}

image-20221128170019119

运行起来之后,虽然还有很多bug,还是能够执行一些简单的指令的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/42670.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

excel提示stdole32.tlb的解决方法

大家在使用excel时有遇到stdole32.tlb错误提示吗&#xff1f;出现这个问题直接导致excel无法启动&#xff0c;非常影响用户的工作效率。为了顺利解决问题&#xff0c;小编给大家带来了详细的解决办法&#xff0c;希望可以帮到你。 win7系统打开excel提示stdole32.tlb的解决方法…

Nosql inject注入

0x00 Nosql inject 最近主要在看那个 YApi 的注入漏洞&#xff0c;也是一个 mongodb的注入 所以来写一下这个东西&#xff0c;其实现在越来越常见的Nosql注入 感觉很多分布式和一些新的系统已经大量使用这种nosql数据库&#xff0c;这个注入和传统的关系型数据库有一点点不同…

【Hack The Box】linux练习-- Meta

HTB 学习笔记 【Hack The Box】linux练习-- Meta &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月27日&#x1f334; &#x1f36d…

[附源码]计算机毕业设计springboot“科教兴国”支教门户网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

复旦MBA项目GNW海外课程|连线全球顶尖商学院,跨时空学习精彩无限!

10月下旬&#xff0c;复旦MBA为期一周的GNW海外课程落下帷幕&#xff0c;复旦在职MBA的同学们通过云端连线&#xff0c;走进全球多所顶级商学院&#xff0c;与深谙商道、学术造诣深厚的教授学者&#xff0c;以及来自不同地域不同文化背景的精英学生取经论道。      复旦MBA…

深入理解死锁问题

死锁问题&#x1f3de;️1. 死锁概念&#x1f301;2. 为什么发生死锁&#x1f320;3. 产生死锁的条件&#x1f301;4. 如何避免死锁&#x1f4d6;4.1 循环等待&#x1f4d6;4.2 持有并等待&#x1f4d6;4.3 非抢占&#x1f4d6;4.4 互斥&#x1f33f;5. 通过调度避免死锁&#…

【Python开发】一文详解Flask-Login

一文详解Flask-LoginFlask-Login 为 Flask 提供用户会话管理。它处理登录、注销和长时间记住用户会话等常见任务。 Flask-Login 不绑定到任何特定的数据库系统或权限模型。唯一的要求是您的 用户对象实现一些方法&#xff0c;并且您向能够 从用户 ID 加载用户 的扩展提供回调。…

Kotlin 开发Android app(十二):Android布局FrameLayout和ViewPager2控件实现滚动广告栏

在上一节中我们简单的介绍了RecyclerView 的使用&#xff0c;他是整个开发的重点控件&#xff0c;这一节我们来看看FrameLayout 布局结合ViewPager2&#xff0c;开发一个广告控件。 新模块banner 先创建一个新的模块&#xff0c;取名为banner&#xff0c;用来创建我们的滚动广…

Spring Boot自定义Namespace

Spring Boot 自定义Namespace 在学些Spring Boot 自定义Namespace之前&#xff0c;先来看一个简单的案例。在Spring Boot出现之前&#xff0c;所有的bean都是在XML文件的格式 中定义。为了管理方便&#xff0c;一些大型复杂的应用系统&#xff0c;通常定个多个xml文件来共同满…

【笑小枫的按步照搬系列】JDK8下载安装配置

笑小枫&#x1f495; 欢迎来到笑小枫的世界&#xff0c;喜欢的朋友关注一下我呦&#xff0c;大伙的支持&#xff0c;就是我坚持写下去的动力。 微信公众号&#xff1a;笑小枫 笑小枫个人博客&#xff1a;https://www.xiaoxiaofeng.com 一、安装 1、方式一&#xff1a;进入官网…

Apifox:成熟的测试工具要学会自己写接口文档

好家伙&#xff0c; 在开发过程中&#xff0c;我们总是避免不了进行接口的测试&#xff0c; 而相比手动敲测试代码&#xff0c;使用测试工具进行测试更为便捷&#xff0c;高效 今天发现了一个非常好用的接口测试工具Apifox 相比于Postman&#xff0c;他还拥有一个非常nb的功…

读《基于深度学习的跨视角步态识别算法研究》

2020 背景&#xff1a; 作为一种新兴的识别技术&#xff0c;步态识别具有在非受控、远距离、低分辨率的场景下进行身份识别的优点&#xff0c;并且步态不易改变和伪装&#xff0c;所以近年来得到的关注逐渐增多。 步态识别作为一种新兴的身份识别技术&#xff0c;可以根据人…

jsp美食管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 美食管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统采用serlvet dao bean mvc模式开发&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式 开发。开发环境为TOMCAT7.0,Myeclipse8…

ZYNQ之FPGA学习----UART串口实验

1 UART串口简介 UART串口基础知识学习&#xff1a;硬件设计基础----通信协议UART 2 实验任务 上位机通过串口调试助手发送数据给 Zynq&#xff0c;Zynq PL 端通过 RS232 串口接收数据并将接收到的数据发送给上位机&#xff0c;完成串口数据环回&#xff0c;管脚分配如下&…

软件测试的分类

这里先讲一些概念&#xff0c;改日从这里边挑几个细讲。&#xff08;给小白看的&#xff09; 按测试对象划分&#xff1a; 界面测试&#xff1a; 软件只是一种工具&#xff0c;软件与人的信息交流是通过界面来进行的&#xff0c;界面是软件与用户交流的最直接的一层&#xff…

基于二次近似(BLEAQ)的双层优化进化算法_matlab程序

参考文献如上。 双层优化问题是一类具有挑战性的优化问题&#xff0c;包含两个层次的优化任务。在这些问题中&#xff0c;下层问题的最优解成为上层问题的可能可行候选。这样的要求使得优化问题难以解决&#xff0c;并使研究人员忙于设计能够有效处理该问题的方法。尽管付出了…

Redis常见面试问题总结

文章目录Redis 基础面试说说你对Redis的了解?说说Redis中的数据类型&#xff1f;说说Redis数据类型对应的数据结构&#xff1f;说说Redis对应的Java客户端有哪些&#xff1f;说说Redis 中持久化发生了什么&#xff1f;说说Redis中持久化以及方式&#xff1f;如何理解Redis中RD…

2022年超实用的推特营销策略

Twitter推广需知的13条基础知识&#xff1a; 1、Twitter日活用户达1亿 2、Twitter月活用户3.25亿 3、Twitter广告价格比其他渠道便宜33% 4、每天产生5亿条推文 5、Twitter推广能够提高29%的线下交易 6、37%的Twitter用户在18到29岁之间 7、86%的带链接推文会比普通推文效…

JUC并发编程与源码分析笔记03-CompletableFuture

Future接口理论知识复习 Future接口&#xff08;FutureTask实现类&#xff09;定义了操作异步任务执行的一些方法&#xff0c;如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。 找到java.util.concurrent.Future&#xff0c;看到里面…

Node.js 入门教程 22 将所有 Node.js 依赖包更新到最新版本

Node.js 入门教程 Node.js官方入门教程 Node.js中文网 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录Node.js 入门教程22 将所有 Node.js 依赖包更新到最新版本22 将所有 Node.js 依赖包更新到最新版本 当使用 npm install <packagename&g…