0
点赞
收藏
分享

微信扫一扫

进程控制详解

unadlib 2022-03-27 阅读 73

进程创建

fork函数初始

fork函数是从一个已存在的进程中创建一个新进程,原进程为父进程。

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

当一个进程调用fork之后,就有两个代码相同的进程且都运行到相同位置,但两个进程将在fork之后分离,开始它们自己的旅程。

例:

int main(void)
{
    pid_t pid;
    printf("Before: pid is %d\n", getpid());
    if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
    printf("After:pid is %d, fork return %d\n", getpid(), pid);
    sleep(1);
    return 0;
}

运行结果:

                                ​​​​​​​        

 

这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示

所以fork之前父进程独立执行,fork之后父子两个执行流分别执行。注意,fork之后谁先执行完全由调度器决定。

写时拷贝

通常操作系统为了节省空间,代码加载到内存中是只读的,所以父子进程代码是共享的。由于进程的独立性,所以数据是各自独立的,但是如果在创建子进程时就把数据完全拷贝过去的话,会存在有之后才会用到的数据和根本不用的数据也会拷贝过去存在空间浪费,所以操作系统做了一件写时拷贝东西。

写时拷贝:当父子进程任意一方试图写入数据时,那么就会将要写入的部分以写时拷贝的方式各自一份。

 

fork常规法

fork调用失败的原因

进程终止

进程退出场景

进程常见退出方式

正常退出(echo $? 可以查看退出码)

异常退出

异常退出退出码也就没有意义了

_exit函数

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?发现返回值是255。

exit函数

exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

exit、_exit、return退出的区别

exit与return退出是一样的,但是不论程序在哪调用exit进程都会直接退出,return如果在main函数之外的函数调用的话进程不会退出,而是返回一个值给它的调用。

exit与_exit也是基本一样的,区别就是exit会完成进程的收尾工作包括输入缓冲区刷新;__exit 强制终止进程,不会进行进程的后续收尾工作。

例:

int main()
{
    printf("hello");
    exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
hello

int main()
{
    printf("hello");
     _exit(0);
}
运行结果:
[root@localhost linux]# ./a.out

进程退出操作系统都做了什么

系统层面,少了一个进程,会释放相关的PCB,虚拟地址空间(mm_struct),页表和各种映射关系,代码和数据在内存中申请的空间也要释放掉。

在思考一个问题进程退出码是给谁的,退出码返回值代表了什么?引出进程等待

进程等待

进程等待的必要性

进程等待的方法

wait方法

头文件:#include<sys/types.h>、#include<sys/wait.h>

函数原型:pid_t wait(int*status);

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

参数:status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL,在下面的wiatpid中讲解。

例:

#include <iostream>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
​
using namespace std;
​
​
int main()
{
  pid_t id = fork();
  if(id == 0){
    int count = 0;
    while(1){
      sleep(1);
      cout <<"child...:" << count << endl;
      if(count >= 5){
        break;
      }
      count++;
    }
    exit(0);
  }
  else{
    cout <<"father before..." << endl;
    wait(NULL); //阻塞等待,等待子进程退出
    cout <<"father after..." << endl;
  }
​
​
  cout << "hello world"<< endl;
}

waitpid方法

  • 函数原型:pid_t waitpid(pid_t pid,int *status,int options);

  • 返回值:

  • 参数:

    pid: Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID,白话就是子进程没有退出的话,父进程干自己的事,但是会重复调用waitpid方法,看子进程有没有退出。非阻塞等待。

阻塞等待的本质:其实是进程PCB被放入了等待队列,并将进程的状态设置为S状态。

返回(唤醒)的本质:进程的PCB等待队列被拿到运行队列,从而被CPU调度。

注意:

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

  • 进程退出并不会立即释放进程相关数据结构,而是等待父进程对其进行回收。

  • 子进程的退出信息会保存在它的PCB中,最终会被父进程的status输出性参数取走,从而父进程获取子进程的退出信息。

  • bash是命令行启动的所有进程的父进程!

  • bash一定是通过wait()方法得到子进程的退出信息,所以我们可以通过echo $?可以查出子进程的退出码。

  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

  • 如果不存在该子进程,则立即出错返回。

获取子进程status

 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<errno.h>
​
int main(void)
{
    pid_t pid;
    
    if((pid=fork())==-1)
        perror("fork"),exit(1);
    if(pid==0)
    {
        sleep(5);
        exit(10);
    }
    else{
        int st;
        int ret=wait(&st);
        
        if(ret>0&&(st&0x7F)==0)
        {
            //正常退出
            printf("child exit code:%d\n",(st>>8)&0xFF);
            //注意这块是为了方便理解而&0xFF的,通常我们自己使用WEXITSTATUS(status)获取子进程的退出码,WIFEXITED(status)判断进程是否正常退出。
        }
        else if(ret>0)
        {
            //异常退出
            printf("sig code :%d\n",st&0x7F);
        }
    }
}

进程程序替换

怎么理解程序替换

创建子进程就是为了执行父进程交给他的任务,之前我们都是通过if else让子进程执行父进程代码的一部分;但是如果我们不想让子进程执行父进程的一部分,而是想让它执行一个全新的代码呢?下面引入进程程序替换的概念。

​ 程序替换的本质就是将硬盘中的代码和数据拷贝一份来替换掉进程之前的全部代码(写时拷贝),这个过程并不会创建新进程。

相关的替换函数

理解:

  • l(list):表示函数采用参数列表

  • v(vector):参数用数组

  • p(path):有p自动搜索环境变量path

  • e(env):表示自己维护环境变量

函数理解

execl函数

参数解释

​ char* path:你想要替换可执行程序文件的文件路径。

​ char* arg:你想要在命令行怎么调用替换后的可执行程序。

​ ... :可变参数,比如你替换的是ls命令,那么...就是在ls命令的各种参数,你可以在后面啥也不跟,也可以将参数都跟上,这就是可变参数的作用。

接下来我们通过一个例子来了解execl函数​​​​​​​

#include<stdio.h>
#include<stdlid.h>
#include<unostd.h>
int main()
{
    printf("execl之前\n");
    int ret=execl("./hello","./hello",NULL);
    printf("%d\n",ret);
    return 0;
}
​
运行结果:
    execl之前
    hello world

可以看出在这块并没有打印ret的值,这是因为execl函数执行后已经用hello的可执行程序替换了原有程序,那么有些人就会好奇到底是替换了全部程序还是只替换了execl函数之后的程序呢?答案是替换了全部的程序,之所以会打印“execl执行之前”这个语句是因为还没有运行到execl。ret是在程序替换失败后才会打印出来的,在替换成功时打印ret的语句会被覆盖所以就不会打印。

execv函数

参数解释:

​ char* path :你想要替换可执行程序文件的文件路径。

​ char* argv[]: 参数数组。

例如:

#include<stdio.h>
#include<sdtlib.h>
#include<unistd.h>
​
int main()
{
    char* argv[]={
        "ls","-l","-t","-r",NULL
    };
    execv("/bin/ls",argv);
    return 0;
}

这个函数本质与execl没有差别,只是execl函数参数是以列表形式传参的,而execv是以数组方式传参的。

execle函数

参数解释:

​ char* path:你想要替换可执行程序文件的文件路径,所谓的环境变量。

​ char* arg:你想要在命令行怎么调用替换后的可执行程序

​ ... :可变参数,比如你替换的是ls命令,那么...就是在ls命令的各种参数,你可以在后面啥也不跟,也可以将参数都跟上,这就是可变参数的作用。

​ char* envp[]:假如这块是用子进程替换父进程。利用子进程里面打印环境变量,然后父进程结合子进程来执行,运行子进程就会有很多环境变量(由于子进程继承了bash的环境变量),再执行父进程就只有我们自己写的环境变量,这是因为execle函数替换时自己构造环境变量并不继承原有的环境变量

例如:

//hello.c文件
#include<stdio.h>
int main(int argc,char* argv[],char* env[])
{
    (void)argc;
    (void)argv;
    size_t i=0;
    for(;env[i]!=NULL;i++)
    {
        printf("%s\n",env[i]);
    }
    return 0;
}
​
//exec.c文件
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
    const char* env[]={"hehehe",NULL};
    execle("./hello","hello",NULL,env);
    return 0;
}

运行后你会发现环境变量变为hehehe。

execve函数差别不大自己结合v,l差别推导

execlp函数

参数解释:

​ char* file: 你要是使用那个程序文件替换当前文件的文件名。

​ char* arg:你想要在命令行怎么调用替换后的可执行程序

​ ... :可变参数,比如你替换的是ls命令,那么...就是在ls命令的各种参数,你可以在后面啥也不跟,也可以将参数都跟上,这就是可变参数的作用。

例如:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
​
int main()
{
    execlp("ls","ls","-l","-t",NULL);
    return 0;
}

execvp函数自己推导,这里就不做解释了。

exec函数族例子

其中只有execve是系统调用 。

实例(miniShell)

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
​
#define SIZE 256
#define NUM 16
​
int main()
{
    char cmd[SIZE];
    const char* cmd_line="[temp@VM-0-3-centos linux]#";
    while(1)
    {
        cmd[0]=0;
        printf("%s",cmd_line);
        fgets(cmd,SIZE,stdin);
        cmd[strlen(cmd)-1]='\0';  //注意这个是必须的,不加\0的话会出错。
        char *args[NUM];
        args[0]=strtok(cmd," ");
        int i=1;
        do{
            args[i]=strtok(NULL," ");
            if(args[i]==NULL)
                break;
            ++i;
        }while(1);
        //接下来创建子进程,让子进程完成解析工作
        pid_t id=fork();
        if(id<0){
            perror("fork error\n");
            continue;
        }
        //子进程
        if(id==0){
            execvp(args[0],args);
            exit(1);
        }
        //获取退出码
        int status=0;
        pid_t ret =waitpid(id,&status,0);
        if(ret>0){
            printf("status code: %d\n",(status>>8)&0xff);
        }
    }
    return 0;
}

举报

相关推荐

0 条评论