一个进程的一生

关键词助手:进程创建,进程终止,进程等待,进程程序替换。

目录

进程控制

进程创建

fork函数

 用户空间&内核空间

写时拷贝

fork的一些简单特性

进程终止

正常终止

exit函数与_exit函数

进程等待

进程等待的必要性

 wait函数

waitpid函数

 子进程退出状态信息

 进程程序替换

为什么要进行进程程序替换

进程程序替换的原理

 exec函数簇


进程控制

进程创建

fork函数

父子进程

从一个已存在进程中创建一个新进程。新的进程为子进程,而原进程为父进程。

创建子进程的方法——fork函数

fork用于创建一个新的进程,调用fork()的进程被称为父进程,而被fork()创建的进程被称为子进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的程序计数器,相同的CPU寄存器,在父进程中使用的相同打开文件。

#include 
#include 

pid_t fork(void)    //pid_t为宏定义,实质为int

//创建成功,返回值有两个,子进程中返回0,父进程中返回子进程pid
//创建失败,返回-1

fork内部完成的事情:

创建了子进程,子进程拷贝父进程PCB的大部分内容。

其大致过程如下:

1.分配新的内存块和内核数据结构(task_struct)给子进程

2.将父进程的部分数据结构拷贝给子进程

3.添加子进程到系统进程列表中——添加到双向链表中

4.fork返回,操作系统开始调度

而父子进程之间的区别与联系,根据查阅相关资料,可以简要总结如下:(可能不太全面)

1.进程的上下文信息——所以当fork()产生了子进程后,子进程是从fork()的下一个操作开始的。

2.进程堆栈——子进程刚刚创建时,父子进程页表指向的物理内存是同一块,但当父/子进程对其内变量进行改变时,会给父子进程各拷贝一份副本,我们也将这样的操作成为写时拷贝

一个进程的一生_第1张图片

3.内存信息

4.打开的文件描述符

5.进程优先级

6.信号控制设置

7.进程组号

8.当前工作目录

9.根目录

10.资源限制

11.控制终端

父子进程的区别 

1.锁——父进程设置的锁,子进程不继承

2.进程号——本身进程号以及它们的父进程号(这当然了)

3.自己的文件描述符和目录流的拷贝

4.子进程的未决告警被清除

5.子进程的未决信号集设置为空集

……

 用户空间&内核空间

 概念简介

内核空间:Linux操作系统和驱动程序运行在内核空间。同理,由于系统调用函数是操作系统所提供的函数,所以系统调用函数都是运行在内核空间的。

用户空间:应用程序是运行在用户空间的,即我们自己写的代码及代码实现都是在用户空间进行的,只有当我们的代码涉及到系统调用函数时,程序才会切换到内核空间中运行,系统调用函数相关部分执行完毕后,便又会切换回用户空间。

我们上边所提到的fork函数就是一个系统调用函数。

一个进程的一生_第2张图片

写时拷贝

写时拷贝:

这是操作系统的一种拖延战术。父进程创建出来子进程,子进程的PCB绝大多数拷贝于父进程,页表也是从父进程拷贝过来的。所以一开始的时候,同一变量在父子进程中其虚拟地址到物理地址的映射关系是一样的,即此时操作系统并没有给子进程中的变量在物理内存中单独分出空间来存储,子进程中变量所用的还是原来父进程变量的物理地址。

当父/子进程进行‘写’操作试图改变变量的时候,才会给变量再分出一块空间,并且修改页表中的相应映射。

写实拷贝技术所突出的一个重要概念:

进程之间的数据是独有的。 

fork的一些简单特性

1.父子进程是独立运行的,进程之间相互不干扰。父子进程各自拥有一套进程地址虚拟空间于页表。

2.父子进程是抢占式执行,(并不存在什么尊老爱幼孔融让梨...),进程执行的先后本是上是由操作系统的调度决定的。

3.子进程会从父进程处拷贝程序计数器与上下文信息,故子进程是从fork之后开始运行的。

4.父子进程代码共享,数据独有。

关于fork再实际工程中的一些应用场景

1.一个父进程希望复制自己,与子进程执行不同的代码段。如,nginx。

2.守护进程。

父进程创建子进程,使子进程执行可能存在风险的代码段(真正的业务代码,一般会使用进程程序替换),父进程负责等待子进程,也可以称作守护子进程,如果子进程因为意外崩溃了,父进程负责重新启动子进程,让子进程继续执行业务。

进程终止

正常终止

这是我们首先要了解的一点,什么叫做进程的正常终止?

和人一样,人在生活中犯了大错,致命的错误,被抓走,砰,没了,这叫崩溃,非正常终止。相反,一辈子安安稳稳的,风平浪静,寿终正寝,这叫正常终止。对进程来说,如果在运行过程中,出了什么大BUG,或者被用户一个ctrl+c(Linux),啪,这叫异常终止,而做完自己该做的,接收到本该接收的退出信号终止了,这就是正常终止。

正常终止的情况总结

1.从main函数当中通过return返回

2.调用exit函数(库函数)

3.调用_eixt函数(系统调用函数)

exit函数与_exit函数

#include 

void exit(int status);

//库函数
//status,进程退出时的退出码
//哪个进程调用该函数,就终止哪个进程
#include 

void _exit(int status);

//系统调用函数
//status,进程的退出码
//哪个进程调用该函数,就终止哪个进程

 两者在功能描述上来看,似乎是一样的,但在实际应用中,调用两个函数可能会产生完全不一样的结果。

#include 
#include 

int main() {
    printf("hello");
    exit(0);
    printf("error");
    return 0;
}

#include 
#include 
#include 

int main() {
    printf("hello");
    _exit(0);
    printf("error");
    return 0;
}

 

ps.因为偷懒,所以直接对同一个文件进行的修改 

 可以看出,调用exit()的程序就如我们初看时预想的一般,将hello打印了出来,而调用_exit()的程序却并没有如预想的一样将hello打印出来。

导致这般结果的原因是,我们平日里所使用的printf与scanf这般输入输出函数,都是C语言所提供的库函数,为了提高程序运行的效率,C标准库定义了一个缓冲区,用来暂时存放我们准备输出的内容。

作为C标准库的一份子,exit函数自然会关照到自家定义的一些东西,会自主的将缓冲区中还未来得及输出的信息输出,并且做好一系列善后操作。(最后再调用_exit函数完成终止)

而属于系统调用函数的_exit函数就不管那么多了,直接根据操作系统的需求关闭就完事了,干脆利落的做完终止进程的任务,全然不管什么缓冲区什么标准输入,标准输出,标准错误……

一个进程的一生_第3张图片

 PS.关于我为什么没有在hello后边加上\n。那肯定是因为……\n会直接刷新缓冲区啊……

进程等待

进程等待的必要性

通过对进程基础概念的学习,我们可以知道:

如果子进程先于父进程退出,父进程没有来得及回收子进程,那么会导致子进程成为僵尸进程。僵尸进程会占用服务器资源,如果僵尸进程过多,会严重影响服务器性能。

而如果父进程先于子进程退出,会导致子进程成为孤儿进程,但这种情况并不会导致什么过大的危害,因为孤儿进程会被init进程收养,在运行完成后释放。

因此,有一种杀死僵尸进程的方法便是杀死它的父进程,但这种方法明显与我们创建父子进程的目的相悖,所以,我们就需要用到进程等待相关知识了。

 灭掉僵尸进程的最合理办法——进程等待

在父进程的程序中调用进程等待函数,进行等待子进程退出,回收子进程的退出状态信息。

 wait函数

#include 
#include 

pid_t wait(int* status);

//pid_t本质上是int
//返回值:成功后返回被等待进程的PID,失败返回-1;
//参数status,输出型参数,获取子进程的退出状态,如果不关心可以设置为NULL。
//函数特性:调用后会一直等到子进程退出,通常将这种状态称为阻塞状态。

关于参数int* status:

由于有些时候我们需要获取子进程的退出状态来进行一些处理,但是返回值已经被返回子进程pid的需求占据了,所以使用了一个指向整型变量的指针来作为参数,通过函数内改变指针指向变量的值来将子进程退出状态传出。

这是我们在C语言编程中常用的一种手法,相信诸君并不陌生。

#include 
#include 
#include 
#include 

int main() {
    int fd = fork();
    if (fd > 0) {
        printf("fd = %d\n", fd);
        printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
        while(1) {
            sleep(2);
        }
    } else {
        sleep(1);
        printf("fd = %d\n", fd);
        printf("i am child, pid: %d, ppid: %d\n", getpid(), getppid());
        exit(0);
    }
    return 0;
}

可见,如果没有使用wait()函数回收的话,子进程得状态会成为Z+,即僵尸状态。

#include 
#include 
#include 
#include 

int main() {
    int fd = fork();
    if (fd > 0) {
        printf("fd = %d\n", fd);
        printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
        pid_t pid = wait(NULL);
        printf("%d, recover\n", pid);
        while(1);
    } else {
        sleep(1);
        printf("fd = %d\n", fd);
        printf("i am child, pid: %d, ppid: %d\n", getpid(), getppid());
        exit(0);
    }
    return 0;
}

 一个进程的一生_第4张图片

 

 由以上两图可以看出,在程序中使用wait函数后,子进程在运行完毕后资源会被彻底释放,而不是以僵尸状态存在。

wait函数是典型的阻塞调用函数,所谓阻塞

即在函数发起调用之后,如果完成函数功能缺少某种资源,函数会一直等待该资源,直到资源到来,函数完成其既定功能之后,才会返回。

对应的有非阻塞函数,当一个非阻塞函数被调用时,它会自主地判断所需资源是否齐备,若是,则执行函数功能;若否,则报错返回。

与wait函数相同,可以回收子进程退出信息的函数还有waitpid函数。waitpid函数可以选择阻塞调用还是非阻塞调用。

waitpid函数

#include 
#include 

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

//pid:若pid > 0子进程id,等待id为此值的子进程退出;pid = -1,等待任意子进程退出
//status:同wait的参数status
//options:WNOHANG,设置函数为非阻塞状态,若执行到此函数时,子进程没有运行结束,则返回0,
//不予以等待,否则,返回子进程id。若要为阻塞状态,将此参数设置为0。
//options还可以设置为WUNTRACED,此类情况很少碰见(也不是很懂),在此不表。
#include 
#include 
#include 
#include 
#include 

int main() {
    int fd = fork();
    if (fd > 0) {
        printf("fd = %d\n", fd);
        printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
        int status = 0;
        int pid = waitpid(-1, &status, WNOHANG);
        printf("%d, recover\n", pid);
        if (pid > 0 && ((status & 0xff) == 0)) {
            //子进程正常退出
            printf("child return code: %d\n", (status >> 8));
        } else if (pid > 0) {
            //子进程异常退出
            printf("child receive signal: %d\n", (status & 0x7f));
        }
        while(1);
    }
    else {
        printf("fd = %d\n", fd);
        printf("i am child, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(3);
        exit(131);
    }
    return 0;
}

一个进程的一生_第5张图片

 

 可见当options设置为WNOHANG的时候,父进程并没有等待子进程,从而导致了子进程成为了僵尸进程。

#include 
#include 
#include 
#include 
#include 

int main() {
    int fd = fork();
    if (fd > 0) {
        printf("fd = %d\n", fd);
        printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
        int status = 0;
        int pid = waitpid(-1, &status, 0);
        printf("%d, recover\n", pid);
        if (pid > 0 && ((status & 0xff) == 0)) {
            //子进程正常退出
            printf("child return code: %d\n", (status >> 8));
        } else if (pid > 0) {
            //子进程异常退出
            printf("child receive signal: %d\n", (status & 0x7f));
        }
        while(1);
    }
    else {
        printf("fd = %d\n", fd);
        printf("i am child, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(3);
        exit(131);
    }
    return 0;
}

一个进程的一生_第6张图片

 当options为0时,waitpid函数起到了与wait函数一样的效果。

 子进程退出状态信息

通过以上对wait函数以及waitpid函数的描述介绍,我们已经简单知晓了它们的输出型参数status是用来传出子进程退出状态信息的,那么,子进程的退出状态信息究竟是怎么一回事?我将在以下进行一些简单的介绍:

int status

在存储子进程退出状态信息时,并没有用到该int型变量的所有字节,操作系统只取了低位的两个字节用于存储信息。在用户选择传递status的值时:

若子进程正常退出:获取到子进程的退出状态。

若子进程异常退出:获取到coredump标识位+退出信号。

 一个进程的一生_第7张图片

当子进程是正常退出时,退出码会被设置,coredump标识位为0,不会有退出信号。

当子进程异常退出是时,退出码不会被设置。coredump标识位为1,有退出信号。

所以,一般判断子进程的退出情况便是根据是否有退出信号来判断的。

子进程正常退出时的status。 

#include 
#include 
#include 
#include 

int main() {
    int fd = fork();
    if (fd > 0) {
        printf("fd = %d\n", fd);
        printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
        int status = 0;
        int pid = wait(&status);
        printf("%d, recover\n", pid);
        if (pid > 0 && ((status & 0xff) == 0)) {
            //子进程正常退出
            printf("child return code: %d\n", (status >> 8));
        } else if (pid > 0) {
            //子进程异常退出
            printf("child receive signal: %d", (status & 0x7f));
        }
        int st[16] = {0};
        for (int i = 0; i < 16; i++) {
            st[15 - i] = status % 2;
            status = status >> 1;
        }
        for (int i = 0; i < 16; i++) {
            printf("%d ", st[i]);
        }
        printf("\n");
    } else {
        printf("fd = %d\n", fd);
        printf("i am child, pid: %d, ppid: %d\n", getpid(), getppid());
        exit(131);
    }
    return 0;
}

一个进程的一生_第8张图片

当子进程异常退出时

#include 
#include 
#include 
#include 

int main() {
    int fd = fork();
    if (fd > 0) {
        printf("fd = %d\n", fd);
        printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
        int status = 0;
        int pid = wait(&status);
        printf("%d, recover\n", pid);
        if (pid > 0 && ((status & 0xff) == 0)) {
            //子进程正常退出
            printf("child return code: %d\n", (status >> 8));
        } else if (pid > 0) {
            //子进程异常退出
            printf("child receive signal: %d\n", (status & 0x7f));
        }
        int st[16] = {0};
        for (int i = 0; i < 16; i++) {
            st[15 - i] = status % 2;
            status = status >> 1;
        }
        for (int i = 0; i < 16; i++) {
            printf("%d ", st[i]);
        }
        printf("\n");
    } else {
        printf("fd = %d\n", fd);
        printf("i am child, pid: %d, ppid: %d\n", getpid(), getppid());
        int* p_null = NULL;
        *p_null = 1;
        exit(131);
    }
    return 0;
}

一个进程的一生_第9张图片

 进程程序替换

为什么要进行进程程序替换

父进程先启动,创建一个子进程,让子进程程序替换成为另外一个程序,实现不同的一个功能。

PS.以下为个人理解。

因为子进程的代码是从父进程处拷贝过来的,有时候子进程所运行的代码可能不是我们想要其实现的功能,并且某些功能我们可能要多次使用,且不好封装,这时候如果可以将子进程的代码替换为可实现目标功能的代码可以节省不少的力气,也提高了代码的可读性。

进程程序替换的原理

1.替换子进程的代码段和数据段,从磁盘加载新的代码段和数据段到物理内存。

2.用页表映射回进程虚拟地址空间的代码段、数据段,同时更新堆栈与命令行参数。

3.pid 不变、环境变量不变。

 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, const char* argv[]);
int execvp(const char* file, char* const argv[])
int execve(const char* path, char* const argv[], char* const envp[]);

//path:带路径的可执行程序,如,/user/bin/ls
//file:不带路径,如果参数中包含/,则视为路径名,
//    否则视为无路径的程序名,在PATH环境变量的目录中搜索该程序
//arg:将新程序的每个命令行参数分别传给它,作为分隔,最后应传入一个NULL
//argv[]:字符型指针数组,每个指针指向的字符串都是一个命令行参数,
//    数组中最后一个指针应指向NULL。它就是arg的一个打包。
//envp[]:自定义的环境变量,如果你懒得写,可以extern默认的环境变量传入。

以上六个函数中,只有execve函数是系统调用函数,其他函数都是由其派生来的。 

特征字母 对应参数 特点
l const char* arg 列表形式传入(不打包)
v char* const argv[] 数组形式传入(打包)
有 p const char* file 不带路径
无 p const char* path 带路径
e char* const envp[] 自己组装环境变量

或者以下表格表示

函数名 命令行参数传入 是否带路径 默认环境变量?
execl 列表
execlp
execle 否,自己装配
execv 数组
execvp
execve 否,自己装配

一个进程的一生_第10张图片

 六个函数的应用实例

#include 
#include 
#include 
#include 

//execl
#if 0
int main() {
    pid_t fd = fork();
    if (fd > 0) {
        int* status = NULL;
        wait(status);
        return 0;
    } else {
        execl("/bin/ls","ls", "-l", "-a", NULL);
        printf("error\n");
        return 0;
    }
    return 0;
}
#endif

//execlp
#if 0
int main() {
    pid_t fd = fork();
    if (fd > 0) {
        int* status = NULL;
        wait(status);
        return 0;
    } else {
        execlp("ls", "ls", "-l", "-a", NULL);
        printf("error\n");
        return 0;
    }
    return 0;
}
#endif

//execle
#if 0
int main() {
    extern char** environ;

    pid_t fd = fork();
    if (fd > 0) {
        int* status = NULL;
        wait(status);
        return 0;
    } else {
        execle("/bin/ls", "ls", "-l", "-a", NULL, environ);
        printf("error\n");
        return 0;
    }
    return 0;
}
#endif

//execv
#if 0
int main() {
    char* const arg[] = {"ls", "-l", "-a", NULL};

    pid_t fd = fork();
    if (fd > 0) {
        int* status = NULL;
        wait(status);
        return 0;
    } else {
        execvp("/bin/ls", arg);
        printf("error\n");
        return 0;
    }
    return 0;
}
#endif

//execvp
#if 0
int main() {
    char* const arg[] = {"ls", "-l", "-a", NULL};

    pid_t fd = fork();
    if (fd > 0) {
        int* status = NULL;
        wait(status);
        return 0;
    } else {
        execvp("ls", arg);
        printf("error\n");
        return 0;
    }
    return 0;
}
#endif

//execve
#if 1
int main() {
    char* const arg[] = {"ls", "-l", "-a", NULL};
    extern char** environ;

    pid_t fd = fork();
    if (fd > 0) {
        int* status = NULL;
        wait(status);
        return 0;
    } else {
        execve("/bin/ls", arg, environ);
        printf("error\n");
        return 0;
    }
    return 0;
}
#endif

 一个进程的一生_第11张图片

 

你可能感兴趣的:(笔记,Linux基础学习,linux)