进程控制 ─── linux第15课

目录

进程控制

1.进程创建

fork函数

写时拷贝

fork常规用法

fork调用失败的原因

进程终止

进程退出的场景

退出码

​编辑

进程终止的方法

_exit函数

exit函数

exit与_exit

进程等待

进程等待必要性

[进程等待的方法(wait 和waitpid)](#进程等待的方法(wait 和waitpid))

阻塞非阻塞问题

​编辑

非等待轮询代码

获取子进程的退出码或退出信号

进程程序替换

替换原理

替换函数

函数解释

execl函数。

使用示例:

命名理解

关于环境变量:

execl和execv的区别

execvpe使用样例


进程控制

1.进程创建

fork函数

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

cpp 复制代码
 pid_t fork(void); 
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

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

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

写时拷贝

写时拷贝: 申请内存,发生拷贝,进行数据修改, 修改页表的物理内存 , 恢复权限.

并没有对原数据区进行覆盖

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

父进程创建子进程时 ,子进程继承的页表中权限全是只读的, 如果子进程尝试修改,会触发系统错误(因为权限都是只读) 触发缺页中断(进行系统检测)

1.子进程如果修改代码段(代码段不能修改),会导致子进程被杀掉

2. 子进程如果修改数据区, 则发生写时拷贝,也恢复了页表中的读写权限.

fork常规用法

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

fork调用失败的原因

系统中有太多的进程

实际用户的进程数超过了限制

进程终止

进程退出的场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止(代码有问题 ,OS提前用信号终止了进程)

退出码

退出码用于表示进程的终止状态,正常退出时由进程显式设置

只有进程跑完才会设置退出码

  • 0: 成功
  • 非零: 错误 不同的数字代表不同的错误类型
cpp 复制代码
echo $?                    查看最近一个正常终止进程的退出码

main函数的返回值(退出码)--->返回给父进程或者系统

进程异常退出时通常是通过信号(Signal)终止的,而不是设置退出码

退出码和信号是两种不同的机制,但都在进程的PCB中.

  • 进程异常中止 会记录本身的退出信号
  • 进程跑完 , 结果对 ---->退出码设置为0

结果不对----> 退出码设置为非0

进程终止的方法

  1. main函数return 而其他函数return仅仅只是函数的调用结束,与main函数不同
  2. exit( ) 在代码的任意地方表示进程结束(会将缓冲区的内容刷新出来)推荐用
  3. _exit( ) 不会将缓冲区的内容刷新

_exit函数

cpp 复制代码
 #include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
后面wait中将会说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值
是255。

exit函数

cpp 复制代码
#include <unistd.h>
void exit(int status);

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

exit与_exit

  • exit( )是上层, _exit( )是下层
  • exit()=_exit() +fllush( )
  • return退出return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

示例

cpp 复制代码
#include <unistd.h>
void exit(int status);
int main()
{
     printf("hello");
     exit(0);
}

运行结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
 
int main()
{
     printf("hello");
     _exit(0);
}

运行结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#

进程等待

进程等待必要性

一般而言,父进程创建子进程,就要等待子进程,直到子进程结束

之前讲过,子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存漏。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法(wait 和waitpid)

wait方法

返回值: 成功返回被等待进程pid,失败返回-1。

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

cpp 复制代码
  1 #include<iostream>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<unistd.h>
  5 #include<cstdio>
  6 #include<string.h>
  7 #include<stdlib.h>
  8 #include<errno.h>
  9 
 10 int main()
 11 {
 12     printf("我是父进程,开始创建子进程\n");
 13     pid_t id = fork();
 14     if(id < 0)
 15     {
 16         printf("errno : %d, errstring: %s\n", errno, strerror(errno));
 17         return errno;
 18     }
 19     else if(id == 0)//子进程
 20     {
 21         int count = 5;
 22         while(count)
 23         {
 24             printf("子进程运行中,pid:%d\n", getpid());
 25             count--;
 26             sleep(1);
 27         }
 28         exit(1); //此处退出码设为1,便于后续观察
 29     }
 30     else//父进程
 31     {
 32         int status=0,count =10;
 33         pid_t rid = wait(&status);
 34         printf("已经等到子进程结束,退出码是%d\n",status);
 35         while(count)
 36         {
 37             printf("我是父进程: pid:%d\n", getpid());
 38             count--;
 39             sleep(1);
 40         }
 41     }
 42     return 0;

waitpid方法

cpp 复制代码
pid_t rid = waitpid(id, &status, 0); // == wait
cpp 复制代码
pid_ t waitpid(pid_t pid, int *status, int options);

返回值:
     > 0 : 当正常返回的时候waitpid返回收集到的子进程的进程ID;
     == 0: 成功等待,但子进程没有退出
     < 0:  如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:
     pid:
     Pid=-1,等待任一个子进程。与wait等效。
     Pid>0.等待其进程ID与pid相等的子进程。

 (输出型参数)status:
     WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
     WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)


 options:
     option默认是0 :   代表阻塞等待.
     WNOHANG:         代表非阻塞等待 , 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,后续还会进行轮询检测子进程是否结束. 若正常结束,则返回该子进程的ID。

阻塞非阻塞问题

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

非等待轮询代码

cpp 复制代码
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        while(true)
        {
            printf("我是子进程, pid : %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }

    // father
    while(true)
    {
        sleep(1);
        pid_t rid = waitpid(id, nullptr, WNOHANG);
        if(rid > 0)
        {
            printf("等待子进程%d 成功\n", rid);
            break;
        }
        else if(rid < 0)
        {
            printf("等待子进程失败\n");
            break;
        }
        else
        {
            printf("子进程尚未退出\n");

            // 做自己的事情
            //for(auto &task : tasks)
            //{
            //    task();
            //}
        }
    }

}

获取子进程的退出码或退出信号

  • status是32个bit位的位图, 只考虑低16位, 次低8位是退出码(退出状态) , 低7位是退出信号的值.
  • 正常退出就只填(退出码)次低8位
  • 异常退出就只填低7位

这也是为什么上面代码子进程exit(1) ,而父进程的status被修改成256(第8位被改为了1 )

下面的代码已经改了回来

  • WIFEXITED(status): 若为正常终止 子进程返回的状态,则为。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
cpp 复制代码
 else//父进程
 31     {
 32         int status=0,count =10;
 33         pid_t rid = wait(&status);
 34         if(WIFEXITED(status))
 35         {
 36             printf("已经等到子进程正常结束,退出码是%d\n",WEXITSTATUS(status));
 37         }
 38         while(count)
 39         {
 40             printf("我是父进程: pid:%d\n", getpid());
 41             count--;
 42             sleep(1);
 43         }
 44     }

进程程序替换

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。

当进程调用一种exec函数时 ,该进程的用户空间代码和数据完全被新程序替换 ,从新程序的启动例程开始执行。调用exec并不创建新进程 ,进行进程替换 所以调用exec前后该进程的id并未改变。
创建进程
调用exec家族

替换函数

其实有六种以exec开头的函数,统称exec函数:

cpp 复制代码
int execve(const char *path, char *const argv[], char *const envp[]);
函数解释
  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值

先来认识execl函数

execl函数。

execl 是 C 标准库中的一个函数,用于执行另一个程序。它属于 exec 函数族的一部分,用于替换当前进程的映像(即代码和数据)为新程序的映像。

A进程执行execl ,是将新进程的代码和数据覆盖到了父进程的代码和数据上(只是改页表,PCB PID不变),execl成功运行后,A程序的execl后面的代码不会被运行了(代码区被新进程代码全覆盖)

参数:

path: 带路径的可执行程序**(执行谁)**

argc: 命令行怎么写的, 在这就怎么写(怎么执行)

最后一定要写NULL/nullptr (因为本质命令行参数是在一张表,最后是NULL/nullptr)
返回值:只要返回就是失败

使用示例:

cpp 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5     execl("/usr/bin/ls","ls" ,"-a","-l",NULL );
  6 
  7     return 0;
  8 }

命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH****带有p的不用带路径,只带可执行程序的名称就行

e(env) : 表示自己维护环境变量 带e的用途: 使用全新的****环境变量传递给目标程序
putenv定义在 <stdlib.h> 头文件中,允许程序动态地修改或添加环境变量

cpp 复制代码
  const std::string myenv="HELLO=AAAAAAAAAAAAAAAAAAAA";
  putenv((char*)myenv.c_str());

进程调用putenv 向自己的环境变量中写入,此进程的子进程也能看到,但此进程的父进程看不到

程序替换 ,不影响环境变量(全局性)

关于环境变量:

1. 默认让子进程继承了父进程的环境变量

2.如果要传递全新的环境变量,自己定义,自己传递(带e的exec)

3.新增环境变量(putenv函数)

execl和execv的区别

就是使用方法的不同

cpp 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5     char* const argv[]={
  6    (char*)"ls",
  7    (char*) "-a",
  8    (char*)"-l",
  9         NULL
 10         };
 11     execv("/usr/bin/ls",argv);
 12 
 13     return 0;
 14 }

execvpe使用样例

other.c代码

cpp 复制代码
1 #include<stdio.h>
  2 
  3 int main()
  4 {
  5 
  6 extern char** environ;
  7 int i=0;
  8     for( ;environ[i];i++)
  9     {
 10         printf("env[%d]:%s\n",i,environ[i]);
 11     }
 12 return 0;
 13 }

myprocess.cc代码

cpp 复制代码
  1 #include <iostream>
  2 #include <cstdio>
  3 #include <unistd.h>
  4 #include <sys/types.h>
  5 #include <sys/wait.h>
  6 
  7 const std::string myenv="HELLO=AAAAAAAAAAAAAAAAAAAA";
  8 
  9 int main()
 10 {
 11     putenv((char*)myenv.c_str());
 12     pid_t id = fork();
 13     if(id == 0)
 14     {
 15         char *const argv[] = {
 16             (char*)"other",
 17             nullptr
 18         };
 19 
 20         char *const env[] = {
 21             (char*)"HELLO=linux",
 22             (char*)"HELLO1=linux1",
 23             (char*)"HELLO2=linux2",
 24             (char*)"HELLO3=linux3"
 25         };
 26 
 27         execvpe("./other", argv ,env);
 28         exit(1);
 29     }
 30 
 31     pid_t rid = waitpid(id, nullptr, 0);
 32     if(rid > 0)
 33     {
 34         printf("等待子进程成功!\n");
 35     }
 36     return 0;
 37 }

相关推荐
L_09071 分钟前
【Linux】进程状态
linux·开发语言·c++
啟明起鸣2 分钟前
【Nginx 网关开发】上手 Nginx,简简单单启动一个静态 html 页面
运维·c语言·前端·nginx·html
小生不才yz5 分钟前
shell编程 - 数据流指南
linux
lisanmengmeng10 分钟前
添加ceph节点
linux·服务器·ceph
Tinyundg14 分钟前
Linux系统分区
linux·运维·服务器
要做一个小太阳17 分钟前
华为Atlas 900 A3 SuperPoD 超节点网络架构
运维·服务器·网络·华为·架构
江畔何人初21 分钟前
service发现
linux·运维·云原生
life码农27 分钟前
Linux系统清空文件内容的几种方法
linux·运维·chrome
zbguolei32 分钟前
虚拟机安装Ubuntu后无法登录
linux·运维·ubuntu
UP_Continue35 分钟前
Linux--基础IO
linux·运维·服务器