Linux---进程控制

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

1. 进程创建

fork函数

写时拷贝

fork常规用法  

fork调用失败的原因  

2. 进程终止 

2.1 进程终止时操作系统做了什么

2.2 进程终止的常见方式 

main函数return返回值意义 

2.3 进程正常终止方法

_exit 和 exit 的区别

3. 进程等待  

3.1 进程等待必要性

3.2 进程等待的方法  

3.2.1 wait方法

3.2.2 waitpid方法  

获取子进程status

非阻塞式等待 

4. 进程程序替换

4.1 什么是进程程序替换

4.2 替换原理

4.3 如何进程程序替换

4.3.1 不创建子进程替换

4.3.2 创建子进程替换 


1. 进程创建

fork函数

        在linux fork 函数时非常重要的函数 它从已存在进程中创建一个新进程 新进程为子进程而原进程为父进程。
#include <unistd.h>//头文件

pid_t fork(void);//pid_t 为返回值类型是OS一种数据结构为整形
返回值子进程返回0父进程返回子进程id出错返回-1
进程调用 fork 当控制转移到内核中的 fork 代码后 内核做
  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回开始调度器调度

 

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

写时拷贝

        为了实现进程的独立性子进程被创建后也应该有自己的代码和数据可是一般而言我们没有加载过程也就是说子进程没有自己的代码和数据所以子进程只能使用“父进程的代码和数据”。

代码都是不可被写的只能读取所以父子共享没有问题

数据可能被修改所以必须分离

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

fork常规用法  

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

fork调用失败的原因  

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

2. 进程终止 

2.1 进程终止时操作系统做了什么

OS释放进程申请的相关内核数据结构和对应的数据和代码本质就是释放系统资源。

2.2 进程终止的常见方式 

  1. 代码运行完毕结果正确
  2. 代码运行完毕结果不正确
  3. 代码异常终止(即代码没跑完程序崩溃了)

main函数return返回值意义 

我们知道当代码运行完毕不管结果正确与否都会执行main函数最后的return语句那么return 0有什么意义呢一定要是0吗能不能是1,2,3...这些数字

main函数返回值的意义是返回给上一级进程用来评判该进程执行结果用的可以忽略。0表示退出码0success(正常退出)非0表示运行结果不正确。下面简单用Linux演示一下

可以用 echo $? 查看最近一次进程的退出码

接下来我们查看一下Linux系统给的进程退出码方案

 我们可以使用系统提供的退出码和含义但是如果你想要自己定义也可以自己设计一套退出方案(但是不建议)

2.3 进程正常终止方法

  • 1.调用exit
  • 2.调用_exit
  • 3. 从main返回
_exit函数
#include <unistd.h>
void _exit(int status);

参数status 定义了进程的终止状态父进程通过wait来获取该值
说明虽然 status int 但是仅有低 8 位可以被父进程所用。所以 _exit(-1) 时在终端执行 $? 发现返回值是255

exit函数  

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

 

_exit 和 exit 的区别

_exit是系统接口exit是库函数。并且exit最后也会调用_exit但是在调用_exit之前exit函数会关闭所有打开的流并把缓冲数据全部写入。请看下面简单的例子

int main()
{
    printf("hello world!\n");
    sleep(3);
    //这里不管用exit还是_exit结果都是先打印再休眠3秒因为\n会刷新缓冲区数据
}

如果我们把打印语句中 \n 去掉即不刷新缓冲区数据结果会有什么不同

 

 

 return退出

        return是一种更常见的退出进程方法。执行 return n 等同于执行 exit(n), 因为调用 main 的运行时函数会将 main 的返回值当做 exit 的参数。
        return和exit区别 是一个是语句一个是函数而且return只有在main函数才退出进程在其他函数则退出函数exit在任何函数都是退出进程。

3. 进程等待  

3.1 进程等待必要性

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

3.2 进程等待的方法  

3.2.1 wait方法

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int*status);

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

3.2.2 waitpid方法  

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。
 将wait演示代码中
    pit_t ret = wait(NULL);
 改为
    pid_t ret = waitpid(id,NULL,0);//id即子进程id,option为0表示阻塞等待
效果是一样的。

  我们运行看看子进程退出码

上述结果是为什么呢我们来了解一下status参数

获取子进程status

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

 上图可以发现status次低8位表示子进程退出状态低7位表示子进程收到的退出信号(即判断是否正常退出)。代码演示如下

当然我们也可以用上述status定义的宏获取退出码

非阻塞式等待 

之前我们都是options设为0表示阻塞式等待如果需要非阻塞等待那么要将options设为WNOHANG(WNOHANG是宏定义表示1)

非阻塞式等待父进程通过调用waitpid来进行等待如果子进程没有退出waitpid这个系统调用立马返回。

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt--)
        {
            printf("我是子进程:%d\n",cnt);
            sleep(1);
        }
        exit(55);//55仅为了测试无意义
    }
    else
    {
        //父进程
        int quit = 0;
        while(!quit)
        {
            int status = 0;
            pid_t res = waitpid(-1,&status,WNOHANG);//以非阻塞方式等待
            if(res > 0)
            {
                //等待成功 && 子进程退出
                printf("等待子进程退出成功退出码:%d\n",WEXITSTATUS(status));
                quit = 1;
            }
            else if(res == 0)
            {
                //等待成功 && 但子进程并未退出
                printf("子进程还在运行中暂未退出父进程可以等等先做其他事\n");
            }
            else
            {
                //等待失败
                printf("wait 失败!\n");
                quit = 1;
            }
            sleep(1);
        }
    }
}

4. 进程程序替换

4.1 什么是进程程序替换

我们知道当我们fork()之后父子进程各自执行父进程代码的一部分那么如果我们想要子进程执行一个全新的程序呢这就需要进程的程序替换来完成这个功能。

程序替换通过特定的接口加载磁盘上的一个全新的程序代码和数据加载到调用进程的地址空间中让子进程执行其他程序

4.2 替换原理

原理新程序被加载到物理内存中进程PCB指向的虚拟地址空间通过页表重新与物理内存建立映射关系。

4.3 如何进程程序替换

我们用七种以exec开头的替换函数替换程序,统称exec函数

#include <unistd.h>//以下替换函数都是这个头文件

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[])

函数解释

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

命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记
        l(list) : 表示参数采用列表
        v(vector) : 参数用数组
        p(path) : 有p自动搜索环境变量PATH
        e(env) : 表示自己维护环境变量

看到这么多函数是不是头都大了没关系其实它们都很相似具体怎么用接下来会介绍。 

4.3.1 不创建子进程替换

先介绍一下 execl 函数

接下来举个例子演示一下

首先我们知道在Linux下诸如 ls 这种指令也是进程所以我们就替换成 ls 程序先找到 ls 路径

接下来演示代码

 

 程序替换后原来后面代码(即上面例子中第二个printf)不会执行原因

        execl是程序替换调用该函数成功之后会将当前进程的所有的代码和数据都进行替换包括以及执行和没有执行的代码所以一旦调用成功后续所有代码全部不会执行。这也是execl函数只有调用失败返回值没有调用成功返回值的原因

4.3.2 创建子进程替换 

execv 

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

 我们发现execv于execl效果一样只是传参方式有区别。

 execlp:

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

 exexlp带p表示不需要写路径该函数会自己搜索环境变量PATH。

execvp: 

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

结合execv与execlp,我们不难理解execvp表示自动搜索环境变量PATH且传参为数组形式。

 

 至于带e的函数只是在最后加上要传的环境变量罢了。我们说进程具有独立性但是环境变量却可以继承其实就是这里传参

 

实际上只有execve是系统调用接口其他6个函数都是系统提供的基本封装用于满足不同的调用场景。

 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux