Linux 多进程编程

一、知识背景

学习Linux 进程控制理论无非就是学习进程的几个方面:创建、退出、等待其他进程结束、执行新的程序。但是在学习这些理论知识以及

具体实现方法之前,有必要了解一下进程其他的基本知识点

1、Linux 系统以进程为基本单位分配资源,以线程为基本单位进行调度;

2、进程拥有自己的地址空间,进程相关所有的信息都存放在进程的地址空间里,换句话说,进程所能访问到的地址空间的集合就是进程的地址空间;

3、Linux 内核所有的进程都有一个基本结构:进程控制块(PCB),也叫做任务结构(task_struct),这些结构通过双向循环链表组成一个进程链表,即每个进程都会在进程表里占据一个表项;进程控制块里包含了进程相关的所有信息,如堆栈段、代码段、打开的文件描述符、所属的用户ID以及组ID等等。  

4、每个进程都会有一个进程 描述符:PID,不同的进程所拥有的描述符一定不同,进程结束后它的描述符可以再次被使用;

5、PID为0的是调度程序,PID为1的是init进程;

6、进程的结构

         一个Linux 进程由三段组成:代码段、数据段、堆栈段,这三个段就是一个可执行文件的必备组成结构。顾名思义,代码段存储的是可执行代码,数据段存放的是全局变            量、静态变量等,堆栈段存放了临时变量和动态申请到的资源。具体哪个段存放哪些数据,戳这里查看:Linux 程序地址空间分布

二、创建进程

1、创建进程的时机

          在Linux系统中创建进程的时机是可以总结出来的:

1)、系统初始化

          在Linux  系统启动的过程中,Linux内核会创建许多必要的进程以完成整个初始化过程,比如init进程就是第一个用户态的进程。

2)、一个已存在的进程调用系统调用创建一个新的进程

          这是Linux 系统中最常见的一个创建进程的方式,由一个进程在代码里直接调用系统调用创建一个新的进程,再在新的进程里执行其他代码。这也是本文描述的对象。

3)、用户请求创建以额新的进程

         这种方法就比较像windous系统和桌面版Linux 系统了,用户通过鼠标或者命令行新建一个进程。

4)、一个批处理作业的初始化

         用户提交批处理作业,操作系统检查完资源之后会创建一个进程,以运行批处理作业队列的下一个作业。

从技术上讲,这些创建新进程的方式中,本质上都是:由一个已经存在的进程执行了创建新进程的系统调用后完成。

2、进程创建函数

1)、fork()函数

         该函数创建一个与调用进程一模一样的进程,新进程叫子进程,调用函数的进程叫父进程。也就是说,当父进程调用了一个fork()系统调用时,fork()便会创建一个新的地址          空间,并且把父进程的地址空间的内容大部分的复制到子进程的地址空间上。 没错,是大部分而不是全部复制。例如父子进程的PID肯定不同对吧,且父子进程的父进程            ID也不可能相同,除此之外父进程设置的文件锁也不会被子进程继承,不同的内容还有其他。当然相同内容更多,比如代码是共享的,即只有一份,但是堆栈段和数据段            都有两个副本,以及打开的文件描述符,用户ID、组ID控制终端等都两个副本且内容相同。

         fork()函数还有一个很有趣的特点便是:一次调用两次返回,返回0给子进程,返回子进程的PID给父进程。为什么要这样做呢?想想也是,fork的结果是创建了一个新的进            程,并且两个进程的地址空间的内容都差不多,那要区分这两个进程还真不好办,但是“返回两次”这个特性就为我们区分父、子进程提供了方法。我们可以通过一个判断返          回值的内容来区分父、子进程。以下是基本代码框架:

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

{
        pid_t pid;
        printf("Before fork\n");
        if((pid=fork())<0)
       {
              printf("Fork error!\n");
              exit(0); 
        }
        if(pid==0)//child process
        {
              execl("/bin/ls","ls","/home",NULL);
      
        }
        else if(pid>0)
        {
              printf("This is father process\n");
              exit(0);  
        }
}


2)、vfork()函数

         vfork() 函数的功能和fork()异曲同工,除了以下几点:a、fork()函数并不能确保在fork()函数返回后是哪个进程先运行(由调度算法决定),但是vfork()函数返回后一定是         子进程先运行;b、vfork() 函数创建的子进程并不会复制父进程的数据段和堆栈段,即父子进程共享代码段、数据段、堆栈段。

三、进程的终止

1、终止时机

1)、正常退出(自愿)

          进程完成自己的工作后便自动的交出CPU的控制权,通常进程执行exit()函数或者return语句后便结束,这种属于正常的退出。

2)、出错退出(自愿)

          进程在发现自己需要完成的工作没法完成时便会退出,但是这种退出仍然属于自愿、非强制性的,比如编译进程执行:cc  funct.c ,但是funct.c 并不存在,因此编译进程便           退出,并且返回出错信息,诸如此类的退出都叫做出错退出。

3)、严重错误(非自愿)

         进程发现了一些致命的错误,如执行了非法指令、引用非法地址、除书是零等,它便会通知操作系统,操作系统会给进程发出一个信号。

4)、被其他进程杀死(非自愿)

          在Linux 中,一个进程通过调用一个系统调用通知操作系统杀死其他进程。

2、进程终止的方法

       1)正常终止的方法包括:a、在主函数调用了return 语句;b、调用了exit函数;事实上在主函数调用了return语句其效果等于调用了exit函数,不同的是如果在其他函数                  里调用了exit函数,则进程直接结束;c、进程的最后一个线程在代码里执行了return语句;d、进程的最后一个线程调用了pthread_exit函数。

        2)、异常终止的方法包括:a、调用abort,产生SIGABRT信号;b、进程接收到某些信号,这些信号使得进程退出;c、最后一个线程对“cancellation”请求作出响应。

正常退出里的a、b情况是本文描述的重点,关于信号和线程将在后续的文章里详细描述。

2、终止进程的函数

       exit(int status) 函数将直接终止调用进程,并且通过status参数将本进程的退出状态返回给父进程,父进程通过wait或者waitpid函数可以获得子进程的退出状态。需要注意的是exit()函数是不会返回的,想想便理解,如果有返回那么应该返回哪里?调用这个函数的进程已经退出了,更暴力一点的说法就是,进程的地址空间已经被释放了,它已经找不到”家“了,还如何返回?这里还有一个奇妙的问题:既然退出状态是返回给父进程的,如果父进程在子进程终止之前就已经退出了,那么子进程的退出状态将返回给谁呢?答案是:所有进程在退出时,如果其子进程还没有退出,则这些子进程都将变成init的子进程。事实上,在一个进程要退出时,内核逐个检查每个活动进程,如果他们是正要退出的进程的子进程,则直接将这些进程的父进程ID改成1(init 进程的PID),这样就保证了每一个进程都有一个父进程,也说明了其实init 可以是每个进程的父进程。

        无论进程如何终止,最后都会执行内核里的同一段代码,这段代码处理进程的收尾工作,如关闭打开的文件描述符、释放进程所使用的存储器等等。

四、进程的等待

        父进程在创建了一个子进程后便和子进程“分道扬镳”,各自运行在自己的地址空间,因此父进程要获取子进程的退出状态,只能通过内核。内核会实时监控子进程,如果子进程退出,内核就会给父进程发出一个SIGCHLD的信号,获得信号以后,父进程可以选择忽略它(默认动作)或者提供一个处理该信号的函数。通常情况下,父进程需要等待子进程的退出信号,这时我们可以像办法让父进程进入阻塞态。

        wait()函数和waitpid()函数就提供了这个功能。

        wait()函数使得父进程进入阻塞态,并且等待子进程退出,如果任何一个子进程退出,则进程从这个函数返回(当然中间还有进程调度的问题),并且此时已经获得子进程的退出状态。当然调用这个函数的进程可能没有子进程,或者所有子进程都已经结束了,那么调用这个函数就不一定会阻塞进程,具体的后果视情况而定。如果进程在接到SIGCHLD后调用这个函数,则会立即返回。

        waitpid()函数功能和wait()函数差不多,并且有所加强,前者能够指定等待的进程,并且能够指定调用进程在调用后不进入阻塞态。

        注意:两个函数都拥有一个指向整型数据的指针,如果不指定为NULL ,则可以返回子进程的终止状态,如果指定为NULL ,则终止转态将不返回。如果不关注终止状态可以指定为NULL。

五、执行新的程序

         exec函数族:

         exec 是execute的缩写,是一类函数的总称,在Linux 系统里,这些函数将在现有的进程空间里装入指定的可执行代码。

        当fork()函数创建一个子进程以后,子进程的地址空间已经填充了一些由父进程复制过来的内容,包括代码段、数据段、堆栈段,因此子进程在fork()返回以后会立即执行fork() 后面的代码,直到遇到exec函数的其中一个。在很多情况下我们不需要子进程拥有和父进程同样的可执行代码,而是希望子进程能够装入其他的代码以执行。exec函数族就完成这样的功能。

        当进程调用exec 函数以后,这个进程的代码段数据段以及堆栈段都将被新程序的内容所覆盖。其他地址空间里的内容不一定都改变,其中PID、UID、GID、工作目录、终端等都不变,而文件描述符是否关闭就要看进程中描述符的“执行时关闭标志”。

六、代码举例

       其实代码可以直接使用进程创建章节里的框架。稍加修改就可以加上前面所有的函数,以实现一个比较完整的功能:父进程创建一个子进程,然后等待子进程的退出,子进程退出后父进程打印消息;子进程执行一个新的程序,这里执行"/bin/ls"。代码如下:

int main(int argc, char *argv[])
{
    pid_t pid;
    printf("\nBefore fork\n");
    if((pid=fork())<0)
    {
	printf("Fork error!\n");
        exit(0); 
    }
    if(pid==0)//child process
    {
        printf("\nThis is the child process\n");
        printf("executing new program\n");       
        execl("/bin/ls","ls","/home",NULL);
      
    }
    else if(pid>0) //father process
    {

        wait(NULL);
        printf("\nThis is the father process\n\n");
        exit(0);  
    }
}





  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值