Linux多进程编程(典藏、含代码)

目录

一、基础知识

1.1、进程的概念

1.2、多进程(任务)并行的实现

1.3、重要指令

1.4、父子进程和进程ID

二、多进程编程

2.1创建子进程 (fork/vfork 叉子)

2.1.1 fork

2.1.2 vfork

2.2进程结束

2.2.1正常退出

2.2.2异常退出

2.3等待进程结束并资源回收 

2.3.1 wait函数

2.3.2 waitpid 函数

2.4替换进程内容(exec系列函数)


一、基础知识

1.1、进程的概念

一个程序文件, 只是一堆待执行的代码和部分待处理的数据
它们只有被加载到内存中,然后让CPU逐条执行其代码,根据代码做出相应的动作,才形成一个真正“活的”、动态的 进程(Process)
因此, 进程是一个动态变化的过程,是一出有始有终的戏
而程序文件只是这一系列动作的原始蓝本,是一个静态的剧本
 
1、进程就是程序在内存中 动态执行的过程
2、进程是系统资源管理的 最小的单位
3、进程是动态的概念, 创建—运行--消亡
4、每个进程有 4G独立的进程空间,其中0-3G是用户空间,3G-4G是内核空间。 每个进程也有4G地址空间的,仅仅是地址空间,不是实际的内存,需要使用时,向系统申请
5、进程是独立可调度的任务,绝大多数的操作系统都支持多进程
 
 
 
 

1.2、多进程(任务)并行的实现

进程我们了解了,其实就可以说是一个后台运行的任务

多进程(多任务)并行就好比如我们的电脑可以上谷歌浏览器、微信、网易云音乐 

它们都是一个个任务,在执行各自的功能

看上去它们“同时”一起运行

对吧~

 

但是实际上,对于一个单核CPU来讲
宏观上是并行的,而从 微观上是串行的
它使用 时间片划分周期调用来实现,每个任务在一段时间内会分到一段 时间片(占cpu的时间),在这段时间内该任务只能运行时间片长度,每个任务执行一点每个任务执行一点,从而达到“同时”的效果
 
 
而每个任务是如何调度和切换的?
这是由 系统调度器来实现
所以使得任务有多种状态
 就绪状态: 未占到CPU, 进程准备好了,等待系统调度器调度。
 运行状态: 占到CPU  , 已经开始运行。
 暂停状态: 没占,收到外部暂停信号,暂停运行 (不在参与任务调度)
 挂起(睡眠)状态: IO资源不满足, 导致进程睡眠。 (不在参与任务调度)(例如键盘输入)
 僵尸状态: 进程已经结束, 但是资源(内存、硬件接口)没有回收。

Linux多进程编程(典藏、含代码)_第1张图片

就这样
任务被切来切去
我一点你一点(指的是cpu抢占时间)
最后达到宏观上的并行运行
 
    
 
 

1.3、重要指令

(1) ps ——— 查看进程信息

    ps aux ——— 显示系统所有的进程
    ps -elf  ———   显示系统所有的进程(通用)

(2)top ————   动态查看进程信息

(3)pstree ———— 查看父子关系结构的进程

(4)kill -9 进程号   ———   杀死进程。(通过信号)

 

1.4、父子进程和进程ID

Linux中的进程都是由其它进程启动。如果进程a启动了进程b, 所以称a是b的父进程, b是a的子进程

 

Linux启动时,0进程启动1号进程(init )和2号进程(内核线程), 0号进程退出, 其它进程是由1、2直接或间接产生

       1号进程(init ) 所有用户进程的祖先

       2号进程(内核线程) 内核进程的祖先

 
 

进程号 PID (process ID)(类型pid_t , 什么什么_t 都是正整数) : 

        每个任务拥有唯一ID, 由操作系统管理和分配

        每个进程创建会分配一个ID , 结束会取消一个ID

        取消的那个ID会延时重复使用 , 但不会同时出现同一个ID 

        相关函数:

        函数getpid() 获取本进程的ID

        函数getppid() 获取父进程的ID (get perent pid)

 

(小彩蛋) 想让程序在后台运行执行指令  

./执行文件 &

 

 
 

二、多进程编程

2.1创建子进程 (fork/vfork 叉子)

2.1.1 fork

Linux多进程编程(典藏、含代码)_第2张图片

相信大家有用过叉子,它有“一分为二”的形状

而我们今天要介绍的是fork函数 , 它也有这样的能力

 

调用fork函数时

复制父进程的进程空间来创建子进程

Linux多进程编程(典藏、含代码)_第3张图片

上图是父进程的进程空间,其中代码段是不会不复制到子进程的,而是共享

其它段需要复制,属于写拷贝 (即只有改的时候, 才需要拷贝)

这样提高效率, 节省资源

总而言之,相当于克隆了一个自己

 

现在我们要让它们分别干不同的事

在fork函数执行完毕后

则有两个进程,一个是子进程,一个是父进程

子进程中,fork函数返回0,在父进程中,fork返回子进程的进程ID

因此, 我们可以通过fork返回的值来判断当前进程是子进程还是父进程

从而让它们同时干不同的事情

示例如下(fork):

#include 
#include 
#include 

int main()
{
    printf("+++process %d start running!ppid = %d\n",getpid(),getppid());

    pid_t pid = fork();

    if(pid)//父进程
    {
        printf("parent:process %d start running!ppid = %d\n",getpid(),getppid());
        //do something
        //...
    }
    else//子进程
    {
        printf("child:process %d start running!ppid = %d\n",getpid(),getppid());
        //do something
        //...
        exit(0);
    }

    exit(0);
}

注意⚠️:如果父进程提前结束,那么子进程就会变成孤儿进程  (知识点在下一节2.3)

 

2.1.2 vfork

另外还有一个函数vfork

但是子进程和父进程是不能同时运行的

由于函数不复制父进程的进程空间, 而是抢占父进程的资源, 导致父进程堵塞, 无法继续运行

子进程完成后, 父进程才能继续运行

示例如下(vfork):

#include 
#include 
#include 
#include 

int main()
{
    printf("+++process %d start running!ppid = %d\n",getpid(),getppid());

    pid_t pid = vfork();

    if(pid){//父进程
        printf("parent:process %d start running!ppid = %d\n",getpid(),getppid());
        printf("parent:process %d finish running!ppid = %d\n",getpid(),getppid());
    }
    else{//子进程
        printf("child:process %d start running!ppid = %d\n",getpid(),getppid());
        printf("child:process %d finsish running!ppid = %d\n",getpid(),getppid());
        exit(0);
    }

    exit(0);
}

 

2.2进程结束

学会创建子进程后

我们来学习一下进程结束

不管是子进程还是父进程

进程的退出分为正常退出异常退出

  
2.2.1正常退出

进程的正常退出有四种,如下:

    1、return 只是代表函数的结束, 返回到函数调用的地方。

    2、进程的所有线程都结束。

    3、exit()   代表整个进程的结束,无论当前执行到哪一行代码, 只要遇到exit() , 这个进程就会马上结束。

    4、 _exit()  或者 _Exit() 是系统调用函数。

         

_exit() / _Exit 和 exit 的区别:

    _exit() / _Exit 是 系统调用函数, exit 是库函数

    exit 它是通过调用_exit()来实现退出的

    但exit() 多干了两件事情: 清空缓冲区、调用退出处理函数  

 

退出处理函数:  

    进程正常退出,且调用exit()函数,会自动调用退出处理函数

    退出处理函数可以做一些清理工作

    需要先登记才生效,退出处理函数保存在退出处理函数栈中(先进后出的原则)

    示例如下(退出处理函数):

#include 
#include 
#include 

void func1(void)
{
    printf("%s\n",__func__);
}

void func2(void)
{
    printf("%s\n",__func__);
}

void func3(void)
{
    printf("%s\n",__func__);
}

int main()
{
    atexit(func1);//先登记
    atexit(func2);
    atexit(func3);

    printf("hello!");

    exit(0);
    //_exit(0); //无法调用退出处理函数
    //return 0; //无法调用退出处理函数
}

    输出结果:

hello!func3
func2
func1

 

2.2.2异常退出

进程除了正常退出, 还有异常退出

    1、被信号打断( ctrl + c ,段错误 , kill -9)

    2、最后线程(主线程)被取消。

 

 

2.3等待进程结束并资源回收 

子进程退出时, 不管是正常还是异常, 父进程会收到信号

子进程退出后,内存上的资源必须是父进程负责回收

但是有时候会出现下面两种情况 :

1、子进程先结束, 会通知父进程(通过信号), 让父进程回收资源 , 如果父进程不处理信号, 子进程则变成僵尸进程

2、父进程先结束,子进程就会变成孤儿进程, 就会由1号进程(init )负责回收,但在实际编程中要避免这种情况, 因为1号进程很忙

 

2.3.1 wait函数

wait函数等待子进程的结束信号

它是阻塞函数,只有任意一个子进程结束,它才能继续往下执行,否则卡住那里等

它获得结束子进程的PID以及 退出状态/退出码 , 并且回收子进程的内存资源

#include 
#include 

pid_t wait(int * status);

    status是传出参数, 传出退出状态/ 退出码

Linux多进程编程(典藏、含代码)_第4张图片

演示代码(wait):

#include 
#include 
#include 

int main()
{
    pid_t pid = fork();
    if(!pid){//子1
        printf("child %d start running!\n",getpid());
        sleep(10);
        printf("child exit!\n");
        exit(10);
    }

    pid = fork();
    if(!pid){//子2
        printf("child %d start running!\n",getpid());
        sleep(15);
        printf("child exit!\n");
        exit(30);
    }

    //父进程
    //等待子进程结束
    int status = 0;
    pid_t pid1 = 0;
    pid1 = wait(&status);//等待任意一个子进程的结束
    if(WIFEXITED(status)){
        printf("%d正常结束!退出码 = %d\n",pid1,WEXITSTATUS(status));
    }
    if(WIFSIGNALED(status)){
        printf("%d被信号打断!信号 = %d\n",pid1,WTERMSIG(status));
    }

    pid1 = wait(&status);//等待任意一个子进程的结束
    if(WIFEXITED(status)){
        printf("%d正常结束!退出码 = %d\n",pid1,WEXITSTATUS(status));
    }
    if(WIFSIGNALED(status)){
        printf("%d被信号打断!信号 = %d\n",pid1,WTERMSIG(status));
    }

    return 0;
}

 

2.3.2 waitpid 函数

wait函数的加强版

可以选择等指定哪个子进程 , 还可以选择等待方式(可以选择堵塞、不堵塞)

Linux多进程编程(典藏、含代码)_第5张图片

代码示例(waitpid):

#include 
#include 
#include 

int main()
{
    pid_t pid1 = fork();
    if(!pid1){//子1
        printf("child1  %d start running!\n",getpid());
        sleep(1);
        printf("child1 exit!\n");
        exit(10);
    }

    pid_t pid2 = fork();
    if(!pid2){//子2
        printf("child2  %d start running!\n",getpid());
        while(1);
        printf("child2 exit!\n");
        exit(30);
    }
    
    //父进程
    //等待子进程结束
    int status = 0;
    pid_t pid = 0;
    printf("等待child2结束 \n");
    pid = waitpid(pid2,&status,0);//指定子进程,堵塞
    if(pid==0){
        printf("没有等到子进程!\n");
        return -1;
    }
    printf("child2结束 \n");

	
    if(WIFEXITED(status)){
        printf("%d正常结束!退出码 = %d\n",pid1,WEXITSTATUS(status));
    }
    if(WIFSIGNALED(status)){
        printf("%d被信号打断!信号 = %d\n",pid1,WTERMSIG(status));
    }


    return 0;
}

编译

gcc 5waitpid.c -o 5waitpid

在后台运行

./5waitpid &

输出

等待child2结束 
child1  3966 start running!
child2  3967 start running!
child1 exit!

回车几下

waitpid此时还在等child2退出

用信号杀死进程

kill -9 3967

输出

child2结束 
3966被信号打断!信号 = 9
[1]+  Done                    ./5waitpid

可见,我们已经成功指定哪个子进程

 

(小彩蛋) 

有时候忘记回收子进程资源

或者你的代码不是很完美

那么

该怎么一键收回父进程所有的僵尸子进程的资源呢?

while(waitpid(-1, NULL, WNOHANG)> 0);

 

2.4替换进程内容(exec系列函数)

    fork/ vfork 产生的子进程内容和父进程完全一致, 但是在很多时候, 我们希望新的子进程去执行全新的程序

    而exec系列函数就提供了这样的功能,使用一个程序去替换进程的内容 (不会产生新的进程,是替换 )

    单独使用没有意义,一般 是和fork/ vfork 连用。 用fork/ vfork 产生子进程, 然后用exec替代

    使用vfork堵塞父进程, 抢了资源, 但是使用exec后, 子进程替换了内容, 便不抢占资源了,父进程继续执行,不用等子进程

Linux多进程编程(典藏、含代码)_第6张图片

exec系列函数太多

这里讲常用的一个

代码如下(execl):

#include 
#include 
#include 

int main(int argc,char *argv[],char *env[])
{
    printf("pid = %d\n",getpid());
    pid_t pid = vfork();
    if(!pid){//子
        printf("child pid = %d\n",getpid());
        int res = execl("./tst","tst",NULL);//用这个程序替换掉子进程内容
        if(res==-1){
            perror("execl");
        }

        exit(0);
    }

    //父
    printf("parent running!\n");
    sleep(1);//....
    wait(NULL);
    printf("%d end!\n",getpid());
    return 0;
}

 

 

 

 

 

 

整理不易

点赞收藏关注喔~❤️❤️❤️❤️❤️❤️

 

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