详解:进程控制那些事儿

一、进程创建

1. fork

  • 头文件#include <unistd.h>
  • 函数原型pid_t fork(void);
  • 说明:通过复制调用进程创建新进程,调用进程称为父进程,创建出来的新进程称为子进程,父子进程共用同一个代码段,但是它们的数据并不共用
  • 返回值对于父进程来说返回值是子进程的PID,对于子进程来说返回值是0,如果创建子进程失败,则返回-1。我们可以通过返回值来判断父子进程,从而进行代码分流。

     当一个进程调用fork时,控制会转移到内核中去,在内核中会有以下几个过程:

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

     在fork返回后,会出现两个代码相同的进程,而且它们运行到相同的地方,此时,这两个进程将各自开始执行。
实例:

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

int num = 5;	//全局变量num

int main()
{
    printf("Before fork:PID=%d\n", getpid());
    pid_t pid = fork();  //创建一个新进程
    if(pid < 0)
    {   
        printf("fork error\n");
        return -1; 
    }   
    else if(pid == 0) //子进程,返回值是0
    {   
        num = 10;	//子进程修改num值
        printf("This is a child process.  [PID=%d] [pid=%d]	num=%d\n", getpid(), pid, num);
    }   
    else //父进程,返回值是子进程的pid
    {   
        printf("This is a parent process. [PID=%d] [pid=%d]	num=%d\n", getpid(), pid, num);
    }
    while(1)
    {}
    return 0;
}

运行结果:
在这里插入图片描述
     通过上面的结果来看,我们可以发现fork之前,父进程独立执行,而fork之后,父子进程分别执行,而谁先执行完全由调度器决定。此外,我们还发现当子进程修改num值后,父进程的值并不变,说明父子进程的数据并不共享。
     通常,父子进程用的是相同的物理空间,子进程的代码段、数据段、堆栈段都是指向父进程的物理空间,也就是说,两者的虚拟地址空间不同,但它们对应的物理空间是同一个。但是,当父子进程任意一方中有更改相应段的行为发生时,则再为子进程相应的段分配物理空间。这就是写实拷贝技术。
     写时拷贝是一种可以推迟甚至免除拷贝数据的技术。在子进程刚创建完后,内核此时并不复制整个父进程地址空间,而是让父子进程共享同一个物理空间,只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的数据空间。这种技术避免了拷贝大量根本就不会被使用的数据,极大地提高了进程快速执行的能力
     有时在实际创建进程中,我们也会fork失败,这主要有以下两个原因:

  • 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN
  • 系统内存不足,这时errno的值被设置为ENOMEM

2. vfork

  • 头文件
              #include <sys/types.h>
              #include <unistd.h>
  • 函数原型pid_t vfork(void);
  • 说明:该函数功能和fork一样,但是两者还是有区别的
    (1)vfork用于创建一个子进程,但内核并不会像fork那样给子进程创建独立虚拟地址空间,而是直接共享父进程的虚拟空间,也就是说,父子进程代码共享,数据也共享
    (2)vfork保证子进程先运行,在子进程调用 execexit 之后父进程才可能被调度运行

实例:

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

int num = 5;	//全局变量num

int main()
{
    pid_t pid = vfork();
    if(pid < 0)
    {   
        printf("vfork error\n");
        return -1; 
    }   
    else if(pid == 0)
    {   
        num = 10;	//在子进程中修改num值 
        printf("child num:%d\n", num);
        exit(0);
    }   
    else
    {   
        printf("parent num:%d\n", num);
    }
    while(1)
    {
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述
     通过上面的结果来看,我们发现,子进程是先于父进程运行的,而且当子进程修改num值后,父进程的num值也跟着变,说明父子进程的数据是共享的。

二、进程终止

1. 进程退出的场景

(1)正常退出

          正确退出:代码运行完毕,结果正确
          错误退出:代码运行完毕,结果错误

(2)异常退出

          代码异常终止

2. 进程退出的方式

(1)_exit函数
  • 头文件#include <unistd.h>
  • 函数原型void _exit(int status);
  • 说明立即终止调用进程,属于该进程的任何打开的文件描述符都被关闭
  • 参数status 作为进程的退出状态返回给父进程,父进程可以通过 wait 来获取该值。不过虽然 statusint型,但是仅有低8位可以被父进程所用

实例:

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

int main()
{
    printf("hello world!");
    _exit(-1);
}

运行结果:
在这里插入图片描述
在这里插入图片描述
     通过上面的结果来看,我们发现 _exit()什么都没有做,而是立即终止程序,并且通过echo $?来查看进程退出码时,显示结果并不是-1,而是255,这是因为父进程只能使用退出码-1的低8位所导致的。

(2)exit函数
  • 函数原型void exit(int status);
  • 说明exit 最后也是会调用的 _exit 的,不过它在调用_exit之前,还做了其他一些工作:
    (1)执行用户通过atexit或on_exit定义的清理函数
    (2)关闭所有打开的流,所有的缓存数据均被写入
    (3)调用_exit

实例:

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

int main()
{
    printf("hello world!");
    exit(-1);
}

运行结果:
在这里插入图片描述
     在这里,我们可以用下面这幅图去更加容易地理解 _exitexit 的关系:
在这里插入图片描述

(3)return

     return是一种更为常见的退出进程的方法,但是它只有在main中执行才会退出进程,并且在main中执行 return n 等同于执行 exit(n)

三、进程等待

1. 进程为什么需要等待

     从我们之前接触的僵尸进程来看,当一个子进程先于父进程退出时,如果父进程没有关心这个子进程的退出状态,那么就有可能形成僵尸进程,进而出现内存泄露的问题。所以为了避免这种问题的出现,我们需要在子进程退出后,让父进程通过进程等待的方式,去获取子进程的退出状态,接着回收子进程的资源。

2. 进程等待的方法

(1)wait
  • 头文件
              #include <sys/types.h>
              #include <sys/wait.h>
  • 函数原型pid_t wait(int *status);
  • 参数:输出型参数,获取终止子进程的退出状态,若不关心可设置为NULL
  • 返回值:成功时,返回终止子进程的PID;出错时,返回-1
  • 功能等待任意一个子进程退出,若没有子进程退出,则一直阻塞等待
(2)waitpid
  • 头文件
              #include <sys/types.h>
              #include <sys/wait.h>
  • 函数原型pid_t waitpid(pid_t pid, int *status, int options);
  • 参数
    (1)pid
                   pid=-1时,等待任意一个子进程退出,与 wait 效果一样
                   pid>0时,等待一个进程ID与pid相等的子进程退出
    (2)status
                   WIFEXITED(status):若子进程正常终止,则返回true
                   WEXITSTATUS(status):若WIFEXITED为true,则返回子进程的退出状态
    (3)options
                   WNOHANG:若指定的子进程没有结束,则立即返回0,不予以等待;若正常结束,则返回该子进程的PID
  • 返回值:当终止子进程正常结束的时候,返回该子进程的PID;如果options被设置为 WNOHANG ,并且此时没有子进程退出时,则返回0;若调用中出错,则返回 -1,这时 errno 会被设置为相应的值以指示错误所在。
  • 功能可以等待指定的子进程退出,也可以等待任意一个子进程退出

说明

  •      如果终止子进程已经退出,此时调用 wait 或 waitpid 会立即返回,获得子进程退出信息,并且释放资源;
  •      如果在任意时刻调用 wait 或 waitpid 时,子进程存在且正常运行,则父进程可能会处于阻塞等待状态;
  •      如果不存在该子进程,则立即出错返回。

3. 获取进程退出状态码

     通过上面的介绍后,我们知道,如果父进程不关心子进程的退出状态信息,那么可以将 status 设置为NULL,否则,操作系统会根据 status ,将子进程的退出信息反馈给父进程。
     在这个 status 参数中存储了子进程的退出原因以及退出码,而参数中只用了低16位(两个字节)来存储这些信息,我们可以把它当做一个位图来看,如下:
在这里插入图片描述

  • 正常退出时高8位存储的是退出码,只有子进程运行完毕退出时才会有,低8位为0
  • 异常退出时低7位存储的是导致子进程异常退出的信号值,第8位存储core dump标志,只有子进程异常退出时才会有,高8位为0

4. 实例

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

int main()
{
    pid_t pid = fork(); // 创建进程
    if(pid < 0)
    {   
        printf("fork error!\n");
        return -1; 
    }   
    else if(pid == 0)	// 子进程
    {   
        sleep(10);
        exit(10);
    }   
    else // 父进程
    {   
        int status;
        pid_t ret = wait(&status); // 进程等待
        if(ret>0 && (status & 0x7F)==0) // 正常退出
        {
            printf("exit code:%d\n", (status>>8)&0xFF); // 打印退出状态码
        }
        else if(ret > 0) // 异常退出
        {
            printf("signal value:%d\n", status&0x7F); // 打印终止信号值
        }
    }
    
    return 0;
}

运行结果:

  • 等待10s,子进程正常退出:
    在这里插入图片描述
  • 在其他终端 kill -9 掉这个子进程,子进程异常退出:
    在这里插入图片描述
(2)waitpid
  • 阻塞式等待:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid = fork();
    if(pid < 0)
    {   
        printf("fork error!\n");
        return-1;
    }   
    else if(pid == 0)
    {   
        sleep(10);
        exit(10);
    }   
    else
    {   
        int status;
        pid_t ret = waitpid(-1, &status, 0); // 阻塞式等待,等待10s
        if(ret>0 && WIFEXITED(status)) // 等待成功,子进程正常退出
        {
            printf("exit code:%d\n", WEXITSTATUS(status)); // 打印退出状态码
        }
        else // 等待失败,子进程异常退出
        {
            printf("Waiting for child process to exit failed!\n");
        }
    }
    
    return 0;
}

运行结果:

  • 等待10s,子进程正常退出:
    在这里插入图片描述
  • 在其他终端 kill -9 掉这个子进程,子进程异常退出:
    在这里插入图片描述
  • 非阻塞式等待:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid = fork();
    if(pid < 0)
    {   
        printf("fork error!\n");
        return-1;
    }   
    else if(pid == 0)
    {   
        sleep(10);
        exit(10);
    }   
    else
    {   
        int status;
        pid_t ret = 0;
        do  
        {
             ret = waitpid(-1, &status, WNOHANG); // 非阻塞式等待,若此时没有子进程退出,则立即返回0
             printf("The child process is running!\n");
             sleep(1);
        }while(ret == 0);
        if(ret>0 && WIFEXITED(status)) // 等待成功,子进程正常退出
        {
            printf("exit code:%d\n", WEXITSTATUS(status)); // 打印退出状态码
        }
        else // 等待失败,子进程异常退出
        {
            printf("Waiting for child process to exit failed!\n");
        }
    }
    
    return 0;
}

运行结果:

  • 等待10s,子进程正常退出:
    在这里插入图片描述
  • 在其他终端 kill -9 掉这个子进程,子进程异常退出:
    在这里插入图片描述

四、进程程序替换

1. 替换原理

     大多数时候,我们创建一个进程并不希望子进程跟父进程做相同的事情,而是希望能够做另一件事,这时候就用到了程序替换,子进程会调用一种 exec 函数以执行另一个程序。要注意的是,调用exec函数并不会创建新进程,所以调用exec函数前后该进程的PID并不会改变
     当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,因为只是替换了内容,所以并不会重新创建虚拟地址空间和页表,替换后这个进程将从新程序的入口函数开始运行。如果替换成功,则表示该进程运行的代码段已经不是以前的代码段了,而是新程序,因此原来代码exec函数以后的代码都不会运行,除非替换出错。

2. 替换函数

(1)exec函数族
  • 头文件:#include <unistd.h>
  • 函数原型:
    (1)int execl(const char *path, const char *arg, ...);
    (2)int execlp(const char *file, const char *arg, ...);
    (3)int execle(const char *path, const char *arg, ..., char * const envp[]);
    (4)int execv(const char *path, char *const argv[]);
    (5)int execvp(const char *file, char *const argv[]);
    (6)int execve(const char *filename, char *const argv[], char *const envp[]);
  • 返回值:如果调用成功,则加载新的程序从启动代码开始执行,不在返回; 如果调用出错,则返回-1。所以可以说:exec函数只有出错的返回值,而没有成功的返回值。
(2)命名理解
  • l(list):参数采用列表格式
  • v(vector):参数采用数组格式
  • p(path):自动搜索环境变量PATH
  • e(env):自己维护环境变量
函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表否,自动在PATH中寻找
execle列表否,需要自己组装环境变量
execv数组
execvp数组否,自动在PATH中寻找
execve数组否,需要自己组装环境变量
(3)实例:
  • execl

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

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

int main()
{
    execl("/bin/ls", "ls", "-l", NULL);
    // 带全路径
    // 参数是以列表格式给出
    // 不定参数要以NULL结尾
    return 0;
}

执行结果:
在这里插入图片描述

  • execlp

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

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

int main()
{
    execlp("ls", "ls", "-l", NULL);
    // 不需要带上路径,只需要告诉文件名即可,会自动到环境变量PATH中的路径下寻找
    // 参数是以列表格式给出
    return 0;
}
  • execle

原型int execle(const char *path, const char *arg, ..., char * const envp[]);

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

int main()
{
    char * const envp[] = {"MYENV=666", NULL};
    execle("/bin/ls", "ls", "-l", NULL, envp);
    // 带全路径
    // 参数是以列表格式给出
    // 需要自己组装环境变量
    return 0;
}
  • execv

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

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

int main()
{
    char * const argv[] = {"ls", "-l", NULL};
    execv("/bin/ls", argv);
    // 带全路径
    // 参数是以数组格式给出
    return 0;
}
  • execvp

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

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

int main()
{
    char * const argv[] = {"ls", "-l", NULL};
    execvp("ls", argv);
    // 不需要带上路径,只需要告诉文件名即可,会自动到环境变量PATH中的路径下寻找
    // 参数是以数组格式给出
    return 0;
}
  • execve

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

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

int main()
{
    char * const argv[] = {"ls", "-l", NULL};
    char * const envp[] = {"MYENV=123", NULL};
    execve("/bin/ls", argv, envp);
    // 带全路径
    // 参数是以数组格式给出
    // 需要自己组装环境变量
    return 0;
}

     事实上,只有execve是真正的系统调用,其它五个函数最终都会调用execve(execve在man手册第2节,其它五个函数在第3节)。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值