BSP Day50

今天我们继续学习昨天没有学习完的进程的相关知识。

进程共享

父子进程之间在fork后。有哪些相同,哪些相异之处呢?

父子相同处:

全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…

父子不同处:

1. 进程ID

2. fork返回值

3. 父进程ID

4. 进程运行时间

5. 闹钟(定时器)

6. 未决信号集

似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。

父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己

的逻辑都能节省内存开销。

接下来用一个程序来测试下,父子进程是否共享全局变量。

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

int var = 100;

int main()
{
    printf("init var = %d\n",var);
    pid_t pid = fork();
    if(pid<0)
        {
            perror("pid error");
            exit(1);
        }
    else if(pid>0)
        {
            var = 120;
            printf("parent,var = %d\n",var);
            printf("I' am parent pid = %d,getppid = %d\n",getpid(),getppid());

        }
   else if(pid == 0)
        {
            var = 140;
            printf("child,var = %d\n",var);
            printf("I' am child pid = %d,getppid = %d\n",getpid(),getppid());

        }
    printf("------finish----------\n");

 return 0;
}

结论:

【注】:父子进程不共享全局变量。

【注】:父子进程共享。

1. 文件描述符(打开文件的结构体)
2. mmap建立的映射区

特别的,fork之后父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法。

fork生成的子进程和父进程的功能一样,如果想让fork生成的子进程的功能不一样,即拥有与父进程不一样的代码段数据段以及堆栈段,应该怎么办呢?

使用exec函数系列。

fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。

exec替换进程映像:

在进程的创建上Unix采用了一个独特的方法,它将进程创建与加载一个新进程映象分离。这样的好处是有更多的余地对两种操作进行管理。

当我们创建了一个进程之后,通常将子进程替换成新的进程映象,这可以用exec系列的函数来进行。当然,exec系列的函数也可以将当前进程替换掉。

例如:在shell命令行执行ps命令,实际上是shell进程调用fork复制一个新的子进程,在利用exec系统调用将新产生的子进程完全替换成ps进程。

exec系列函数(execl,execlp,execle,execv,execvp)

包含头文件<unistd.h>

功能:

 用exec函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID。exec名下是由多个关联函数组成的一个完整系列。

头文件<unistd.h>

extern char **environ;

原型:

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

参数:

path参数表示你要启动程序的名称包括路径名

arg参数表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且arg必须以NULL结束

返回值:成功返回0,失败返回-1

注:上述exec系列函数底层都是通过execve系统调用实现:

       #include <unistd.h>

       int execve(const char *filename, char *const argv[],char *const envp[]);

DESCRIPTION:
       execve() executes the program pointed to by filename.  filename must be
       either a binary executable, or a script starting with  a  line  of  the form

若参数file文件名:包含/视为路径名,否则按PATH环境变量去指定各目录搜寻文件
  函数名的解读:
    (1)l 表示参数以列表(list)方式提供。
    (2)v 表示参数以数组[向量(vector)]方式提供。
    (3)p 表示用户在PATH环境变量中寻找可执行文件。
(只需简单提供文件名,主要用于shell,因为shell所指向进程通常会从shell继承环境变量)
    (4)e 表示会提供给新进程以新的环境变量。
  exec系列函数没有一个同时可搜索路径和使用新环境变量的函数。
  exec系列函数成功调用不仅改变地址空间与进程映像,
  还改变进程的一些属性:
    1.任何挂起的信号都会丢失。
    2.捕捉的任何信号会还原为缺省的处理方式,因为信号处理函数已经不存在于地址空间中了。
    3.任何内存的锁定会丢失。
    4.多数线程的属性会还原到缺省值。
    5.多数关于进程的统计信息会复位。
    6.与进程内存相关的任何数据都会丢失,包括映射的文件。
    7.包括c语言库的一些特性(例如aexit())等独立存在于用户空间的数据都会丢失。
  未改变的进程属性例如 pid、父进程的pid、优先级、所属的用户和组。

实例:

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

int main (int argc,char *argv[])  
{  
    int ret = 0;                                //返回值
    printf("Executing ls\n"); 

    /**
     *          调用execl函数
     *  参数1:带路径文件名
     *  参数2:文件名
     *  最后参数:NULL 
     */
    ret = execl("/bin/ls","ls","-l",NULL);


    /**
     *  若execl()函数有返回,说明调用失败 
     */
    if(ret == -1){
        perror("execl failed to run ls");
    }
    
    exit(1);                                    //退出
} 

编译及运行结果:

1.编译gcc execl.c -o execl
2.运行:./execl
3.结果: 

-rwxrwxr-x 1 hhb hhb 16232 11月 7 14:47 fork

-rw-rw-r-- 1 hhb hhb 563 11月 7 14:47 fork.c

结论:

execl()调用后紧跟着 perror()的无条件调用。这是因为若调用程序还存在且 execl()调用返回,那么肯定是 execl()调用出错了。
只要 execl()其它exec 调用成功,就肯定清除了调用程序而代之以新的程序。 

execv函数实例:

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

int main (int argc,char *argv[])  
{  
    char* av[] = {"ls","-1",NULL};
    execv("/bin/ls",av);
    perror("execl failed");
    exit(1);
} 

 编译及运行结果:

1.编译gcc execv.c -o execv
2.运行:./execv
3.结果: 

-rwxrwxr-x 1 hhb hhb 16232 11月 7 14:47 fork

-rw-rw-r-- 1 hhb hhb 563 11月 7 14:47 fork.c

execlp()和 execvp()(p表示用户在PATH环境变量中寻找可执行文件)
execlp()和 execvp()分别类似于系统调用 execl()和 execv(),主要区别是:
函数名后多了一个p,多用于shell,因为shell所执行进程通常会从shell继承环境变量,表示:
第一个参数指向的是文件名(不包含路径)。
可通过检索 shell 环境变量 PATH指出的目录,来得到该文件名的路径前缀部分。如可在 shell 中用下述命令序列来设置环境变量 PATH:

$PATH=/bin;/usr/bin;/sbin

$export PATH

即: execlp() execvp()执行时:先在目录/bin,然后在目录/usr/bin,最后在目录/sbin 中搜索程序文件。另外, execlp() execvp()还可以用于运行 shell 程序,而不只是普通的程序。 

execvp函数实例

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

int main (int argc,char *argv[])  
{  
    int ret = 0;                                //返回值
    
    /**
     *          调用execvp函数
     *  参数1:可执行文件名
     *  参数2:参数
     *  最后参数:NULL 
     */
    char *const args[] = {"vi","./data.txt",NULL};
    ret = execvp("vi",args);

    /**
     *  若execvp()函数有返回,说明调用失败 
     */
    if(ret == -1){
        perror("execvp");
    }
    
    exit(1);                                    //退出
} 

execle()和 execve()(e表示会提供给新进程以新的环境变量)

execle()和 execve()分别类似于系统调用 execl()和 execv(),主要区别是:
函数名后多了一个e。

实例:

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

int main (int argc,char *argv[])  
{  
    char *args[] = {"/bin/ls",NULL};

    printf("PID = %d\n",getpid());
    if(execve("/bin/ls",args,NULL) < 0)
    {
        perror("execve");
    }
    
    exit(1);                                   
} 

错误的返回值:

回收子进程

孤儿进程
孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。

僵尸进程
僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

【注意】:僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。

清除掉僵尸进程的方法:

  1. wait();
  2. waitpid();
  3. 杀死其父进程 kill -9 ppid,使其变成孤儿进程, init进程回收

wait函数 

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:
如果是正常终止则保存着退出状态
如果是异常终止则保存着导致该进程终止的信号是哪个。
这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。

父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
① 阻塞等待子进程退出
② 回收子进程残留资源
③ 获取子进程结束状态(退出原因)。

pid_t wait(int *status);

成功:清理掉的子进程`ID`;失败:`-1` (没有子进程)

当进程终止时,操作系统的隐式回收机制会:
1.关闭所有文件描述符
2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:

1. WIFEXITED(status) 为非0    → 进程正常结束
    WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
2. WIFSIGNALED(status) 为非0 → 进程异常终止
    WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态
    WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
    WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行

waitpid函数

作用同wait,但可指定pid进程清理,可以不阻塞。

pid_t waitpid(pid_t pid, int *status, in options);    
成功:返回清理掉的子进程`ID`;
失败:-1(无子进程)
特殊参数和返回情况:
参数`pid`: 
> 0 回收指定`ID`的子进程    
 -1 回收任意子进程(相当于`wait`)
  0 回收和当前调用`waitpid`一个组的所有子进程
<-1 回收指定进程组内的任意子进程
返回0:参3为`WNOHANG`,且子进程正在运行。

注意:一次`wait`或`waitpid`调用只能清理一个子进程,清理多个子进程应使用循环。    

 今天就学到这儿了,明天接着分享。


 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixiaxiao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值