Linux系统编程——进程控制

进程控制

  • 1. 进程相关
    • 1.1 CPU 与 MMU
      • 1.1.1 CPU
      • 1.1.2 MMU(内存管理单元)
    • 1.3 进程控制块PCB
  • 2. 环境变量
    • 2.1 常见环境变量
    • 2.2 相关环境变量函数
      • 2.2.1 getenv函数
      • 2.2.2 setenv函数
      • 2.2.1 unsetenv函数
  • 3. 进程控制
    • 3.1 进程ID相关函数
    • 3.2 子进程创建
      • 3.2.1 创建一个子进程
      • 3.2.2 循环创建n各子进程
    • 3.3 进程共享
    • 3.4 gdb调试
  • 4. exec函数族
    • 4.1 execlp函数
    • 4.2 execl函数
    • 4.4 exec函数族一般规律
    • 4.5 例子:将当前系统中的进程信息打印到文件中
  • 5. 回收子进程
    • 5.1 孤儿进程
    • 5.2 僵尸进程
    • 5.3 wait函数
    • 5.4 waitpid函数

1. 进程相关

1.1 CPU 与 MMU

1.1.1 CPU

Linux系统编程——进程控制_第1张图片
CPU 执行一条指令过程:预取器先从cache或者内存中取出一条指令,然后交给译码器分析,译码器分析该条指令需要用到哪个寄存器,并把相关数据存储到对应的寄存器中,ALU对其进行运算,然后把数据回写到寄存器中,最后再把数据放到缓冲区,然后由内存把数据传输到总线,再显示到设备上

1.1.2 MMU(内存管理单元)

Linux系统编程——进程控制_第2张图片
MMU功能

  • 虚拟内存与物理内存的映射
  • 设置修改内存访问级别

虚拟地址:可用地址空间0-4G
假如虚拟地址使用了2KB,那么映射到物理内存大小应该为4KB,因为一个page作为物理内存的最小单位,大小为4KB。

1.3 进程控制块PCB

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
/usr/src/linux-headers-3.16.0-30(可能不一样)/include/linux/sched.h文件中可以查看struct task_struct 结构体定义。

其内部成员有很多,重点掌握以下部分即可:

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
  • 进程的状态,有就绪、运行、挂起、停止等状态。
  • 进程切换时需要保存和恢复的一些CPU寄存器。
  • 描述虚拟地址空间的信息。
  • 描述控制终端的信息。
  • 当前工作目录(Current Working Directory)。
  • umask掩码。
  • 文件描述符表,包含很多指向file结构体的指针。
  • 和信号相关的信息。
  • 用户id和组id。
  • 会话(Session)和进程组。
  • 进程可以使用的资源上限(Resource Limit)。ulimit -a

2. 环境变量

环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数

通常具备以下特征

  • ① 字符串(本质)
  • ② 有统一的格式:名=值[:值]
  • ③ 值用来描述进程环境信息。

存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
使用形式:与命令行参数类似。
加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。
引入环境变量表:须声明环境变量。extern char ** environ;

2.1 常见环境变量

按照惯例,环境变量字符串都是name=value这样的形式,大多数name由大写字母加下划线组成,一般把name的部分叫做环境变量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
PATH
可执行文件的搜索路径。ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以查看这个环境变量的值:echo $PATH

SHELL
当前Shell,它的值通常是/bin/bash。echo $SHELL
TERM
当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
LANG
语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME
当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。

练习:打印当前进程的所有环境变量。

#include 
#include 
using namespace std;
extern char **environ;//须声明环境变量

int main(){
    for(int i = 0;environ[i];i++){
        cout << environ[i] << endl;
    }
    return 0;
}

2.2 相关环境变量函数

2.2.1 getenv函数

  • 获取环境变量值
  • char *getenv(const char *name);
    • 成功:返回环境变量的值;
    • 失败:NULL (name不存在)

2.2.2 setenv函数

  • 设置或添加环境变量的值
  • int setenv(const char *name, const char *value, int overwrite);
    • 成功:0;失败:-1
    • 参数overwrite取值:
      • 1:覆盖原环境变量
      • 0:不覆盖。(该参数常用于设置新环境变量,如:ABC = haha-day-night)

2.2.1 unsetenv函数

  • 删除环境变量name的定义
  • int unsetenv(const char *name);
    • 成功:0;失败:-1
  • 注意事项:name不存在仍返回0(成功),当name命名为"ABC="时则会出错。
#include 
#include 
#include 
using namespace std;

int main(){
    
    const char* name  = "ABD";
    char *val;
    val = getenv(name);//获取环境变量ABD的值,此时环境变量不存在,所以为空
    if(val){
        cout << name << ": " << val << endl;
    }else{
        cout << "环境变量不存在" << endl;
    }

    setenv(name,"day-day-up",1);//设置环境变量的值为day-day-up
    val = getenv(name);
    cout << name << ": " << val << endl;

    int ret  = unsetenv("ABCD");//删除环境变量的定义
    cout << "环境变量ABCD 的值为:" << ret << endl;

    ret = unsetenv("ABD");
    cout << "环境变量ABD的值为:" << ret << endl;
    cout << getenv(name) << endl;
    return 0;
}

3. 进程控制

3.1 进程ID相关函数

函数 函数原型 说明
getpid函数 pid_t getpid(void); 获取当前进程ID
getppid函数 pid_t getppid(void); 获取当前进程的父进程ID
getuid函数 uid_t getuid(void); 获取当前进程实际用户ID
geteuid()函数 uid_t geteuid(void); 获取当前进程有效用户ID
getgid函数 gid_t getgid(void); 获取当前进程使用用户组ID
getegid函数 gid_t getegid(void); 获取当前进程有效用户组ID

区分一个函数是“系统函数”还是“库函数”依据:

  • 是否访问内核数据结构
  • 是否访问外部硬件资源 二者有任一 → 系统函数;二者均无 → 库函数

3.2 子进程创建

3.2.1 创建一个子进程

fork函数:创建一个子进程

pid_t fork(void);

  • 失败返回-1;成功返回两个值:① 父进程返回子进程的ID(非负) ②子进程返回 0
  • pid_t类型表示进程ID,但为了表示-1,它是有符号整型。(0不是有效进程ID,init最小,为1)
  • 注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需各自返回一个
#include 
#include 
#include 
using namespace std;

int main(){

    cout << "testtesttesttest!!!" << endl;
    pid_t pid = fork();

    if(pid == -1){
        perror("fork");
        exit(1);
    }else if(pid == 0){
        cout << "此进程为子进程" << endl;
        cout << "父进程pid为:" << getppid() << endl;
        cout << "子进程pid为:" << getpid() << endl;
    }else{
        cout << "此进程为父进程" << endl;
        cout << "父进程pid为:" << getppid() << endl;
        cout << "子进程pid为:" << getpid() << endl;
        sleep(1);
    }
    cout << "hellohellohello!!!" << endl;
    return 0;
}

Linux系统编程——进程控制_第3张图片
说明:

  • testtesttest!!! 打印一次,而hellohellohello!!!打印了两次,是因为fork()函数出现在testtesttest!!!之后,此句只有父进程执行,而hellohellohello!!!在fork()函数之后,此时创建了一个子进程,父子进程各执行一次,所以出现两次
  • 当前进程的父进程为bash,如上所式 ps aux | grep 22865

3.2.2 循环创建n各子进程

使用for(i = 0; i < n; i++) { fork(); } 创建的子进程:
Linux系统编程——进程控制_第4张图片

  • 当n为3时候,循环创建了(2^n)-1 个子进程,而不是N个子进程。
  • 需要在循环的过程,保证子进程不再执行fork ,因此当(fork() == 0)时,子进程应该立即break;才正确。
//循环创建5个子进程
#include 
#include 
#include 
using namespace std;

int main(){
    cout << "创建5个子进程" << endl;
    pid_t pid;
    int i;
    for(i = 0;i < 5;i++){
        pid = fork();
        if(pid == 0){//子进程就跳出此循环,防止为子进程创建进程
            break;
        }
    }
    
    if(i < 5){//子进程,break跳出后执行后续程序
        sleep(i);//保证进程输出的顺序
        cout << "I am " << i + 1 << " child fork!!!" << endl;
    }else{  //父进程
        sleep(i);
        cout << "I am parent fork!!!" << endl;
    }
    return 0;
}

Linux系统编程——进程控制_第5张图片
一次循环结束后,父进程会执行下一次循环,子进程会跳出循环执行循环后的内容,父子进程执行先后顺序取决于谁先抢到CPU资源,并不能保证其执行顺序(一般是父进程先执行,但是没有理论依据),如下图(注释程序中sleep(i)后的运行结果,其中shell进程也参与cpu资源争夺)。
Linux系统编程——进程控制_第6张图片

3.3 进程共享

父子进程遵循原则:读时共享写时复制

刚fork之后:

父子相同处: 全局变量(不能共享)、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…

父子不同处:

  • 1.进程ID
  • 2.fork返回值
  • 3.父进程ID
  • 4.进程运行时间
  • 5.闹钟(定时器)
  • 6.未决信号集

       似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
       当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。

【重点】:父子进程共享:

  • 文件描述符(打开文件的结构体)
  • mmap建立的映射区 (进程间通信详解)

3.4 gdb调试

       使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程

  • set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。
  • set follow-fork-mode parent 设置跟踪父进程。

       注意,一定要在fork函数调用之前设置才有效

4. exec函数族

       fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
       将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。

其实有六种以exec开头的函数,统称exec函数:
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 execve(const char *path, char *const argv[], char *const envp[]);

4.1 execlp函数

加载一个进程,借助PATH环境变量

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

  • 成功:无返回;失败:-1
  • 参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
  • 该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
//实现ls -la
#include 
#include 
#include 
#include 
using namespace std;

int main(){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error");
        exit(1);
    }else if(pid > 0){
        sleep(2);
        cout << "father fork" << endl;
    }else{
    	//execlp函数
        execlp("ls","ls","-l","-a",NULL);//实现ls -la
    }
    return 0;
}

4.2 execl函数

加载一个进程, 通过 路径+程序名 来加载。可利用子进程执行自己写的程序
int execl(const char *path, const char *arg, ...);

  • 成功:无返回;失败:-1
  • 对比execlp,如加载"ls"命令带有-l,-a参数
  • execlp(“ls”, “ls”, “-l”, “-a”, NULL); 使用程序名在PATH中搜索。
  • execl("/bin/ls", “ls”, “-l”, “-a”, NULL); 使用参数1给出的绝对路径搜索。
//实现ls -la
#include 
#include 
#include 
#include 
using namespace std;

int main(){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error");
        exit(1);
    }else if(pid > 0){
        sleep(2);
        cout << "father fork" << endl;
    }else{
        //execl函数
        execl("/bin/ls","ls","-l","-a",NULL);//实现ls -la
        //execl("./hello.cpp","hello",NULL);//运行hello.cpp文件 
    }
    return 0;
}

4.4 exec函数族一般规律

exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。

  • l (list) 命令行参数列表
  • p (path) 搜素file时使用path变量
  • v (vector) 使用命令行参数数组
  • e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量
    事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
    Linux系统编程——进程控制_第7张图片

4.5 例子:将当前系统中的进程信息打印到文件中

命令行方式:ps aux > 文件

#include 
#include 
#include 
#include 
using namespace std;

int main(){
    int fd = open("out.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
    if(fd < 0){
        perror("open error");
        exit(1);
    }
    dup2(fd,STDOUT_FILENO);//dup2(3,1),3相当于打开的文件:out.txt,而1是标准输出standout
    execlp("ps","ps","aux",NULL);
    return 0;
}

Linux系统编程——进程控制_第8张图片

5. 回收子进程

5.1 孤儿进程

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

#include 
#include 
#include 
using namespace std;

int main(){
    pid_t pid = fork();

    if(pid == -1){
        perror("fork error");
        exit(1);
    }else if(pid > 0){
        cout << "I am parent,my pid is " << getpid() << endl;
        sleep(8);
        cout << "----------- parent going to die -------------"<< endl;
    }else{
        while(1){
            cout << "I am child,my parent pid is " << getppid() << endl;
            sleep(1);
        }
    }
    return 0;
}

子进程每隔一秒打印一次,父进程在八秒后结束,此时子进程还没结束,成为孤儿进程
Linux系统编程——进程控制_第9张图片

5.2 僵尸进程

       僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
       特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止

#include 
#include 
#include 
using namespace std;

int main(){
    pid_t pid = fork();

    if(pid == 0){
        cout << "child ,my father pid is " << getppid() << endl;
        sleep(8);
        cout << "--------child die -------" << endl;
    }else if(pid > 0){
        while(1){
            cout << "I am father,my pid is " << getpid() << "  myson pid is " << pid << endl;
            sleep(1);
        }
    }else{
        perror("fork error");
        exit(1);
    }
    return 0;
}

ps aux在这里插入图片描述

5.3 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) 为真 → 进程暂停后已经继续运行

#include 
#include 
#include 
using namespace std;

int main(){
    pid_t pid = fork();
    int status;//wait(&status)
    if(pid == 0){
        cout << "child ,my father pid is " << getppid() << endl;
        sleep(30);
        cout << "--------child die -------" << endl;
        exit(66);
    }else if(pid > 0){
       // pid_t wpid = wait(NULL); //一般回收
        pid_t wpid = wait(&status);
        if(wpid == -1){
            perror("wait error");
            exit(1);
        }
        //正常结束
        if(WIFEXITED(status)){
            cout << "child exit with " << WEXITSTATUS(status) << endl;
        }
        //异常终止
        if(WIFSIGNALED(status)){
            cout << "child killed by " << WTERMSIG(status) << endl;
        }

        while(1){
            cout << "I am father,my pid is " << getpid() << "  myson pid is " << pid << endl;
            sleep(1);
        }
    }else{
        perror("fork error");
        exit(1);
    }
    return 0;
}

打开另一个终端窗口,手动发送信号 9 杀死子进程,此时子进程异常终止,返回终止信号 9。
在这里插入图片描述
Linux系统编程——进程控制_第10张图片

5.4 waitpid函数

作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options);

  • 成功:返回清理掉的子进程ID;
  • 失败:-1(无子进程)

特殊参数和返回情况:

  • 参数pid:
    • 大于0: 回收指定ID的子进程
    • -1 :回收任意子进程(相当于wait)
    • 0 :回收和当前调用waitpid一个组的所有子进程
    • 小于-1 :回收指定进程组内的任意子进程
  • 参3
    • WNOHANG,非阻塞回收(轮询),且子进程正在运行。
    • 0 : (wait)阻塞回收
  • 返回值
    • 成功:pid
    • 失败:-1
    • 返回0 : 参数3为WNOHANG,且子进程正在运行
      注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

waitpid(-1,NULL,0); 相当于 wait(NULL);

你可能感兴趣的:(Linux系统编程,linux,系统编程,操作系统)