进程相关知识

程序和进程

程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、
设备、锁…)
进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资
源。在内存中执行。(程序运行起来,产生一个进程)
程序 → 剧本(纸) 进程 → 戏(舞台、演员、灯光、道具…)
同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼
此之间互不影响)
如:同时开两个终端。各自都有一个 bash 但彼此 ID 不同。

并发

并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状
态。但,任一个时刻点上仍只有一个进程在运行。
例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做
一个进程的话,为什么可以同时运行呢,因为并发
在这里插入图片描述
把一段时间t按时间片划分多分,每个进程只能在分到的时间段运行,时间到后cpu就执行下一个得到时间片的程序,但是某一个时刻只能有一个程序在运行。cpu的处理速度很快,所以就显得多个程序在同时运行。

MMU内存管理单元

中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统
cpu不直接操作物理内存地址,而是用过MMU把物理地址映射为虚拟地址,在进行操作

虚拟地址与物理地址: 与虚拟地址空间和虚拟地址相对应的是物理地址空间和物理地址;物理地址空间只是虚拟地址空间的一个子集。如一台内存为256MB的32bit X86主机,其虚拟地址空间是0 ~ 0xffffffff(4GB),物理地址空间范围是0 ~ 0x0fff ffff(256M)
图下就是数据流向
在这里插入图片描述

以32位机子的虚拟地址为例
地址划分是03G为用户空间,3G4G为内核空间。
在一个程序开辟一个大的数组,在虚拟地址是连续的,但是在物理地址不一定连续,这可以充分利用物理内存。
2个进程的的用户空间通过MMU映射到物理内存是相互独立的,但是内核空间却是公用的。

在这里插入图片描述

进程状态

进程基本的状态有 5 种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始
态为进程准备阶段,常与就绪态结合来看
在这里插入图片描述

进程控制

fork 函数

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

getpid 函数
获取当前进程 ID
pid_t getpid(void);
getppid 函数
获取当前进程的父进程 ID
pid_t getppid(void);


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

int main(int argc, char *argv[])
{
    

    printf("fork函数前,只有父进程执行过这代码,子进程有这代码,但是不执行\n");

    pid_t pid=fork();
    if(pid==-1)
    {
        perror("fork error\n");
    }else if(pid==0)
    {
        printf("子进程:pid号=%d,他的父进程pid号=%d\n",getpid(),getppid());
    }else
    {
        printf("父进程:子进程pid号=%d,父进程pid号=%d\n",pid,getpid());
    }

    printf("父 子进程一起会执行的代码\n");
    return 0;
}

创建n个子进程

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

#define N   2
int main(int argc, char *argv[])
{
    int i=0;
    for( i = 0; i < N; i++)
    {
        pid_t pid =fork();

        if(pid==0)
        {
            printf("son :pid=%d,parent=%d\n",getpid(),getppid());
            break;
        }
        if(pid>0)
        {
            printf("parent:pid=%d ,son:pid=%d\n",getpid(),pid);
        }

    }
    return 0;
}

在这里插入图片描述
如果这行代码没有break,即子进程又会创子进程那么会得到(2^n)-1个子进程
if(pid==0)
{
printf(“son :pid=%d,parent=%d\n”,getpid(),getppid());
break;//这个判断是pid=0,就是子进程的时候就break出来
}
在这里插入图片描述

进程共享

父子进程之间在 fork 后。有哪些相同,那些相异之处呢?
刚 fork 之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户 ID、宿主目录、进程工
作目录、信号处理方式…
父子不同处: 1.进程 ID 2.fork 返回值 3.父进程 ID 4.进程运行时间 5.闹钟(定
时器) 6.未决信号集
似乎,子进程复制了父进程 0-3G 用户空间内容,以及父进程的 PCB,但 pid 不同。真
的每 fork 一个子进程都要将父进程的 0-3G 地址空间完全拷贝一份,然后在映射至物理内存
吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进
程的逻辑还是执行自己的逻辑都能节省内存开销。
练习:编写程序测试,父子进程是否共享全局变。
【fork_shared.c】
重点注意!躲避父子进程共享全局变量的知识误区!
【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap 建立的映射区
(进程间通信详解)
特别的,fork 之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算

ps ajx 命令

查看 pid ppid gid sid

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

int main(int argc, char *argv[])
{
    
    int num = 100;

    printf("fork函数前,num=%d\n", num);

    pid_t pid=fork();
    if(pid==-1)
    {
        perror("fork error\n");
    }else if(pid==0)
    {
        num =200;
        printf("num被修改为200,num=%d\n",num);
    }else
    {
        #if 0//写时复制
        num =300;
        printf("num被修改为300,num=%d\n",num);       
        #endif

        #if 1//读时共享
        printf("num不修改,num=%d\n",num);       
        #endif
    }
    return 0;
}

gdb 调试

使用 gdb 调试的时候,gdb 只能跟踪一个进程。可以在 fork 函数调用之前,通过指令设
置 gdb 调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。
set follow-fork-mode child 命令设置 gdb 在 fork 之后跟踪子进程。
set follow-fork-mode parent 设置跟踪父进程。
注意,一定要在 fork 函数调用之前设置才有效。

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

execlp 函数

加载一个进程,借助 PATH 环境变量
int execlp(const char *file, const char *arg, …); 成功:无返回;失败:-1
参数 1:要加载的程序的名字。该函数需要配合 PATH 环境变量来使用,当 PATH 中
所有目录搜索后没有参数 1 则出错返回。
该函数通常用来调用系统程序。如:ls、date、cp、cat 等命令。

execl 函数

加载一个进程, 通过 路径+程序名 来加载。
int execl(const char *path, const char *arg, …); 成功:无返回;失败:-1
对比 execlp,如加载"ls"命令带有-l,-F 参数
execlp(“ls”, “ls”, “-l”, “-F”, NULL); 使用程序名在 PATH 中搜索。
execl("/bin/ls", “ls”, “-l”, “-F”, NULL); 使用参数 1 给出的绝对路径搜索。

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

int main(int argc, char *argv[])
{

    pid_t pid=fork();
    if(pid==-1)
    {
        perror("fork error\n");
        exit(1);
    }else if(pid==0)
    {
        printf("子进程调用execlp-》ls查看目录\n");

        #if 1 //用绝对路径
        execl("/bin/ls", "ls", "-l",  NULL);
        #endif 

        #if 1 //利用path
        execlp("ls", "ls", "-l",  NULL);
        #endif  
        perror("exec");//如果exec函数正常调用就不会执行下面代码
        exit(1);
    }else
    {
          printf("父进程\n");
    }

    printf("如果没调用exc函数族,或者调用失败,那么子进程也会跑到这里来\n");
    return 0;
}

运行自己写的程序
被调用的程序

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


int main(int argc, char *argv[])
{
   int i=0;
    printf("被exec调用的程序\n");
    for( i = 0; i < 3; i++)
    {
        sleep(2);
        printf("%s\n",argv[i+1]);
    }
    return 0;
}

execl程序

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

int main(int argc, char *argv[])
{

    pid_t pid=fork();
    if(pid==-1)
    {
        perror("fork error\n");
        exit(1);
    }else if(pid==0)
    {
        printf("子进程调用execlp-》ls查看目录\n");

        #if 1 //用绝对路径或相对路径
        execl("./exec", "exec", "aa","bbb","ccc",  NULL);
        #endif 
        perror("exec");//如果exec函数正常调用就不会执行下面代码
        exit(1);
    }else
    {
          printf("父进程\n");
    }

    printf("如果没调用exc函数族,或者调用失败,那么子进程也会跑到这里来\n");
    return 0;
}

结果如下
在这里插入图片描述

exec 函数族一般规律

exec 函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通
常我们直接在 exec 函数调用后直接调用 perror()和 exit(),无需 if 判断。
l (list) 命令行参数列表
p (path) 搜素 file 时使用 path 变量
v (vector) 使用命令行参数数组
e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运
行的环境变量
事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用 execve,
回收子进程

孤儿进程

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

用ps ajx查看进程状态

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

int main(int argc, char *argv[])
{
    

    printf("fork函数前,只有父进程执行过这代码,子进程有这代码,但是不执行\n");

    pid_t pid=fork();
    if(pid==-1)
    {
        perror("fork error\n");
    }else if(pid==0)
    {
        printf("子进程:pid号=%d,他的父进程pid号=%d\n",getpid(),getppid());
        sleep(20);
    }else
    {
        
        sleep(5);
        printf("子进程pid号=%d成为孤儿\n",pid);
    }

    return 0;
}

僵尸进程

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

特别注意,僵尸进程是不能使用 kill 命令清除掉的。因为 kill 命令只是用来终止进程的,任何程序都会有僵尸态
而僵尸进程已经终止

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 调用只能清理一个子进程,清理多个子进程应使用循环。
【waitpid.c】

来自黑马程序员教案,本人摘抄并尝试实现代码以便自己学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值