Linux进程控制

Posted 野猪佩奇`

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程控制相关的知识,希望对你有一定的参考价值。

文章目录

一、进程创建

1、再谈 fork 函数

我们在 进程概念与进程状态 中对 fork 函数进行了初步的介绍与使用,在这里我们来详细的学习一下 fork 函数;fork 是 Linux 中非常重要的一个系统调用函数,它用于在当前进程下创建一个新的进程,新进程是当前进程的子进程;我们可以 man 2号手册来查看 fork 函数:

头文件:unistd.h
    
函数原型:pid_t fork(void)
    
函数功能:创建一个子进程
    
函数返回值:创建成功 -- 给父进程返回子进程的pid,给子进程返回0;创建失败 -- 给父进程返回-1,没有子进程被创建
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() 
    pid_t id = fork();  //创建子进程
    if(id == -1) 
        printf("fork fail\\n");
        return -1;
     else if (id == 0)   //子进程
        while(1) 
            printf("子进程, pid:%d, ppid:%d, id:%d\\n", getpid(), getppid(), id);
            sleep(1);
         
     else   //父进程 
        while(1) 
            printf("父进程, pid:%d, ppid:%d, id:%d\\n", getpid(), getppid(), id);
            sleep(1);
         
    
    return 0;

tips:我们在编写 makefile 的时候,目标文件的依赖方法中,可以用 “$@” 表示要形成的目标文件,即依赖关系中 “:” 左边的内容;用 “$^” 表示目标文件的依赖文件,即依赖关系中 “:” 右边的内容。

2、fork 函数返回值

学过 C/C++ 的时候同学都知道,一个函数最多只能有一个返回值,那么我们应该如何理解 fork 函数有两个返回值呢?

首先,fork 函数是一个系统调用,即 fork 函数是操作系统为我们提供的一个操作接口,那么 fork 函数肯定也是由操作系统实现的;所以当我们调用 fork 函数时,其实是 OS 在帮我们创建子进程;

其次,一个函数在正常执行的情况下,函数 return 之前函数的主体功能肯定已经被执行完了;对于 fork 函数来说,fork 函数的作用是创建子进程,所以 fork 在 return 之前就已经创建了子进程,那么此时就存在两个进程;

既然存在两个进程,那么 fork 自然也就会被返回两次 – 每一个进程都要 return,所以 fork 函数有两个返回值。

另外,为什么 fork 给父进程返回子进程的 pid,而给子进程返回0呢? – 因为一个父进程可能有多个子进程,而一个子进程只能有一个父进程,父进程需要子进程的 pid 来判别不同的子进程,而子进程则不需要判别父进程。

3、写时拷贝

在上一节 进程地址空间 中我们写了如下程序:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int g_val = 100;

int main() 
    int id = fork();
    if(id < 0) 
        perror("fork fail");
        return 1;
     else if(id == 0) 
        int cnt = 0;
        while(1) 
            if(cnt == 5) 
                g_val = 200;
                printf("子进程已经修改了全局变量...........................\\n");
            
            cnt++;
            printf("我是子进程,pid:%d, ppid:%d, g_val:%d, &g_val:%p\\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        
     else 
        while(1) 
            printf("我是父进程,pid:%d, ppid:%d, g_val:%d, &g_val:%p\\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        
    
    return 0;

我们发现,子进程和父进程中 g_val 变量的地址相同,但是值却不相同;我们现在知道 – OS会为每一个进程都创建一个进程地址空间以及页表,然后将通过页表将地址空间映射到物理内存;

对于父子进程来说,父进程和子进程共享代码和数据,但是为了保证进程的独立性,当其中一方想要修改数据时,就会发生 写时拷贝 – OS 会在物理内存中重新开辟一块空间,然后将原空间中的数据拷贝都新空间,再修改页表映射关系,最后再让进程修改对应的数据;

所以虽然表面上父子进程 g_val 的地址相同,但这只是虚拟地址相同,而物理地址并不相同,所以父子进程 g_val 的值也能够不同;对于接受 fork 返回值的变量 id 来说也一样,先进行 return 的进程会对 id 进行写时拷贝,所以对于父子进程来说,id 的值不同:

4、fork 常规用法

fork 一般应用于一下两种场景:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段;例如,父进程等待客户端请求,生成子进程来处理请求;我们前面使用的 fork 都属于这种情况。
  • 一个进程要创建子进程来执行一个不同的程序;例如子进程从 fork 返回后,调用 exec 系列函数;这是我们下面要重点学习的内容。

5、fork 调用失败原因

如下两种原因可能会导致 fork 调用失败:

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

我们可以写一个死循环创建进程的程序来测试我们当前OS最多能创建多少个进程:

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

int main() 
    int cnt = 0;
    while(1) 
        int id = fork();
        if(id < 0)   //进程创建失败
            printf("fork fail, cnt:%d\\n", cnt);
            break;
         else if(id == 0)   //子进程
            printf("子进程持续创建中...\\n");
        
        cnt++;
    
    return 0;

注:运行上面这个程序可能导致服务器或者虚拟机直接挂掉,虚拟机直接 shutdown 关机重启即可;服务器则需要到对应的服务器控制台进行重启。


二、进程终止

1、进程退出码

我们运行一个进程是为了让该进程完成某一项任务,而既然是完成任务,就需要对任务执行结果的正确性进行标定;进程退出码的作用就是就是标定一个进程执行结果是否正确,不同的退出码表示不同的执行结果,一般来说:

  • 0表示进程运行结果正确;
  • 非0表示运行结果错误;

对于非0来说,不同的数字有又对应着不同的错误,我们可以自己设定不同退出码所对应的错误信息,也可以使用系统提供的退出码映射关系:

在 Linux 中,存在一个变量 “?” – 该变量中始终保存着最近一个进程执行完成时的退出码,我们可以使用 “echo $?” 来查看最近一个进程的退出码:

注:由于 echo 本身也是一个可执行程序,我们使用 echo 查看 ? 时 echo 也会被运行,所以我们后面再次查看 $? 时,得到的结果是0。

2、进程退出的情况

进程退出时一共有如下三种情景:

  • 代码运行完毕且结果正确 – 此时退出码为0;
  • 代码运行完毕且结果不正确 – 此时退出码为非0;
  • 代码异常终止 – 此时退出码无意义。

3、进程退出的方法

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

  • main 函数 return 返回;
  • 调用 exit 终止程序;
  • 调用 _exit 终止程序。

我们平时接触最多的就是通过 main 函数 return 返回来退出进程,但其实我们也可以通过库函数 exit 和系统调用 _exit 来直接终止进程;

库函数 exit

头文件:stdlib.h
    
函数原型:void exit(int status);

status:status 定义了进程的终止状态,父进程通过wait来获取该值
    
函数功能:终止进程

可以看到,exit 会将我们的进程直接终止,无论程序代码是否执行完毕。

系统调用 _exit

头文件:unistd.h
    
函数原型:void _exit(int status);

status:status 定义了进程的终止状态,父进程通过wait来获取该值
    
函数功能:终止进程

exit 和 _exit 的区别

关于 exit 和 _exit 的区别和联系,我们以一个例子说明:

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

int main() 
    printf("hello linux");
    exit(1);
    //_exit(1);
    printf("process is done...\\n");
    return 0;

首先,由于 exit 是C语言库函数,而 _exit 是系统调用,所以可以肯定的是 exit 的底层是 _exit 函数,exit 是 _exit 的封装

其次,由于计算机体系结构的限制,CPU之和内存交互,所以数据会先被写入到缓冲区,待缓冲区刷新时才被打印到显示器上;而在上面的程序中,我们没用使用 ‘\\n’ 进行行缓冲的刷新,可以看到,exit 最后打印了 “hello linux”,而 _exit 什么都没有打印;所以 exit 在终止程序后会刷新缓冲区,而 _exit 终止程序后不会刷新缓冲区

最后,由于 exit 的底层是 _exit,而 _exit 并不会刷新缓冲区,也可以反映出 缓冲区不在操作系统内部,而是在用户空间

上面三种进程退出的方法都是正常退出,但除了正常退出,进程也会异常退出,比如 Ctrl C 终止进程,或者程序中遇到除0,野指针,空指针解引用等问题:


三、进程等待

1、为什么要进行进程等待

我们创建一个进程的目的是为了让其帮我们完成某种任务,而既然是完成任务,进程在结束前就应该返回任务执行的结果,供父进程或者操作系统读取。

所以,一个进程在退出的时候,不能立即释放全部资源 – 对于进程的代码和数据,操作系统可以释放,因为该进程已经不会再被执行了,但是该进程的PCB应该保留,因为PCB中存放着该进程的各种状态代码,特别是退出状态代码。

对于父子进程来说,当子进程退出后,如果父进程不对子进程的退出状态进行读取,那么子进程就会变成 “僵尸进程”;而进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼” 的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程;进而就会造成内存泄漏;

所以,我们需要父进程对子进程进行 进程等待,获取子进程的退出信息,并让操作系统回收子进程资源 (释放子进程的 PCB)。

进程等待的本质

我们知道,子进程的退出信息是存放在子进程的 task_struct 中的,所以进程等待的本质就是从子进程 task_struct 中读取退出信息,然后保存到相应变量中去:

2、如何进行进程等待

在 Linux 下,我们一般通过以下两种系统调用来进行进程等待:

pid_t wait(int *status);

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

wait 和 waitpid 都可以获取子进程的退出信息,并让操作系统回收子进程资源 (释放子进程的 PCB);

status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;

options:指定父进程等待方式 – 阻塞式等待和非阻塞式等待;

(1)、wait 系统调用

我们可以通过 wait 系统调用来进行进程等待:

头文件:sys/types.h	 sys/wait.h
    
函数原型:pid_t wait(int *status);

status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;
    
返回值:成功返回被等待进程的pid,失败返回-1;

下面我们举例来演示 wait 的使用:

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

int main() 
    int id = fork();
    if(id == -1) 
        printf("fork error\\n");
        exit(-1);
     else if(id == 0)   //子进程
        int cnt = 5;
        while(cnt--) 
            printf("子进程, pid:%d, ppid:%d, cnt:%d\\n", getpid(), getppid(), cnt);
            sleep(1);
        
        exit(1);
     else   //父进程
        sleep(10);
        int status = 0;
        pid_t ret = wait(&status);
        if(ret == -1) 
            printf("wait fail\\n");
            exit(1);
         else 
            printf("wait success\\n");
        
        printf("exit code:%d\\n", status);
    
    return 0;

我们可以通过一个监控脚本来检测子进程从创建到终止到被父进程回收的过程:

while :; do ps axj | head -1 && ps axj | grep process | grep -v grep; sleep 1; done

可以看到,最开始父子进程都处于睡眠状态 S,之后子进程运行5s退出,此时由于父进程还要休眠5s,所以没有对子进程进行进程等待,所以子进程变成僵尸状态 D;5s过后,父进程使用 wait 系统调用对子进程进行进程等待,所以子进程由僵尸状态变为彻底死亡状态。

(2)、status 位图结构

在上面的例子中,子进程使用 exit 终止进程时返回的退出码是1,但是我们发现保存子进程退出信息的 status 的值非常奇怪,这是由于 status 的位图结构造成的;

wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充;

如果传递NULL,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程;

status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低16比特位):

可以看到,status 低两个字节的内容被分成了两部分 – 第个一字节前七位表示退出信号,最后一位表示 core dump 标志;第二个字节表示退出状态,退出状态即代表进程退出时的退出码;

对于正常退出的程序来说,退出信号和 core dump 标志都为0,退出状态等于退出码;对于异常终止的程序来说,退出信号为不同终止原因对应的数字,退出状态未用,无意义。

所以 status 正确的读取方法如下:

printf("exit signal:%d,  exit code:%d \\n", (status & 0x7f), (status >> 8 & 0xff)); 

其中,status 按位与上 0x7f 表示保留低七位,其余九位全部置为0,从而得到退出信号;

status 右移8位得到退出状态,再按位与上 0xff 是为了防止右移时高位补1的情况;

WIFEXITED 与 WEXITSTATUS 宏

Linux 提供了 WIFEXITED 和 WEXITSTATUS 宏 来帮助我们获取 status 中的退出状态和退出信号,而不用我们自己去按位操作:

  • WIFEXITED (status):若子进程正常退出,返回真,否则返回假;(查看进程是否是正常退出)(wait if exited)
  • WEXITSTATUS (status):若 WIFEXITED 为真,提取子进程的退出状态;(查看进程的退出码)(wait exit status)
if(WIFEXITED(status))   //正常退出
    printf("exit code:%d\\n", WEXITSTATUS(status));
 else   //异常终止
    printf("exit signal:%d\\n",WIFEXITED(status));

(3)、waitpid 系统调用

我们也可以用 waitpid 来进行进程等待:

头文件:sys/types.h	 sys/wait.h
    
函数原型:pid_t waitpid(pid_t pid, int *status, int options);

pid:Pid=-1,等待任意一个子进程,与wait等效;Pid>0.等待其进程id与pid相等的子进程;
    
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;
    
options:等待方式,options=0,阻塞等待;options=WNOHANG,非阻塞等待;
    
返回值:waitpid调用成功时返回被等待进程的pid;如果设置了WNOHANG,且waitpid发现没有已退出的子进程可收集,则返回0;调用失败则返回-1

我们还是举例来说明 waitpid 的使用:

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

int main() 
    int id = fork();
    if(id == -1) 
        printf("fork error\\n");
        exit(-1);
     else if(id == 0)   //子进程
        int cnt = 5;
        while(cnt--) 
            printf("子进程, pid:%d, ppid:%d, cnt:%d\\n", getpid(), getppid(), cnt);
            sleep(1);
        
        exit(1);
     else   //父进程
        sleep(10);
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);  //阻塞等待
        if(ret == -1) 
            printf("wait fail\\n");
            exit(1);
         else 
            printf("wait success\\n");
        
        printf("exit signal:%d,  exit code:%d\\n", (status & 0x7f), (status >> 8 & 0xff));
    
    return 0;

可以看到,waitpid 和 wait 还是有很大区别的 – waitpid 可以传递 id 来指定等待特定的子进程,也可以指定 options 来指明等待方式。

(4)、阻塞与非阻塞等待

waitpid 函数的第三个参数用于指定父进程的等待方式:

其中,options 为0代表阻塞式等待,options 为 WNOHANG 代表非阻塞式等待;

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

而非阻塞式等待则不同,当父进程执行到 waitpid 函数时,如果子进程未退出,父进程会直接读取子进程的状态并返回,然后接着执行后面的语句,不会等待子进程退出。

轮询

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

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

void task1() 
    printf("task is running...\\n");


void task2() 
    printf("task is runnning...\\n");


int main() 
    int id = fork();
    if(id == -1) 
        printf("fork error\\n");
        exit(-1);
     else if(id == 0)   //子进程
        int cnt = 5;
        while(cnt--) 
            printf("子进程, pid:%d, ppid:%d, cnt:%d\\n", getpid(), getppid(), cnt);
            sleep(1);
        
        exit(1);
     else   //父进程
        int status = 0;
        while(1)   //轮询
            pid_t ret = waitpid(id, &status, WNOHANG);  //非阻塞式等待
            if(ret == -1) 
                printf("wait fail\\n");  //调用失败
                exit(1);
             else if(ret == 0)  //调用成功,但子进程未退出
                printf("wait success, but child process not exit\\n");
                task1();  //执行其他命令
                task2();
             else   //调用成功,子进程退出
                printf("wait success, and child exited\\n");
                break;
            <

Linux-进程控制

Linux-进程控制

进程创建

进程的创建:

  1. 命令行启动命令(程序、指令……)
  2. 通过程序自身,fork出来的子进程

fork函数

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

语法:

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

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

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

注:

  • 当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方
  • fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
  • 所有fork出来的子进程,都是以父进程为模板,数据代码继承父进程
  • 创建子进程的本质是系统多了一个进程,多了一套进程相关的数据结构(以父进程为模板)


注:

  • 上端代码中的pid是变量,是父进程创建的,是父进程运行fork中的代码的
  • pid中有两个不同的值,是因为fork() return返回的时候本质是把返回值写入变量,必定会写时拷贝(注:这里的写时拷贝并不是立刻进行写时拷贝,只是写时拷贝机制(策略),只用当父子进行数据写入或者数据修改时,才进行写时拷贝),一个变量名内容是不同的本质是父子页表映射数据到了不同的内存区域
  • pid变量里有两个不同的值,可以从而让父子进入不同的业务逻辑
  • fork函数只是创建子进程的过程,创建父子进程的依旧是操作系统

补: fork创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

fork函数返回值

  • 子进程返回0,
  • 父进程返回的是子进程的pid

fork常规用法

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

fork调用失败的原因

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

写时拷贝

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

注:

  • 写时拷贝的实现过程时,需要OS参与完成,OS时进程、内存的管理者
    总结:
  • 写时拷贝可以保证父子进程的独立性
  • 写时拷贝的意义:
    1. 并不是所有数据父子进程都会写入,不需要写入修改的数据(只读)拷贝是没有意义的,因此写时拷贝是可以避免内存和系统资源的浪费
    1. fork时,创建数据结构,如果还要将数据拷贝一份,fork函数的效率就会降低,因此写时拷贝可以提高fork的效率
    1. fork本身就是向系统索要更多的资源,如果再拷贝数据的话,索要的资源就会变多,创建子进程就很容易失败,因此写时拷贝可以提高fork创建子进程的成功率

补充:

  • Fork后子进程保留了父进程的:
  • 环境变量
  • 当前工作目录
  • 不会保留 父进程的文件锁,pending alarms和pending signals
  • 不会保留 进程号
  • fork调用通过复制父进程创建子进程,子进程与父进程运行的代码和数据完全一样
  • fork创建子进程就是在内核中通过调用clone实现

进程终止


注:

  • main函数中return 0 中的0代表程序的退出码,代表程序退出,结果是否运行正确
  • 这个退出码默认是给系统看的确认进程执行结果是否正确(0代表运行成功,!0代表运行结果有问题)
  • 用户想得知退出码,需要用到echo $? 来查看
  • echo $? 是用来查看最近一次执行的程序的退出码

进程退出场景

进程退出情况:

  1. 代码跑完,结果正确,退出码0
  2. 代码跑完,结果不正确,逻辑问题,但是没有导致程序崩溃,退出码!0
  3. 代码没有运行完毕,程序崩溃,退出码没有意义


注:

  • 退出码,可以是人为定义的,也可以是使用系统的错误码list
  • 当程序运行失败的时候,往往最关系的是失败的原因(计算机擅长处理整数类型的数据,将int转化为string(错误码描述))
  • 子进程的退出情况是父进程所关心的

总结:

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

进程常见退出方法

  • 正常终止(可以通过 echo $? 查看进程退出码):
    1. 从main返回
    1. 调用exit
    1. _exit
  • 异常退出:
  • ctrl + c,信号终止

注:

  • 子进程运行,父进程可以关心,也可以不关心子进程的运行结果

  • 进程非正常退出:野指针、/0、越界访问……,退出码是无意义的

_exit函数&&exit函数&&return退出


注:main函数return,非main函数的return不是终止进程,而是结束进程

注:任何函数exit都表示直接终止进程



注:

  • exit:在退出的时候,会进行后续资源处理,包括刷新缓冲区
  • _exit:在退出时,不会进行后续资源储量,直接终止进程

结论:

  • 在调用exit之前:
    1. 执行用户通过 atexit或on_exit定义的清理函数。
    1. 关闭所有打开的流,所有的缓存数据均被写入
    1. 调用_exit
  • return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
  • void _exit(int status):
    1. 参数:status 定义了进程的终止状态,父进程通过wait来获取该值
    1. 虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。

总结:

  • 站在OS角度,进程终止的核心思想是:归还资源
    1. 释放(释放不是真的把数据结构对象销毁,而是设置为不用的状态,然后保存起来,如果不用的对象多了就有一个数据结构池(这种规则在Linux中叫Slab分派器))曾经为了管理进程所维护的所有的数据结构对象
  • 池可以提高申请空间的时间,提高用户效率
    1. 释放(不是代码和数据清空,而是把内存设置为无效就可以了)程序代码和数据占用的内存空间
    1. 取消曾经该进程的连接关系

进程等待

进程等待必要性:

  1. 回收僵尸进程,解决内存泄漏
  2. 需要获取子进程的运行结束状态(有些父进程不需要子进程的运行结束状态)
  3. 尽量父进程要晚于子进程退出,这样可以规范化进行资源回收(编码角度)

注:进程一旦变成僵尸状态用kill -9 也无能为力,因为没有办法杀死一个已经死去的进程。

进程等待的方法

wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

注:wait方法是等待任意一个子进程,当子进程退出时,wait就可以退出并返回该子进程的pid


循环创建进程以及循环等待:

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

int main()

    int i=0;
    while(i<5)
    
          
       pid_t id=fork();
       if(id<0)
       
           perror("fork");
           return 1;
       
        if(id==0)
       
           int count=5;
           while(count)
           
               printf("child is runing:%d,ppid:%d,pid:%d\\n",count--,getppid(),getpid());
               sleep(1);
           
            printf("child quit.....\\n");
            exit(0);
        
        i++;
    
    for(i=0;i<5;i++)
    
        printf("father is waiting...\\n");
        sleep(10);
        pid_t ret=wait(NULL);
        printf("father is wait done,ret:%d\\n",ret);
        sleep(3);
        printf("father quit...\\n");
    
    return 0;



注:一般而言,使用fork函数后需要让父进程进行等待

waitpid方法

#include<sys/types.h>
#include<sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
 当正常返回的时候waitpid返回收集到的子进程的进程ID;
 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
 pid:
 Pid=-1,等待任一个子进程。与wait等效。
 Pid>0.等待其进程ID与pid相等的子进程。
 status:
 WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
 WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
 options:
 WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

注:

  • 这里等待指定进程的的工作是由父进程来完成的
  • pid_t pid可以用fork的返回值(父进程返回的是子进程的pid)
  • 等待的本质是管理(代码层面的管理)的一种方式
  • waitpid的中的参数options可以设置成0(阻塞式等待)


注:

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

补:

  • waitpid默认阻塞等待任意一个或指定子进程退出,当options被设置为WNOHANG则函数非阻塞,且当没有子进程退出时,waitpid返回0
  • 进程等待:等待子进程退出(绝对不是退出指定子进程),获取子进程返回值,释放子进程资源,避免出现僵尸进程

获取子进程status

pid_t wait(int*status)
pid_ t waitpid(pid_t pid, int *status, int options)

其中的status是输出型参数

注:

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

退出状态

注:

  • 全局变量不可以通过设置全局变量,来告知父进程,子进程的退出码,因为当父子进程中的任意一个进程的数据进行写入或者修改时会进行写时拷贝(当子进程中的数据将修改时,不会影响父进程的数据因此是绝对不行的)
  • waitpid/wait拿到status的值是从OS中的PCB拿到的,因为子进程要退出时要将退出码交给子进程的PCB,waitpid/wait拿到的值是通过task_struct拿到的

终止信号



终止信号(异常):

注:

  • 正常终止是没有收到任何退出信号
  • 只有正常退出退出状态才有显示,否则异常退出退出状态都为0(退出状态是没有意义的)
  • 正常退出终止信号为0;异常退出终止信号不为0
  • 一般进程提前终止,本质是该进程收到了操作系统发送的信号
  • 如果该进程运行时,受到kill -9 【pid】的命令时,该进程终止且终止信号为9

完整的进程等待:

或者:


注:系统提供了一堆的宏(函数),可以用来判断退出码、退出状态

进程的等待方式

阻塞式等待

当父进程调用waitpid函数时,父进程等待子进程,直到子进程运行结束(退出),在这期间父进程一直等(什么事情也不做)——阻塞式等待

pid_t ret=waitpid(id,&status,0);  //阻塞式等待
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()

    pid_t id =fork();
    if(id==0)
    
        int count=3;
        while(count)
        
            printf("child is running:%d,ppid:%d,pid:%d\\n",count--,getppid(),getpid());
            sleep(1);
        
        printf("child quit....\\n");
        exit(0);
    
    int status=0;
    pid_t ret=waitpid(id,&status,0);
    if(ret>0)
    
        printf("wait success!\\n");
        if(WIFEXITED(status))
        
            printf("normal quit!\\n");
            printf("quit code:%d\\n",WEXITSTATUS(status));
        
        else
        
            printf("process quit error!\\n");
        
     
    else if(ret==0)
    
        printf("no process to wait\\n");
    
    else
    
        printf("wait failed\\n");
        return 1;
    
    return 0;


注:

  • 在C语言/C++等这类编程语言中,所带调用的函数全部都是阻塞函数(调用->执行(调用方都在等待什么事情都没做)->返回->结束)
  • 在C语言/C++等这类编程语言中,所带调用的函数全部都是阻塞函数,之所以是阻塞调用:执行的都是单执行流,并且单执行流简单
  • waitpid的中的参数options可以设置成0(默认是阻塞式等待)

非阻塞式等待

当父进程调用waitpid函数时,父进程每隔一段时间会询问子进程的状态,如果父进程等待失败,父进程会往复的等待,直到父进程等待子进程退出并返回该子进程的pid为止,在等待失败到重新询问子进程的期间里,父进程会做一些其他事情——非阻塞式等待

 pid_t ret = waitpid(id,&status,WNOHANG); //非阻塞式等待

注:

  • 父进程多次询问子进程的状态的方式——基于非阻塞函数的轮询检测方案
  • 上述的失败是指并不是真的失败了,仅仅是对方的状态没有达到预期,下次再检测
  • 失败分为两种:
    1. 并不是真的失败了,仅仅是对方的状态没有达到预期
    1. 真正的失败
  • 非阻塞等待的本质是检测子进程的状态(通过多次检测,来检查子进程的状态)
  • 非阻塞式等待在自然界多(这种方式比较高效)

补:

  • 计算机资源在即将被吃完的时候,计算机会卡住,服务器hang住了——宕机
  • 进程中的“等” : 将当前进程放入到等待队列(进程“等”时是不可能将进程放在CPU中的),并将进程状态设置为非R状态,当满足唤醒条件时,唤醒进程将进程从等待队列放回到运行队列,并将进程的状态设置为R状态(这些操作都是OS负责完成的)
  • wait、waitpid是系统函数,它们都是通过操作系统来调的,因此当进程等待或进程调度时都是由OS完成的
  • 进程PCB(task_struct)中存有退出码、退出信号(wait、waitpid系统函数,它们是通过PCB从而得到子进程的status的)……

进程程序替换

创建子进程的目的:

  1. 执行父进程的部分代码
  2. 执行其他程序的代码(需要进行程序替换)

进程程序替换——当父进程创建出子进程让子进程执行其他程序的代码,这时该程序会从磁盘中将代码和数据直接加载到物理内存中,将子进程的页表由指向父进程的代码和数据指向到该程序的代码和数据

注:

  • 程序(代码和数据的集合)本质是文件
  • 操作系统是通过写时拷贝机制的方式将程序代码和数据直接加载到物理空间上交给子进程
  • 在进程程序替换的时候,没有创建新的子进程,原因是该进程相关的内核数据结构没受影响(该进程的pid没变)
  • 进程的程序替换是不改变进程内核的数据结构的,而只会修改部分的页表和数据,将新程序的代码和数据加载到内存,重新构建映射关系,和父进程彻底脱离

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

替换函数

int execl(const char *path, const char *arg, ...);

注:

  • 进程的程序替换是通过exec*函数来完成替换的
  • exec*函数,不用考虑返回值,只要返回一定是这个函数调用失败了

补:

  • 当软件运行时,一定要先将软件加载到内存中,软件从磁盘中加载到内存一定是通过exec*特殊函数(加载器)来完成的

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


注:一般环境变量PATH,都是一些系统命令才能在PATH中找到,或者还可以把自己的命令导入到PATH中

Linux下的环境变量与命令行参数

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


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

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

补:

  • 上述代码中的myenv自定义的环境变量(相当于是局部环境变量),不能被mycmd可执行程序继承(Linux下的继承与编程语言下的继承是两码事),因此想要在在mycmd中打印出myenv就必须使用execle、execve、execvpe
  • 用Makefile编译多个源文件(过程如下):

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


注:

  • 使用execvpe是需要注意自己编译的可执行程序是无法在系统默认路径下找到的需要将可执行程序所在路径放到默认路径下即可
  • execve是其余六个函数的底层实现,只不过是改变了其余函数的调用方式(参数)

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

命名理解

这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:

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


事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。换句话说execve是其余六个函数的底层实现,只不过是改变了其余函数的调用方式(参数)

exec*函数之间的关系:

总结

  • 什么是程序替换:通过exec*让特定进城去加载磁盘中的其它进程,以达到运行的目的,期间不创建新的进程
  • 为什么要程序替换:
    1. 子进程执行父进程部分代码
    1. 子进程自身执行新的程序的需求
  • 如何程序替换:通过exec*进行程序替换
  • exec*只要返回就说明出错了

补充:

  • 程序替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序(加载另一个程序在内存中,更新页表信息,初始化虚拟地址空间

简易的shell

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。

写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(waitpid)

注:

  • 解析命令需要分为内置命令(chdir)和非内置命令(fork及execvp)
  • 内置命令不需要创建子进程,而是在shell内的一个函数调用来使用的

代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#define NUM 128
#define SIZE 32
char command_line[NUM];
char *command_parse[SIZE];
int main()

    while(1)
    
        memset(command_line,'\\0',sizeof(command_line));
        printf("[lc@myhost my_shell]$ ");
        fflush(stdout);
        //数据读取
        if(fgets(command_line,NUM-1,stdin))
        
            command_line[strlen(command_line)-1]='\\0';
            //字符串(命令行数据分析)
            int index=0;
            command_parse[index]=strtok(command_line," ");
            while(1)
            
                index++;
                command_parse[index]=strtok(NULL," ");
                if(command_parse[index]==NULL)
                
                    break;
                linux运维 ps命令

linux运维基础篇 unit8

Linux中的进程管理

架构师成长系列|Linux运维工具—系统监控工具htop(1.1

Linux 线程(进程)数限制分析

linux云自动化运维基础知识21(selinux的初级管理)

(c)2006-2024 SYSTEM All Rights Reserved IT常识