基于进程的并发编程

56 篇文章 1 订阅

构造并发程序最简单的方法就是用进程,例如在linux操作系统下面使用fork()、exec()、waitpid()创建新的进程。

我们先来简单的看一下创建进程的大致过程:
例如:一个构造并发服务器的的方法就是在父进程中接受客户端的链接请求,然后创建一个新的子进程为每个客户端进行服务。

  • 1、
    假设现在有两个客户端和一个服务器,服务器正在监听一个描述符(比如描述符3)上的连接请求,假设现在服务器接受了客户端1的链接请求,并返回一个已连接的描述符(比如描述符4)。

这里写图片描述

  • 2、在接受这个连接请求之后,服务器派生一个子进程,这个子进程获得服务器描述表的完整拷贝,子进程关闭它的监听描述符3,父进程关闭它的连接描述符4。这样就为客户端创建了一个子进程提供服务。(因为父子进程中的已连接描述符都指向同一个文件表表项,所以父进程关闭它的已连接描述符的拷贝时至关重要的,否则将永远也不会释放已连接描述符4的文件表条目,这样就会引发存储器泄漏是的系统崩溃)。

这里写图片描述

  • 3、假设现在父进程又接受一个新的客户端2的连接请求,并返回一个新的描述连接符5,然后父进程又派生一个子进程,这个子进程用已连接描述符5为它的客户端提供服务。此时父进程有在等待下一个连接请求,而两个子进程正在并发地为他们各自的客户端提供服务。

    这里写图片描述

下面来介绍linux下面创建子进程的函数:
一、fork()函数
1、fork入门知识

  • fork()函数通过系统调用创建一个与原来进程一模一样的子进程。也就是说两个进程可以做完全相同的事。但是如果传入的参数或者初始变量不同,他们也可以做不同的事。
    我们知道进程是系统分配资源的最小单位,它包括代码、数据、堆栈等资源。
    创建子进程后,系统就要给子进程分配新的资源,然后把父进程的值克隆一份到子进程中,只有少数的值与原来不同。(与fork相对应的还有vfork(),vfork()给子进程分配资源的时候,采用的是写时拷贝机制,具体原因在将exec函数族的时候再来分析)。

函数原型:pid_t fork(void);
pid_t 是一个宏定义,它就相当于int,被定义在#include

#include<unistd.h>
#include<sys/types.h>

返回值:若是fork()成功的话,一次能够返回两个值。
1、子进程返回0。
2、父进程返回子进程的进程识别码(一个大于0的数)。
3、fork()失败,失败返回-1。

  • 说明:
    一个现有的进程可以调用fork函数创建一个新进程,fork创建的新进程称作子进程。两次返回的区别是子进程中返回0,父进程返回子进程的进程识别码。
    子进程是父进程的副本,它将会克隆一份父进程的数据空间,堆,栈等资源。对于在父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间,进程有独立的地址空间既是优点也是缺点。
    优点就是,一个进程就不可能不小心覆盖另一个进程的数据。
    另一方面,因为是独立的地址空间,所以他们之间要共享信息的话,会非常困难,为了共享信息,他们必须显示的使用IPC(进程间通信)机制。基于进程间通信的另一个缺点是速度比较慢,开销比较大。
    在Unix中waitpid函数是一种基本的IPC形式。还有套接字接口也是IPC的一种重要的形式。

示例1:
调用fork()函数创建一个子进程。让父进程输出”parent–>progress”,子进程输出”sub–>progress”。

#include<iostream>
#include<sys/types.h>
using namespace std;

int main()
{
    pid_t pid=fork();           //创建子进程
    if(pid==0)
    {
       cout<<"sub-->progress"<<endl;
    }
    else if(pid>0)
    {
        cout<<"parent-->progress"<<endl;
    }
    return 0;
}

结果:
可以看到运行结果如图所示,两个进程的输出结果如图:父进程先执行完的,然后子进程再执行完。
这里写图片描述

示例2:利用getpid()函数打印当前进程的进程识别码,利用getppid()函数打印它的父进程的进程识别码。
注:
这里写图片描述

这里写图片描述

这里写图片描述
怎么回事,新创建的子进程的父进程的进程识别码不就是3127吗,为什吗会输出1???
别着急,这不是程序写错了,带着这个问题我们再来看看下面这个实例。

这里写图片描述

这里写图片描述

  • 这次就对了,父进程自己的进程识别码是3150,子进程的父进程的进程识别码也是3150.那么这次为什么又正确了呢???
    首先观察这个程序,在示例3中的父进程中我们加了waitpid(pid,NULL,0)这一句代码,然后结果又正确了,同时观察输出结果的顺序我们也会发现,这次是子进程先完成,接下来父进程才结束。
    对于上面这两个示例不同的主要原因就使进程执行的顺序不同,父子进程执行的顺序是由操作系统调度的,这个过程是不确定的。对于示例2在父进程结束后,子进程就没有了父亲,所以操作系统就默认子进程的父进程的进程识别码为1。在示例3中,我们在父进程中加入了waitpid()函数,它的作用是会先暂停当前进程的执行,直到有信号来或者子进程结束。

示例4:
在fork()之前创建一个变量a=10,在子进程中将a改为a=0输出,然后执行这个程序,观察输出结果。
这里写图片描述

结果:我们会发现,子进程和父进程之间是不受影响的,在子进程之间改变变量的值是不会影响父进程之中变量的值的。
这里写图片描述

示例5:
1、在fork函数之前,加了一行代码printf(“hehe”);观察输出结果,发现子进程和父进程的输出结果中都输出了”hehe”。
这里写图片描述

结果:
这里写图片描述

2、在fork()函数之前加了一行代码printf(“hehe\n”),观察输出结果,发现只输出了一句”hehe”。
这里写图片描述

结果:
这里写图片描述

  • 这个又是怎么回事呢???
    这是因为标准I/O是缓冲的,从标准输出到终端设备的话是进行行缓冲的。所以在1中因为没有加换行符,在为子进程复制数据的时候也将缓冲区中的值复制了一份,而在进程结束的时候会冲洗缓冲区中的数据,所以会输出两次”hehe”。而在2中加了换行符,会将缓冲区中的值清空,所以在创建子进程的时候,缓冲区中就没有”hehe”了,所以也就只输出一次”hehe”。

二、wait和waitpid函数:

  • 一个进程在终止的时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息,如果是正常中终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。
    当一个进程正常或者异常终止时,内核就像其父进程发送SIGCHLD信号。因为子进程终止是一个异步时间,所以发生这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号的发生时即被调用执行的函数。对于这种信号系统默认动作是忽略它。
    调用wait或waitpid有三种情况: 1、如果所有子进程都还在运行,则阻塞。
    2、如果一个子进程已终止,正在等待父进程获取它的终止状态,则取得该子进程的终止状态立即返回。
    3、如果他没有任何子进程,则立即出错返回。
头文件:#include<sys/wait.h>
#include<sys/types.h>

1、wait函数
原型:pid_t wait(int* status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。

  • 进程一但调用wait,就立即阻塞自己,当wait自动分析是否是当前进程的某个子进程已经退出,如果他找到了这样一个已经变成僵尸的子程序,wait就会收集这个子进程的信息,并把它彻底销毁后返回。如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个僵尸进程出现为止。
    参数status是用来保存收集进程退出时的一些状态,它是一个指向int类型的指针。但是如果我们不关心这个子进程是如何死掉的,只想结束这个子进程,我们就可以将这个参数设置为NULL,pid=wait(NULL);
    如果status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值,指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或者是被哪一个信号结束的。由于这些信息是被保存在一个整数的不同的二进制中,为了读取方便,人们就设计了一套专门的宏(macro)来完成这项工作,下面介绍两个常用的。
    1、WIFEXITED(status):若为正常终止子进程返回的状态为非零值。(查看进程是否是正常退出)
    2、WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码(例:如果子进程调用exit(8)结束,WEXITSTATUS(status)就会返回8)。如果进程不是正常退出的,这个值就毫无意义。(查看进程的退出码)

例:
这里写图片描述

结果:我们看到,子进程的退出码是8。
这里写图片描述

2、waitpid函数

  • 原型:pit_t waitpid(pid_t pid,int *status,int options); 返回值:
    1、当正常返回的时候返回子进程的进程识别码。
    2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可以收集则返回0。
    3、当pid所指示的子进程不存在,或此进程存在,但是不调用进程的子进程,waitpid就会出错返回-1,这时errno被设置为ECHILD。

    参数: 1、pid pid=-1; 等待一个子进程,与wait等效。 pid>0; 等待其进程ID与pid相等的子进程。
    pid==0,等待其组ID等于调用进程组ID的任何一个子进程。 pid<-1;等待其组ID等于pid绝对值的任一子进程。

    2、status:保存代码是否运行完毕和结果是否正确四个字节,我们只关心他最低的比特位。
    WIFEXITED(status):若为正常终止子进程返回的状态为真。(查看进程是否是正常退出)
    WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

    3、options options提供了一些额外的选项来控制waitpid,其中WNOHANG是用的最多的。
    WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

例:
这里写图片描述

结果:父进程经过5次失败后终于收集到退出的子程序的信息了。
这里写图片描述

三、exec函数族:

  • 用fork()创建子进程之后执行的是和父进程相同的程序,(但有可能执行的是不同的代码分支),子程序往往要调用一种exec函数执行另外一个进程。当进程调用一种exec函数时,该进程的用户代码和数据完全被新的程序替换,从新程序的启动实例开始执行。调用exec并不创建新进程,所以调用exec后该进程的进程识别码没有发生改变。
    在linux中并不存在一个exec()的函数形式,exec指的是一组函数,一共有六个函数。exec函数族的作用是根据指定的文件名找到可执行的二进制文件,并用它来取代调用进程的内容,也就是在这个调用进程的内部来执行一个可执行文件,可执行文件可以是二进制文件,也可以是任何linux下的可执行的脚本文件。

    这里写图片描述

这里写图片描述
其中只有execv函数是真正意义上的系统调用,其他都是在此基础上经过包装的库函数。
这里写图片描述

  • exec函数族的函数在执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍被保持原样,有点像”三十六计”中的”金蝉脱壳”。看上去是旧的驱壳,但是却已经注入了新的灵魂。只有调用失败了才会返回一个-1,从原程序的调用点接着往下执行,错误信息被存于errno中。
    • 我们知道,fork函数会将所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork()函数完了之后我们紧接着就调用exec,这些拷贝的东西又会被抹掉,这是非常不划算的。于是就有了vfork()函数,vfork与fork的一个重要区别就是vfork采用的是写时拷贝技术,在vfork完了之后并不立即复制父进程的内容,而是等到真正实用的时候才去复制,这样如果下一条语句时exec,那么他就不会去做无用功。vfork与fork相比,还有一个区别就是,vfork会先执行子进程。
  • 在linux中条用exec函数主要有两种情况。
    1、当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数族让自己重生。
    2、如果进程想执行另一个程序,那他就可以调用fork函数创建一个新的进程,然后调用exec函数族中任何一个函数使子进程重生。

示例1:
创建一个子进程,并在这个子进程中执行a.out这个可执行文件。
这里写图片描述

结果:
这里写图片描述

示例2:创建一个子进程,在这个子进程中列出当前目录下的所有文件。
这里写图片描述

结果:
这里写图片描述

在使用exec函数族时,一定要加上错误判断语句。因为exec很容易失败,最常见的原因有:
1、找不到文件或者路径,此时errno被设置成ENOENT。
2、数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT。
3、没有相对应的可执行文件权限,此时errno被设置为EACCES。

exec后新进程保持原进程以下特征
- 环境变量(使用了execle、execve函数则不继承环境变量);
进程ID和父进程ID;
实际用户ID和实际组ID;
附加组ID;
进程组ID;
会话ID;
控制终端;
当前工作目录;
根目录;
文件权限屏蔽字;
文件锁;
进程信号屏蔽;
未决信号;
资源限制;
- tms_utime、tms_stime、tms_cutime以及tms_ustime值。
对打开文件的处理与每个描述符的exec关闭标志值有关,进程中每个文件描述符有一个exec关闭标志(FD_CLOEXEC),若此标志设置,则在执行exec时关闭描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开,利用这一点可以实现I/O重定向。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值