草庐IT

【Linux】进程控制

星河万里᭄ꦿ࿐ 2023-09-08 原文

文章目录


一、进程创建

1. fork函数初识

Linux中的fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>//头文件
pid_t fork(void);
//返回值:自进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

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

我们看下面的代码:

int main()
{
  pid_t id = fork();
  int cnt = 0;
  if(id>0)
  {
    while(1)
    {
      printf("父进程,pid:%d,ppid:%d,id:%d,cnt的值:%d\n",getpid(),getppid(),id,cnt);
      sleep(1);
    }
  }
  else if(id==0)
  {
     while(1)
     {
                
       printf("子进程,pid:%d,ppid:%d,id:%d,cnt的值:%d\n",getpid(),getppid(),id,cnt);  
       cnt++;
       sleep(1);  
     }  
  }
  else{
    printf("fork fail\n");
     return -1;
  } 
  return 0;
}

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。fork之前由父进程独立运行,fork之后父子进程分别执行。当然了,fork之后,谁先执行完全由调度器决定。

💕 如何理解fork之后,给父进程返回子进程的ID,给子进程返回0呢?

这是因为父进程可能有多个子进程,但是子进程只能有一个父进程,所以需要给父进程返回子进程的ID,将子进程管理起来。

💕 如何理解if-else-if-else语句能够同时执行呢?

当fork函数在return之前就已经将子进程创建好了,创建好的子进程可能已经在运行队列中了,而且fork之后代码是供父进程和子进程共享的,在fork内部已经有父子两个执行流了,因为两个return语句都将会被执行,但是两个执行流谁先执行是由调度器决定的,因为返回的本质是写入,所以谁先返回就将谁写入id,由于写时拷贝的发生,所以地址一样,但是内容不一样,因此 就可以让父子进程执行不同的代码了。


2. 写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:


3. fork常规的用法

💕 fork的常规用法:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

💕 fork调用失败的原因:

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

二、进程终止

1. 进程退出码

我们创建进程的目的是为了让进程帮我们执行任务,但是任务执行的结果如何应该如何衡量呢?如果一个程序正常运行完了,那他会有两种结果:结果正确或者结果不正确 ,一般来说,不同的退出码表示的是不同的结果。

一般来说:0表示运行结果正确,非0表示运行结果不正确。这里我们来看一下C语言中的库函数strerror,他表示从内部数组中搜索错误号errnum,然后返回一个指向错误消息的字符串的指针。


查看进程的退出码

在Linux下,我们可以使用echo $?来查看进程退出码,但在这里我们需要注意的是它只能查看最近的一个进程的进程执行完成时的退出码。


2. 进程退出场景及常见的退出方法

💕 进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

💕 进程退出的方法

进程正常退出有如下几种方法:

  • 从main函数返回
  • 调用exit
  • 调用_exit

库函数:exit

exit的用法:

exit函数将我们的程序直接终止,并没有执行exit之后的语句,exit后,组成进程的程序,数据,进程控制块全部消失。

系统调用:_exit

下面我们对上面的程序调用,系统调用的_exit函数来看一下效果:


这里我们可以看到:它的效果和exit的效果是一样的。

_exit和exit的区别

既然exit和_exit的效果一样,那为什么要出现两份呢?其实他们还是有区别的。


这里我们需要知道的是exit的内部调用了_exit,为什么调用exit会打印,而调用_exit不会打印呢?这是因为数据将会先被写入缓冲区,待缓冲区刷新的时候才能被写入到显示器上。在上面的程序中,因为没有使用'\n'进行行缓冲的刷新。所以exit在终止程序后会刷新缓冲区,而_exit不会。但由于exit的底层封装了_exit,所以我们可以得出结论:缓冲区并不在操作系统内部,而是在用户空间。

💕 进程异常退出

我们的进程也是可以异常退出的,比如:Ctrl C终止进程、或者程序中有遇到野指针,/0、空指针野指针等问题。

例如,当我们在程序中使用一个整数去除零的时候:


三、进程等待

进程等待指的是通过系统调用获取子进程退出码或者退出信号的方法。

1. 进程等待的必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的本质就是父进程需要通过进程等待的方式,回收子进程剩余资源(PCB,内核栈等),获取子进程退出信息,父进程需要知道子进程的退出码和执行时间等信息,形象化的比喻就是父进程通过进程等待来给僵尸进程收尸。


2. 进程等待的方法

wait方法

如果等待进程终止成功,wait函数将会返回终止进程的id值,等待失败则会返回-1,下面我们来看一下wait函数。

我们先来看一下wait的用法:


子进程在执行完之后的五秒内由于父进程处于休眠状态,所以子进程正在处于僵尸状态,五秒过后父进程调用wait回收子进程,子进程由僵尸状态退出结束了进程。最后父进程在等待五秒后自动退出。

waitpid方法

如果我们将最后一个options设置了WNOHANG,如果调用中waitpid发现没有已退出的子进程可以收集,则返回0;如果调用中出错,则返回-1,这是errno会被设置成相应的值以指示错误信息。

下面我们改一下我们的代码:


但在这里我们发现status并不是我们想要得到的111,这里的status表示的是位置信息 ,不能被当作普通的整形看待。

下面我们来看一下status如何得到 退出状态终止信号

终止信号(退出码) status>>8&0xFF

退出状态(正常退出还是异常退出) status&0x7F


  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

通过宏来拿到子进程的退出信息

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)


3. 非阻塞等待

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

非阻塞等待 表示父进程执行到waitpid函数时,如果子进程未退出,父进程会直接读取子进程的退出状态并返回,然后接着执行后面的语句,不会等待子进程的退出。由于非阻塞等待不会等子进程退出,所以我们需要以轮询的方式来不断获取子进程 的退出信息。

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



四、进程程序替换

1. 替换原理

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


2. 替换函数

具有六种以exec开头的函数,统称为exec函数:

#include <unistd.h>

extern char **environ;

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[]);
//系统调用
int execve(const char *path, char *const argv[], char *const envp[]);

这些函数提供了一个在进程中启动另一个程序的执行方法,可以根据指定的文件名或者目录名找到可执行程序,并用它来取代原调用进程的数据段,代码段和堆栈段。在执行完之后,原调用进程,的内容除了进程ID外,其他的内容全部被新的进程替换了。

关于这些函数,我们需要注意几点:

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。(这是因为exec函数调用成功之后,exec函数之后的代码就不会再执行了,所以exec函数调用成功之后返回值没有任何的意义,因此,需要调用失败时的返回值)

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量


execlp

int execlp(const char *file, const char *arg, ...);



execv

int execv(const char *path, char *const argv[]);



execvp

int execvp(const char *file, char *const argv[]);


前几个函数没有传递环境变量,但是子进程依然能够通过environ拿到环境变量,是通过进程地址空间的方式让子进程拿到的。


execle

int execle(const char *path, const char *arg,..., char * const envp[]);

这个函数以及下一个我们需要讲解的函数中最后一个'e'代表的是环境变量和argv一样,我们可以显式的初始化envp(指针数组)来传递我们的系统环境变量,当然也可以传递我们的自定义的环境变量。

传递系统环境变量



这里我们可以看到,如果我们传递的是系统环境变量,所以系统环境变量被打印了出来了,那么如果我们传递的是自定义的环境变量呢?


这里我们可以看到我们仅仅获取了自定义的环境变量MYENV,而系统环境变量PATH则获取失败了,如果我们想要同时获取自定义环境变量和系统环境变量该如何做呢?在这里我们就可以使用 putenv将自定义环境变量导入系统环境变量。然后通过传递系统环境变量environ来实现:



execvpe

int execvpe(const char *file, char *const argv[],char *const envp[]);



五、shell的模拟实现

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

#define NUM 1024    //一个命令的最大长度
#define OPT_NUM 64  //一个命令的做多选项

char lineCommand[NUM];
char* argv[OPT_NUM];  
int EXIT_CODE;  //保存进程退出码

int main() {
    while(1) {

        //输出提示符
        printf("[用户名@主机名 当前路径]$ ");
        fflush(stdout);

        //获取输入
        char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin);  //最后留一个位置来存放极端情况下的\0
        if( ret == NULL ) {
            perror("fgets");
            exit(1);
        }
        //消除命令行中最后的换行符
        lineCommand[strlen(lineCommand) - 1] = '\0';  

        //将输入解析为多个字符串存放到argv中,即字符串切割
        argv[0] = strtok(lineCommand, " ");
        int i = 1;
		//ls颜色显示
        if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)  
        {
            argv[i++] = (char*)"--color=auto";
        }

        while(argv[i++] = strtok(NULL, " "));
		//cd改变父进程工作路径
        if(argv[0] != NULL && strcmp(argv[0], "cd") == 0)  
        {
            if(argv[1] != NULL) 
                chdir(argv[1]);  //myargv[1]中保存着指定路径
            continue;
        }
        //处理echo内建命令
        if(argv[0] != NULL && strcmp(argv[0], "echo") == 0) 
        {
            if(strcmp(argv[1], "$?") == 0){  //echo $?
                printf("%d\n", EXIT_CODE);
                EXIT_CODE = 0;
            } else {  //echo $变量
                printf("%s\n", argv[1]+1);
            }
            continue;
        }

        //创建子进程
        pid_t id = fork();
        if(id == -1) {
            perror("fork");
            exit(1);
        } else if (id == 0) {  //子进程
            int ret = execvp(argv[0], argv);  //进程程序替换
            if(ret == -1) {  
                printf("No such file or directory\n");
                exit(1);
            }
        } else {  //父进程
            int status = 0;
            pid_t ret = waitpid(id, &status, 0);  //进程等待
            EXIT_CODE = (status >> 8) & 0xFF;  //获取退出状态
            if(ret == -1){
                perror("wait");
                exit(1);
            }
        } 
    }
    return 1;
}

有关【Linux】进程控制的更多相关文章

  1. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  3. Ruby Readline 在向上箭头上使控制台崩溃 - 2

    当我在Rails控制台中按向上或向左箭头时,出现此错误:irb(main):001:0>/Users/me/.rvm/gems/ruby-2.0.0-p247/gems/rb-readline-0.4.2/lib/rbreadline.rb:4269:in`blockin_rl_dispatch_subseq':invalidbytesequenceinUTF-8(ArgumentError)我使用rvm来管理我的ruby​​安装。我正在使用=>ruby-2.0.0-p247[x86_64]我使用bundle来管理我的gem,并且我有rb-readline(0.4.2)(人们推荐的最少

  4. ruby-on-rails - 带 Spring 锁的 Rails 4 控制台 - 2

    我正在使用Ruby2.1.1和Rails4.1.0.rc1。当执行railsc时,它被锁定了。使用Ctrl-C停止,我得到以下错误日志:~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`gets':Interruptfrom~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`verify_server_version'from~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.

  5. ruby-on-rails - openshift 上的 rails 控制台 - 2

    我将我的Rails应用程序部署到OpenShift,它运行良好,但我无法在生产服务器上运行“Rails控制台”。它给了我这个错误。我该如何解决这个问题?我尝试更新ruby​​gems,但它也给出了权限被拒绝的错误,我也无法做到。railsc错误:Warning:You'reusingRubygems1.8.24withSpring.UpgradetoatleastRubygems2.1.0andrun`gempristine--all`forbetterstartupperformance./opt/rh/ruby193/root/usr/share/rubygems/rubygems

  6. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  7. ruby-on-rails - 在 Rails 控制台中使用 asset_path - 2

    在我的Character模型中,我添加了:字符.rbbefore_savedoself.profile_picture_url=asset_path('icon.png')end但是,对于数据库中已存在的所有角色,它们的profile_picture_url为nil。因此,我想进入控制台并遍历所有这些并进行设置。在我试过的控制台中:Character.find_eachdo|c|c.profile_picture_url=asset_path('icon.png')end但这给出了错误:NoMethodError:undefinedmethod`asset_path'formain:O

  8. ruby - 无法在 Ruby 中将 ffmpeg 作为子进程运行 - 2

    我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope

  9. Ruby 守护进程导致 ActiveRecord 记录器 IOError - 2

    我目前正在用Ruby编写一个项目,它使用ActiveRecordgem进行数据库交互,我正在尝试使用ActiveRecord::Base.logger记录所有数据库事件具有以下代码的属性ActiveRecord::Base.logger=Logger.new(File.open('logs/database.log','a'))这适用于迁移等(出于某种原因似乎需要启用日志记录,因为它在禁用时会出现NilClass错误)但是当我尝试运行包含调用ActiveRecord对象的线程守护程序的项目时脚本失败并出现以下错误/System/Library/Frameworks/Ruby.frame

  10. ruby-on-rails - 带有 Pry 的 Rails 控制台 - 2

    当我进入Rails控制台时,我已将pry设置为加载代替irb。我找不到该页面或不记得如何将其恢复为默认行为,因为它似乎干扰了我的Rubymine调试器。有什么建议吗? 最佳答案 我刚发现问题,pry-railsgem。忘记了它的目的是让“railsconsole”打开pry。 关于ruby-on-rails-带有Pry的Rails控制台,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/question

随机推荐