欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux中进程的创建、进程的终止、进程的等待、进程的程序替换

程序员文章站 2024-03-23 21:44:16
...

进程的创建

在进程的创建中,我们一个非常重要的函数 fork()函数,fork()函数会创建一个新的进程,为原有进程的子进程,原有就为父进程。
我们来看一下fork()函数的原型。
Linux中进程的创建、进程的终止、进程的等待、进程的程序替换

#include <unistd.h>
pid_t fork(void);

返回值:子进程返回0,父进程返回子进程的pid。fork()失败返回-1(linux下)

进程调用fork()函数后具体的操作

1)当进程调用fork()函数后,会将控制转移到内核中的fork()代码。
2)内核会分配新的内存和内核数据结构给子进程
3)将父进程部分数据结构内容拷贝到子进程中(写时拷贝)
4)添加子进程到系统的进程列表中
5)fork()返回,开始调度器调度
第一步:
Linux中进程的创建、进程的终止、进程的等待、进程的程序替换
调用fork()函数后到内核中去申请资源。当资源申请好就创建出子进程,子进程和父进程各自拥有独立的资源,然后把父进程的数据代码拷贝一份到子进程中。但是要注意的是:这里的拷贝是采用了写时拷贝。通常情况下,父子进程是共享代码的,数据是写时拷贝。
当有了子进程时,代码是怎么执行的。
Linux中进程的创建、进程的终止、进程的等待、进程的程序替换
当fork()完了子进程和父进程就从fork()函数开始向下执行,但是父进程先执行还是子进程先执行,这是取决去调度器。

虚拟地址空间简述

我们在写程序的时候每次取地址或者传指针等,这些地址都是操作系统为了物理内存更为合理的使用,虚拟出地址空间。通过虚拟地址空间。
1)能有效的分配和使用物理内存
2)能保护进程与进程之间的独立
3)同时一定程度上提高了运行速度
我们了解一下,虚拟地址与物理地址
Linux中进程的创建、进程的终止、进程的等待、进程的程序替换
fork()创建子进程通常在一个进程下需要做别的事情是时,可以创建一个子进程去做,自己来处理结果。例如客户端请求父进程接受,子进程去处理。
创建子进程势失败的原因
1)系统中子进程数太多达到上限
2)内存不足

vfork()函数

vfork()函数也是用来创建子进程,但是与fork()相比还是有一定的差别 。

  • vfork用于创建一个子进程,而子进程和父进程共享地址空间(也就是共享页表),而fork的子进程具有独立的地址空间
  • vfork是一定保证了子进程先执行,当子进程调用exec或者(_exit)之后父进程才能被执行。fork父子进程调度完全取决于调度器,在同一时刻。
  • 当子进程改变程序中数据时候,父进程也会改变。也是说明了共享地址空间
    在这里有一个我们要注意:
    在vfork函数创建的子进程中,为什么用return程序会崩溃。
    前面我们说的第一点,因为vfork是父子共用一块地址空间,也就是用一个页表,所以在子进程运行的过程中父进程是处于等待状态的,要是在子进程中return,那么也就是将main函数的栈帧结束了,那么当子进程结束后,父进程开始运行,但是这个时候main函数的栈帧都已经释放,所以父进程再运行就会崩溃。

    进程的终止

    进程的终止是一个进程的结束或者说是一个进程的退出,进程的退出可以分为两种。一种是是正常的退出,一种是异常的退出。

    正常退出

    进程的正常退出,大概有三种,严格的说只有两种。
    1)main函数的返回
    2)调用系统函数或者库函数(_exit() exit() )
    main()函数返回
    main()函数返回,这个很好理解,当main()函数执行到return 0 时候进程也正常退出。进程也就结束。就像我们写的第一个程序hello world ,当执行程序时,就会创建出一个进程,当return后,进程也就结束。
    _exit()函数
    先看函数声明:

    
    #include<unistd.h>
    
    // 系统调用
    void _exit(int status);
    // 参数status为退出码。(注意退出码为int 但是用两个字节,后面说)

    _exit()函数是系统函数,用于程序的退出,一般在进程中调用_exit()会直接结束程序,不会做进程结束前的清理工作。
    exit()函数
    函数说明:

#include<stdlib.h>
void exit(int status);

exit函数和_exit()函数功能都是让进程退出,而exit()函数在底层也是调用了_exit函数让进程退出,但是为什么要用exit函数呢?其实exit函数在调用的时候大概分为三个步骤
1)执行用户定义的清理函数
2)关闭所有打开的流,冲涮缓冲区。
3)调用_exit()函数
举个例子

int main()
{
    printf("123");
    exit(0);
    return 0;
}
int main()
{
    printf("123");
    _exit(0);
    return 0;
}

上面两个代码的运行结果是不一样的。
前一个是有输出为123,而后面的没有输出。
这就说明exit会在结束进程前做一些关闭文件,刷新缓冲区等事情。

异常退出

异常退出就是在进程在执行在某个一指令时收到了某一个信号,将程序终止,比如我们在执行过程中用的Ctrl+c这是表示在进程执行的过程中,进程收到了一个2号信号将程序终止掉。

还有在我们写程序的时候总会出现程序奔溃的情况,那就我们拿内存访问越界来说。当我们要访问某个地址空间的时候,因为我们现代的内存空间都用的是虚拟地址空间,所以操作系统会去查页表,然后找到对应的物理内存块,这时会有个mmu地址映射检查,如果映射在自己的内存空间中,则正常访问,否则,将发出11号信号,将程序终止掉。

进程的等待

什么是进程等待?为什么要进程等待?
进程的等待,因为一个进程在为了在它结束前需要等其它(子进程)进程完成一些事情,但是事情又没有完成,所以要进入等待状态,要等其它(子进程)完成,它才能继续执行或者结束。
为什么要进程等待,这个就比如,一个进程在创建出一个子进程后为了不让子进程变成僵尸进程,所以要进行进程的等待,等待接受子进程返回的结果。

那么进程中等待是怎么实现的?

wait方法

首先我们先看看wait方法

#include<sys/wait.h>
pid_t wait(int* status);

返回值:成功返回等待进程的pid,失败则返回-1
参数为输出型参数,如果不需要知道则可以传入NULL

wait 为阻塞式等待,意思就是如果一个子进程在没有结束前父进程都会在wait这里等着子进程的返回信息。

waitpid方法

先来看看函数的原型:

#include<sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);

返回值

有三种情况:
1)当waitpid正常返回时,返回子进程的ID。
2)当参数中options的参数被设置为WNOHANG,而调用中waitpid发现没有已经退出的子进程可以收集,那么就返回0。
3)如果在调用中出错,那么返回-1,这是errorn会被置成相应的错误信息。

参数

pid:
pid值 = -1时,waitpid等待是任意一个子进程,与wait等待类似。
pid值 > 0 时,waitpid等待的子进程的ID(也就是进程pid)与参数pid相等的进程。

status:
参数为输出型参数。
注意:后面讲的输出型参数解析里面,参数虽然是int型,但是在用的时候只用两个字节的内容。所以为了方便使用,定义了两个宏。

WIFEXITED(status):若为正常终止的子进程返回的状态,则为真(非0),否则(0)。(主要用于判断子进程是否正常退出)解释:判断正常退出是判断低int的低15位是否全为0,而退出码是在高两个字节里面存储

WEXITSTATUS(status):·若WIFEXITED判断为非零(即子进程正常退出),那么它就可以提取正常子进程退出的退出码。

options
参数里面如果是0,那么就想wait一样是阻塞式等待,如果是WNOHANG,那么就是非阻塞式等待。
阻塞式等待:就是当子进程创建出来后,父进程在执行wait或者waitpid (-1,status,0),这时候如果子进程还在执行没有退出,那么父进程就会进入睡眠状态不会执行其他任务,等待子进程退出。
非阻塞式等待:当子进程创建出来后,父进程需要知道子进程的结果,调用waitpid(-1,status,WNOHANG)后,那么如果父进程发现子进程孩还在执行时,父进程不会进入睡眠状态,而是去执行其他事情,每过一段时间回来看看子进程是否执行完成,如果完成回收信息,没有则继续去做自己的事情。

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

int main()
{
    pid_t id = fork();
    if (id > 0)
    {   
        // 父进程
        int s = 0;
        do  
        {   
            int ret = waitpid(-1, &s, WNOHANG);
            // 判断子进程是否正常退出
            // 并且返回的是要处理的子进程
            if (WIFEXITED(s) && ret == id) 
            {   
                printf("eixt coid :%d", WEXITSTATUS(s));
                break;
            }
            else if (ret == 0)
            {
                // 程序还在执行
                sleep(1);
                printf("running\n");
            }
            else
            {
                // 等待失败
                perror("waitpid error\n");
                break;
            }
        }while(1);
    }
    else if (id == 0)
    {
        // 子进程
        sleep(10);
        printf("child over");
        exit(123);
    }
    else
    {
        perror("fork\n");
    }                                                                                                                     
    return 0;
}

获取进程的status

关于status的参数解释:

  • 在wait(int status)和waitpid(int status) 中的status是一个int的输出型参数,由操作系统来进程填充。
  • 如果传递的是NULL,表示不关心子进程的退出状态。
  • 改参数为输出型参数,但是参数具体由操作系统填充,所以操作系统会根据改参数,将子进程的退出信息反馈给父进程。
  • 最重要的一点: status虽然是int型的参数,但是具体用,却只用低两个字节,就相当于位图。

首先我们说明为什么要低两个字节?
是因为,我们要表示正常状态,并且要表示出它的退出状态码,或者被信号所截杀的终止信号码。所以采用int只用低两位字节,一个字节表是进程是否正常退出,一个表示退出码。
我们用图来更清楚的认识一下:
Linux中进程的创建、进程的终止、进程的等待、进程的程序替换
我们再来理解一段代码:

#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if (id > 0)
    {
        // 父
        int w = 0;
        // 如果取成功就返回子进程的pid
        int ret = wait(&w);
        if (ret > 0 && (w & 0x7f == 0))
        {
            // 正常退出
            printf("子进程正常退出的退出码 = %d\n",  w>>8);
        }
        else if (ret > 0)
        {
            // 异常退出
            printf("异常终止的信号 = %d", w & 0x7f) // 只用后15个比特位
        }
        else
        {
            perror("wait");
        }
    }
    else if (id == 0)
    {
        // 子
        sleep(10); // 先睡10秒
        exit(0);
    }
    else
    {
        perror("fork()");
    }
    return 0;
}

通过上面的代码我们可以得出,如果正常退出,我们就可以得到退出码,如果异常退出,我们就可以得到异常退出的信号。

进程的程序替换

首先要说明的是进程的程序替换是在程序运行过程中,当执行到exec函数时候,该进程的跑去执行其它的程序,就比如说:我们在用linux时候,我们会输入一个ls来查看当前目录底下有那些内容,那么对于我们来看是一瞬间的事情,但是在输入ls时,系统创建一个进程,用来调用ls的可执行程序,那么这个就是程序的替换。

替换的原理

从原理上来看我们程序替换,而不是进程的替换,那么我们就需要用当前进程去创建出子进程来进行程序的替换,往往子进程用来调用一个程序替换的函数,从而程序的子进程跑去执行,而父进程就处于等待状态,所以进程的程序替换不会改变进程pid。
我们用图来解释一下:
Linux中进程的创建、进程的终止、进程的等待、进程的程序替换
进程替换中需要注意
1)进程的替换只是替换了进程的代码数据,被替换的进程的id不会改变
2)进程替换在调用exec成功后,后面的代码将不会被执行到
3)在进程替换后,堆栈会清空原来进程的数据。

了解进程替换的函数

进程的替换函数以exec开头的家族:
它们的都在头文件为:

#incldue <unistd.h>

大致可以划分为两种类型,一种是参数列表,一种是数组型。
先来看看参数列表exec函数

// path为要执行的程序的路径,arg为参数列表(注意参数列表以NULL结尾)
int execl(const char* path, const char* arg,···);

// 这是自动的去环境变量中找path,file为可执行程序,arg参数列表
int execlp(const char* file, const char* arg, ···);

// path为执行程序路径,arg参数列表,envp为自己需要配置环境变量
int execle(const char* path,const char* arg,···,char* const envp[]);

用数组来装参数的exec函数

// path为执行程序的路径,argv数组为命令行参数
int execv(const char* path, char* const argv[]);

// file为执行程序名字,路径自动查找环境变量中的路径
int execvp(const char* file, char* const argv[]);

// path为可执行程序路径,argv装有命令行参数的数组,envp为需要自己来配置的环境变量。
int execve(const char* path, char* const argv[], char* const envp[]);

实现一个简单的shell

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

// 这是从命令行获取的字符串进行切分,装入argv数组中
void Do_Split(char buf[], char* argv[])
{
    char* token = strtok(buf, " ");
    argv[0] = token;
    int i = 1;
    while (token != NULL)
    {
        token = strtok(NULL, " ");
        argv[i++] = token;
    }
    argv[i] = NULL;
}

// 这是创建出一个子进程,进行程序替换
void Do_Execute(char* argv[])
{
    pid_t id = fork();
    if (id > 0)
    {
        // father
        wait(NULL);
    }
    else if (id == 0)
    {
        // child
        execvp(argv[0], argv);
        perror(argv[0]);
        printf("替换错误 \n");
        exit(1);
    }
    else
    {
        perror("fork\n");
    }
}

int main(int argc, char* argv[], char* env[])
{
    (void)argc;
    (void)env;
    char buf[1024] = {};
    while (1)
    {
        char arr[50] = {};
        gethostname(arr, 9);
        printf("[aaa@qq.com%s]$ ",arr);
        /* gets(buf); */
        scanf("%[^\n]%*c",buf);
        Do_Split(buf,argv);
        Do_Execute(argv);
    }
    return 0;
}