Linux 进程控制

来源:互联网 发布:java api 1.6英文版 编辑:程序博客网 时间:2024/06/16 03:18

进程创建

在操作系统中 一旦开始运行一个可执行程序 该可执行程序在磁盘中的数据和代码被加载到进程中 且同时进程中出现了用于描述其性质的唯一与其对应的PCB 则该可执行程序的实例 它的进程 被创建出来了。

进程地址空间

了解进程地址空间是 学习进程控制的理论基础。
地址空间绝不是物理内存 它只是一个抽象概念。
关于地址空间的结构在我的另一篇博客中提到了:
http://blog.csdn.net/x__016meliorem/article/details/78741868

操作系统为每个进程提供一个假象——–好像他们每个进程在独占地址空间。
一台n位的机器上 地址空间是2的n次方个可能地址的集合 这个地址空间每个进程拥有一份。
linux的PCB(task_struct)中有一个指向 mm_struct结构体的指针
mm_struct结构体是用来描述进程地址空间的实体

这里是关于mm_struct的详细介绍
http://blog.csdn.net/qq_26768741/article/details/54375524

子进程:

根据任务的需要 创建子进程必不可少。
创建子进程的目的:
1. 一个应用程序有时不可避免要让用户感觉同时完成 同类或不同任务任务
2. init 1号进程是操作系统刚刚启动时由内核 引导执行的 他是所有linux进程的父进程 其他进程其实都是它创建出来的子进程。

创建一个子进程在linux下有两个系统调用

1. fork()

#include<unistd.h>pit   fork(void)

在子进程中返回 0 父进程中返回 子进程id 错误返回 -1;

fork()函数干了什么?

在一个进程的代码中 一旦调用了fork() 遂即创建了一个以自己为父进程的子进程。
fork()出的子进程是父进程的副本 但是拥有自己独立的虚拟地址空间
子进程从父进程那里继承了 数据和代码 堆、栈空间(主要是mm_struct的副本) 代码段父子进程共享、
数据理论上是相同的两份 实际上为了节省空间采用写时拷贝机制。
——–父子进程如果都没修改原数据段的数据 则父子进程实际上共用数据 如果有一方修改了数据段数据 则需要将原父进程数据段数据拷贝一份给子进程。

这里写图片描述

子进程继承父进程什么?

用户号UIDs和用户组号GIDs
环境变量
堆栈
共享内存
打开文件的描述符
执行时关闭(Close-on-exec)标志
信号(Signal)控制设定
进程组号
当前工作目录
根目录
文件方式创建屏蔽字
资源限制
控制终端

子进程独有什么?

进程号PID
不同的父进程号
自己的文件描述符和目录流的拷贝
子进程不继承父进程的进程正文(text),数据和其他锁定内存(memory locks)
不继承异步输入和输出

父进程和。。。
进程拥有独立的地址空间和PID参数。

fork()将创建成功的子进程添加到进程列表中 返回后 父子两个执行流从fork()之后的代码分流 谁先执行 由调度器决定 不缺定。
fork()之前只有父进程一个单独的执行流。

———-这个代码干了什么?

int main(){    pid_t pid1 = fork();    pid_t pid2 = fork();    printf("hello world");    exit(0);}

这里写图片描述

嵌套的fork()程序 程序调用了两次fork 程序运行了四个进程
每个进程调用的printf()可以任意顺序执行。

2. vfork()

  1. vfork() 与fork()不同的是 vfork()创建出的子进程在父进程的地址空间内运行
  2. vfork()保证子进程先运行 在子进程调用exec()或exit()后 调度器才可能让父进程运行。
#include<unistd.h>#include<stdio.h>#include<stdlib.h>int g_val = 0;int main(){    pid_t id = vfork();    if(id == 0 ){        sleep(3);        g_val = 100;        printf("I am Child pid:%d, ppid:%d,g_val:%d,&g_val:%p \n",\               getpid(),getppid(),g_val,&g_val);        exit(1);    }else if(id > 0){        sleep(2);        printf("I am Father pod:%d, ppid:%d, g_avl:%d, &g_val:%p \n",\               getpid(), getppid(),g_val,&g_val);    }else{        exit(2);    }    return 0;}

程序第九行的fork()函数被改成了vfork()函数
1. 子进程睡眠 3秒 父进程睡眠 2 秒 如果这里是fork() 必然父进程先执行 可输出结果确是子进程先知执行 exit(1)后 父进程才执行。

  1. 这里输出的结果 父子进程的g_val都是 子进程修改后的值 100 而不是初始化的
    20证明了子进程在父进程的地址空间内运行。

这里写图片描述

这个特点和线程类似 线程就是在进程的地址空间内运行

进程终止

  1. 进程退出的场景
    1. 代码运行完毕 ,结果正确
    2. 代码运行完毕, 结果错误
    3. 代码没有运行完毕 异常终止 (可能是收到了信号)

ctrl + c 其实就是向一个前台进程 发送一个终止进程的信号。

_exit 函数

#include<unistd.h>void _exit(int status);

参数status定义了 进程的推出状态 父进程通过wait函数来获取该值 int status的最低八位表示子进程退出时收到的信号
次低八位表示正常退出时的推出码。

&? 命令可以看到上一个执行结束进程的退出码。

exit函数

#include<unistd.h>void exit(int status);

exit函数最后也会调用 _exit函数 调用前了干三件事
1. 调用atexit() on_exit() 定义的数理函数。
2. 关闭所有流 刷新所有缓冲区。

#include <stdio.h>#include<unistd.h>#include<stdlib.h>void Func(){    printf("Func Runing!\n");    //exit(1);}void Funcc(){    printf("Funcc Done\n");}int main(){    Func();    atexit(Funcc);//call user self-define clean Func and than  exit process     printf("Func Done!\n");    printf("main Done!");   // _exit(0);    exit(0);    //return 0;}

上述代码调用_exit() 与exit() 的区别 这里有证明:

这里写图片描述

return

main() 函数的返回值 return n; 实际上是exit()函数中的退出码。

进程等待

  1. 进程等待的必要性
    父进程对其创建出的子进程有义务回收其退出信息
    如果子进程退出 父进程不回收子进程退出状态 父进程自己又不退出就会造成“僵尸进程问题” 导致内存泄漏。(子进程退出时 关闭所有自己的文件描述符 释放在用户空间分配的内存 但他的PCB还保留着 里面存放它的推出信息 需要父进程接收 则是PCB才会被释放 )
    一个进程一旦成为僵尸进程 连kill -9 信号也无法杀死 。
    父进程派给子进程的任务 父进程有必要必须知道它的完成情况。
    父进程通过进程等待方式 回收子进程退出信息 系统回收资源

  2. 进程等待的方法

wait函数

#include <sys/types.h>#include <sys/wait.h>pid_t wait(int *status);

等待成功返回被等待进程的pid 失败返回

参数为输出型参数 函数调用完成后存储子进程的退出状态
如果父进程不关心子进程推出状态 参数传NULL;

#include<unistd.h>#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int g_val = 0;int main(){    pid_t id = fork();    if(id == 0 ){        g_val = 100;        while(1){            printf("I am Child pid:%d, ppid:%d,g_val:%d,&g_val:%p \n",\               getpid(),getppid(),g_val,&g_val);            sleep(3);            exit(123);        }        //exit(1);    }else if(id > 0){        //sleep(2);        printf("I am Father pod:%d, ppid:%d, g_avl:%d, &g_val:%p \n",\               getpid(), getppid(),g_val,&g_val);    }else{        perror("vfork");    }    int status = 0;    pid_t ret = wait(&status);    if(ret > 0){          printf("wait suzzess ! ret : %d status :%d\n", ret, status);    }    return 0;}

这个小程序子进程也调用了wait 但是因为它没有子进程 所以出错返回-1
只有父进程调用的wait 返回子进程的pid (大于一) 所以有打打印

这里可以看出来 父进程执行完毕后 没有直接退出 而是等待子进程exit后拿到子进程推出信息status 释放了子进程的PCB (status后面介绍)

这里需要说明一点是 wait函数等待子进程退出是阻塞式等待
只要子进程没有退出 父进程什么也不干 就只等待。父进程代码停在wait函数里面

这里写图片描述

waitpid函数

       #include <sys/types.h>       #include <sys/wait.h>       pid_t waitpid(pid_t pid, int *status, int options)`

waitpid 函数的返回值
1. 如果等待成功 返回被等待的子进程id
2. waitpid 可以选择非阻塞方式等待(轮询) 即 多次查看子进程是否有子进程推出 有的话回收其推出信息 没有的话 父进程先运行其他代码 。 一段时间后再检查是否有子进程退出。 第三个参数options如果设置为 WHOHANG 表示为轮询等待 如果这一次等待时没有子进程退出 则 返回0. options参数设置为0表示阻塞等待。
3. 如果调用失败返回 -1 errno自动被设置为相应的值指示错误所在

参数 pid == -1 表示等待任意一个进程 参数 pid > 0表示要等待的参数的pid

status参数和wait函数的status参数一样
status 的最低八位表示 如果进程接到信号而被杀死的话 接收到的信号是什么 。
status的次第八位表示进程 正常退出时的退出码。

阻塞式waitpid 程序正常退出

#include<unistd.h>#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int g_val = 0;int main(){    pid_t id = fork();    if(id == 0 ){        g_val = 100;        while(1){            printf("I am Child pid:%d, ppid:%d,g_val:%d,&g_val:%p \n",\               getpid(),getppid(),g_val,&g_val);            sleep(3);            exit(123);        }        //exit(1);    }else if(id > 0){        //sleep(2);        printf("I am Father pod:%d, ppid:%d, g_avl:%d, &g_val:%p \n",\               getpid(), getppid(),g_val,&g_val);    }else{        perror("vfork");    }    int status = 0;    pid_t ret = waitpid(id,&status,0);    if(ret > 0){       printf("ret:%d, status :%d, exit code: %d, sig: %d\n",ret, status, (status >> 8)&0xff, (status)&0xff);                 }    return 0;}

这里写图片描述

轮询waitpid 用信号杀死进程

介绍四个宏
WIFEXITED(status) //返回子进程是否是正常退出的
WIFSIGNALED(status)//返回子进程是否是收到信号退出的
WEXITSTATUS(status)//返回子进程正常退出退出状态
WTERMSIG(status)//返回子进程收到的信号
(这里所讲的正常退出指的是代码跑完了 并不关心结果是否正确)

#include<unistd.h>#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int g_val = 0;int main(){    pid_t id = fork();    if(id == 0 ){        g_val = 100;        while(1){            printf("I am Child pid:%d, ppid:%d,g_val:%d,&g_val:%p \n",\               getpid(),getppid(),g_val,&g_val);            sleep(3);            //exit(123);        }        exit(1);    }else if(id > 0){        //sleep(2);        printf("I am Father pod:%d, ppid:%d, g_avl:%d, &g_val:%p \n",\               getpid(), getppid(),g_val,&g_val);    }else{        perror("vfork");    }    int status = 0;    do{        pid_t ret = waitpid(id,&status,WNOHANG);        if(ret == 0){            printf("Father doing other things!\n");            sleep(1);        }else if(ret > 0){            if(WIFEXITED(status)){//正常退出                printf("child running done exit code : %d \n",WEXITSTATUS(status));            }else if(WIFSIGNALED(status)){                 printf("child is quit, but it is killed SIG :%d", WTERMSIG(status));            }            break;        }else{            printf("wait failed!\n");    break;        }    }while(1);    return 0;}

这里写图片描述

进程程序替换

进程程序替换是个重点 应用场景十分广泛

原理图:
这里写图片描述

fork()创建子进程 运行的是从父进程地址空间拷贝的代码 和父进程一样 运行时可能运行不同的代码分支
很多时候需要 子进程完成一项全新的和父进程无关联的任务
这时调用 exec系列函数 将进程的数据和代码全部替换为 新程序的数据和代码 但是它不创建新的PCB 原进程id也不变。

shell脚本输入命令实际上就是 fork()子进程之后再exec

exec系列函数

#include <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[]);      #include <unistd.h>       int execve(const char *filename, char *const argv[],                  char *const envp[]);

解释 :如此多exec系列函数如何使用?

有 p 的要输入替换的程序的名字 系统自己程序路径
没有p的 要输入程序的路径

有 l 的 后几个参数都要输入 原本在命令行输入的 命令和参数 作为一个一个的参数传递 最后一定用用NULL表示结束。
有v的 将原在命令行上的命令和参数以数组形式传入

exec系列函数代码 将子进程代码和数据替换成了 ls -a -i -l
父进程创建子进程后 只需要等待子进程即可
exec函数只有失败返回值 -1 没有成功返回值 成功后进程代码和数据都被替换了

 #include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>#include<unistd.h>int main(){    pid_t id = fork();    if(id == 0){        printf("execl running \n");        //sleep(1);        //execl("/bin/ls","ls","-a","-l","-n",NULL);        //execlp("ls","ls","-a","-l","-n",NULL);        char* _argv[] = {            "ls", "-a","-l","-n",NULL        };        execl("/bin/ls","ls","-a","-i","-n",NULL);        //execv("/bin/ls",_argv);        //execvp("ls",_argv);        //execlp("ls","ls","-l","-i",NULL);        exit(1);    }else{       pid_t ret =  waitpid(id,NULL,0);       if(ret > 0){            printf("waitpid success\n");       } }    return 0;}

这里写图片描述
issolve/70/gravity/SouthEast)

exec vp execlp 函数可以向替换的程序传递环境变量

[ym@localhost process]$ cat mybin.c#include<stdio.h>#include<stdlib.h>int main(){    printf("I am mybin: %s\n",getenv("MYENV"));    return ;}

这里有一个mybin.c文件打印 “MYENV”环境变量 这个环境变量并不存在 所以改代码运行结果为null。
这里写图片描述

用execle函数将mybin程序的代码和数据 替换到子进程中 并传给他还境变量 输出结果就不同了

#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>#include<unistd.h>int main(){    pid_t id = fork();    if(id == 0){        printf("execl running \n");        char* _env[] = {"MYENV=/FORTEST/" , NULL};        execle("./mybin","mybin",NULL,_env);        exit(1);    }else{       pid_t ret =  waitpid(id,NULL,0);       if(ret > 0){            printf("waitpid success\n");       }    }`    return 0;}

这里写图片描述

几个exec函数之间的关系

这里写图片描述

函数与进程的相似性:
函数有自己的栈帧 进程有自己的地址空间
函数有参数返回值 进程有exec中的参数 exit()中的返回值
函数有call return 进程有frok exec exit
通过参数和返回值这在拥有私有数据函数之间的调度 也运用到了进程之间   linux鼓励这种模式扩展到进程之间。

原创粉丝点击