Linux---进程控制

目录

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表示退出码0:success(正常退出)非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个函数都是系统提供的基本封装,用于满足不同的调用场景。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值