Linux 进程总结


前言

圣人曰:温故而知新可以为师矣。本文旨在把以前学习的关于linux进程相关的零碎知识串联起来,做一个系统的总结。一方面帮助自己加深理解;另一方面可以为需要的人提供一点点帮助。错漏之处望各位同仁不吝赐教,衷心感谢!


提示:以下是本篇文章正文内容,下面案例可供参考

一、进程的定义:

1、典型的进程定义如下:

1)进程是程序的一次执行。

2)进程是一个程序及其数据在处理机上顺序执行时所发生的活动。

3)进程是具有独立功能的程序在一个数据集合上运行的过程,它是操作系统进行资源分配的调度的独立单位(是资源分配的基本单位)。

2、传统OS中的进程定义:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。

3、拓展概念:

1)进程控制块(Process Control Block,PCB):为了使每个程序都能独立的运行,操作系统位置配置了一个专门的数据结构,称为进程控制块。系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。(实际是一个结构体task_struct,放在sched.h文件中

2)进程实体:由程序段,相关数据段和PCB三部分构成。又称位进程映像。所谓创建进程,实质上是创建进程实体中的PCB;撤销进程实质上是撤销进程中的PCB。

3)代码段:存放代码,全局常量(const),字符串常量。

4)数据段:存放全局变量(初始化以及未初始化的全局变量,其中未初始化的全局变量存放在BSS段中,BSS段属于数据段),静态变量(全局和局部的,初始化以及未初始化的静态变量)。

5)堆:用于动态分配的内存的区域。

6)栈:存放局部变量(初始化以及未初始化的局部变量但不包括静态变量),局部常量(const)。

4、在linux内核中,任务和进程是同一概念,进程的四要素总结如下:

(1)有一段程序可执行,不一定被某进程所专有,可与其他进程共有;

(2)进程有专有的系统堆栈空间;

(3)进程控制块即task_struct结构体,登记在内核中,使得进程可成为一个基本单位接受内核调度;同时它还记录着进程所占有的各项资源;

(4)除上述的专有的系统堆栈空间,还要有独立的存储空间,意味着有专有的用户空间;注意,进程只能改变自身的系统堆栈空间,是不能改变系统空间(不独立);如用于虚存管理的mm_struct以及它下属的vm_area,页目录项和页面表,它们从属于task_struct的资源;

二、进程的特征:

1)动态性:进程的实质是进程实体的执行过程。因此动态性就是进程最基本的特征。它由创建而产生,有调度而执行,由撤销而消亡。

2)并发性:是指多个进程实体同存与内存中,且能在同一时间段内同时运行。引入进程的目的正是为了使进程实体和其他进程实体并发执行。

3)独立性:是指进程实体是一个能够独立运行、独立获得资源、独立接受调度的基本单位。

4)异步性:是指进程按照异步的方式运行,即按各自独立的,不可预知的速度向前推进。为了保证进程并发执行的结果具有可预知性,OS引入了进程同步机制。

三、进程的基本状态即转换:

1、进程的三种基本状态:

1)就绪(Ready)状态:是指进程已处于准备好运行的状态,即进程分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。如果系统中有许多处于就绪状态的进程,通常将他们按一定的策略(如优先级策略)排一个队列,称该队列为就绪队列。

2)执行(Running)状态:指进程已经获得CPU,其程序正在执行的状态。

3)阻塞(Block)状态:是指正在执行的进程由于发生某事件(如IO请求,申请缓存失败等)暂时无法继续执行时的状态。此时引起进程调度,OS把CPU资源分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种暂停状态称之为阻塞状态。系统将处于阻塞状态的进程排成一个队列,称该队列为阻塞队列。在较大的系统中,为了减少队列操作的开销,提高系统效率,根据阻塞原因的不同,会设置多个阻塞队列。

2、三种状态的转换:

    进程在运行过程中经常会发生状态转换。如下图所示,处于就绪状态的进程,在调度程序为其分配了CPU资源后便可执行,相应的,其状态就由就绪态转变为执行态;正在执行的进程如果因为分配给它的时间片已完而被剥夺CPU资源,暂停执行时,其状态便由执行态转为就绪;如果因发生某事件,致使当前进程的执行受阻(例如进程访问某临界资源,而该资源正在被其他进程访问),使之无法继续执行,则该进程将由执行变为阻塞。当该进程申请临界资源,进程状态便由阻塞转化为就绪;

       补充:还有两种常见的状态,创建态和终止态。这里暂不做研究。 

 

四、进程控制

    进程控制是进程管理中的最基本的功能。主要包括创建新进程,终止已完成的进程,将因发生异常情况而无法继续执行的进程置于阻塞状态,负责进程运行中状态转换等功能。

1、进程的创建:

       进程创建过程:

       1)为新进程申请PID,并从PCB集合中索取一个空白PCB;

       2)为新进程分配其运行所需的资源,如:内存,文件,I/O设备,CPU时间等。

       3)初始化PCB;

       4)如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。

       Linux进程创建方式,一共有三种方式:fork  vfork  clone。三个函数分别通过sys_fork()、sys_vfork()和sys_clone()调用do_fork()去做具体的创建工作,只不过传入的参数不同。

2、fork()

      1)函数原型:pid_t fork(void);

      2)作用:创建进程;

      3)头文件:#include <unistd.h>

      4)返回值:在父进程中返回进程ID,在子进程中返回0,出错返回-1;

      fork()函数的特点:

      1)fork()创建的新进程被称为子进程,调用一次返回两次,在父进程中返回进程ID,在子进程中返回0;

      2)子进程是父进程的副本,即子进程获得父进程数据空间、堆、栈的副本。父子进程共享正文段(由CPU执行的机器指令部分)。

      3)子进程从fork()的下一行开始执行;    

      4)现行的操作系统大多采用写时复制(Copy-On-Write,COW)策略。即创建子进程时并不立即执行父进程数据空间、堆、栈的完全复制。这些区域由父子进程共享,而且内核将他们的访问权限转变为只读,如果父子进程中的任何一个试图修改这个区域,则内核只为修改区域的那一块内存制作一个副本。通常是虚拟存储器系统中的一“页”。

      5)例子:

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

void main()
{
        pid_t pid;

        int count = 0;

        pid = fork();

        count++;

        if(pid > 0)
        {

                printf("This is father process pid = %d, count is: %d (%p).\n", getpid(), count, &count);

        }
        else if(pid == 0)
        {

                printf("This is chird process pid = %d, count is: %d (%p).\n", getpid(), ++count, &count);

        }
        else
        {
                printf("fork error!");
        }

        return;

}

       运行结果:

       /media/ext/home$ ./fork               
      This is father process pid = 12105, count is: 1 (0x7fffdbfaae68).
      This is chird process pid = 12106, count is: 2 (0x7fffdbfaae68).

  • 从运行结果里面可以看出父子两个进程的pid不同,堆栈和数据资源都是完全的复制。
  • 子进程改变了count的值,而父进程中的count没有被改变。
  • 子进程与父进程count的地址(虚拟地址)是相同的(注意他们在内核中被映射的物理地址不同)。

      fork()函数的两种应用场景:

     1)一个父进程希望复制自己,使父子进程同时执行不同的代码段。这在网络服务进程中是常见的---父进程等待客户端的服务请求。当这种请求到达时,父进程调用子进程处理此请求。父进程则继续等待下一个服务请求到达。

     2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

3、vfork()

      1)函数原型:pid_t vfork(void);

      2)作用:创建一个新进程,并阻塞父进程;

      3)头文件:#include <unistd.h>     #include <sys/types.h>

      4)返回值:在父进程中返回进程ID,在子进程中返回0,出错返回-1;pid_t为无符号整型;

      vfork()函数的特点:

      1)vfork()用于创建一个新进程,而该进程的目的是exec一个新程序;

      2)vfork保证子进程先运行,它调用exec或exit之后父进程才可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。)

      3)vfork创建的子进程在调用exec或exit之前,是在父进程的空间中运行的(采用了写时复制策略)。

      4)由vfork创建出来得子进程不应该使用return返回调用者,但是它可以使用exit()或_exit()函数来退出

4、例子:

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

void main()
{
        pid_t pid;

        int count = 0;

        pid = vfork();

        count++;

        if(pid > 0)
        {

                printf("This is father process pid = %d, count is: %d (%p).\n", getpid(), ++count, &count);

        }
        else if(pid == 0)
        {

                printf("This is chird process pid = %d, count is: %d (%p).\n", getpid(), ++count, &count);

                /* 调用exec函数 */
                execl("/bin/ls", "ls", "/media/ext/home/fork", NULL);

                printf("This is chird process pid = %d!\n", pid);
                printf("This is chird process pid = %d, count is: %d (%p).\n", getpid(), ++count, &count);
        }
        else
        {
                printf("fork error!");
        }

        return;
}

      运行结果:

       /media/ext/home/fork$ ./myvfork    
      This is chird process pid = 12285, count is: 2 (0x7ffeececf2c8).
      This is father process pid = 12284, count is: 4 (0x7ffeececf2c8).
       /media/ext/home/fork$ fork  fork.c  myvfork  vfork1.c

      /media/ext/home/fork$ 

      1)由vfork创造出来的子进程会导致父进程挂起,除非子进程exit或者execl才会唤起父进程;

      2)从运行结果可以看到vfork创建出的子进程共享了父进程的count变量,2者的count指向了同一个内存,所以子进程修改了count变量,父进程的 count变量同样受到了影响。子进程中count变量进行了两次累加操作,打印值为2;子进程调用execl函数唤醒父进程,父进程中count变量进行了两次累加操作,打印值为4;

     3)子进程调用execl函数后,其后面两个printf未打印,说明程序的代码段全部发生变化。原来的代码段execl后面的代码段会被覆盖。关于exec族函数,后面再做分析。

     4)注意:vfork这个系统调用是用来启动一个新的应用程序。其次,子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。通常,如果应用程序不是在fork()之后立即调用exec(),就有必要在fork()被替换成vfork()之前做仔细的检查。

5、exit()

       1)定义:

       头文件:#include <stdlib.h>

       函数原型:void exit(int status);

       函数作用:用于正常终止一个进程。

       参数:status是一个整形参数,表示终止状态;

      exit函数族有三个函数用于正常终止一个进程:_exit和_Exit立即进入内核,exit则先执行一些清理处理(调用执行各终止处理程序,关闭所有标准I/P流等),然后进入内核。

      #include <stdlib.h>

      void exit(int status);

      void _Exit(int status);

      #include <unistd.h>

      void _exit(int status);

      2)exit与_exit:

      exit()在结束调用它的进程之前,要进行如下步骤: 
      a)调用atexit()注册的函数(出口函数);按ATEXIT注册时相反的顺序调用所有由它注册的函数,这使得我们可以指定在程序终止时执行自己的清理动作.例如,保存程序状态信息于某个文件,解开对共享数据库上的锁等。

      b)cleanup();关闭所有打开的流,这将导致写所有被缓冲的输出,删除用TMPFILE函数建立的所有临时文件。

      c)最后调用_exit()函数终止进程。

      _exit()函数:

      直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;

      关于缓冲区内数据:

  •        在Linux的标准函数库中,有一套称作”高级I/O”的函数,我们熟知的printf()、fopen()、fread()、fwrite()都在此 列,它们也被称作”缓冲I/O(buffered I/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符和文件结束符EOF), 再将缓冲区中的 内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特 定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。

      在由‘fork()’创建的子进程分支里,正常情况下使用‘exit()’是不正确的,这是 因为使用它会导致标准输入输出(stdio: Standard Input Output)的缓冲区被清空两次,而且临时文件被出乎意料的删除(临时文件由tmpfile函数创建在系统临时目录下,文件名由系统随机生成)。

  •       适用于绝大多数情况的基本规则是,‘exit()’在每一次进入‘main’函数后只调用一次。    

      PS:上面这一段是另外一个博主博文中的一段。链接如下:https://blog.csdn.net/drdairen/article/details/51896141

      exit()使用可以保证调用这个函数的进程的缓冲区内的数据正常的输出到目标文件中。应该是不会破坏其他进程在标准输入输出缓冲区内的数据的输出的。但是删除TMPFILE函数建立的所有临时文件对其他进程有没有影响待研究。希望有大神指点一二。

      关于进程终止在UNIX环境高级编程中说了很多,但是看得很懵逼。经常用的函数一旦深究起来发现并不是那么简单。还是要感谢网络上的各路大神指点迷津。

      3)exit与return:

  •  main函数中返回一整型值与用该值调用exit是等价的。于是在main函数中exit(0);等价于return(0);

  •  exit是一个函数,有参数。void exit(int status) 。exit执行完后把控制权交给系统。return是函数执行完后的返回。return执行完后把控制权交给调用函数。 return()是当前函数返回。如果是在主函数main, 自然也就结束当前进程了,如果不是,那就是退回上一层调用。

  • return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示一个进程的结束。

  • exit()执行完一些清理工作(终止处理程序,刷新输出流并关闭所有打开的流)后就调用_exit直接退出,不弹堆栈。而return会弹堆栈,返回到上级调用函数。这一点区别在执行vfork时很关键。

      4)例子:

      

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

void main()
{
        pid_t pid;

        int count = 0;

        pid = fork();

        count++;

        if(pid > 0)
        {

                printf("This is father process pid = %d, count is: %d (%p).\n", getpid(), count, &count);

        }
        else if(pid == 0)
        {

                printf("This is chird process pid = %d, count is: %d (%p).\n", getpid(), ++count, &count);
                //_exit(0);   //使用_exit(0)则什么也没有输出,如果给第一个printf加上'\n'的话,那就只会输出第一句话。
                exit(0);

        }
        else
        {
                printf("fork error!");
        }

        return;

}

      子进程中使用exit(0);输出结果为:

       /media/ext/home$ ./myfork               
       This is father process pid = 17420, count is: 1 (0x7ffcef3643f8).
       This is chird process pid = 17421, count is: 2 (0x7ffcef3643f8).
       /media/ext/home$ 

      以上输出正常。

      

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

void main()
{
        pid_t pid;

        int count = 0;

        pid = fork();

        count++;

        if(pid > 0)
        {

                printf("This is father process pid = %d, count is: %d (%p).\n", getpid(), count, &count);

        }
        else if(pid == 0)
        {

                printf("This is chird process pid = %d, count is: %d (%p).", getpid(), ++count, &count);
                _exit(0);   //使用_exit(0)则什么也没有输出,如果给第一个printf加上'\n'的话,那就只会输出第一句话。
                //exit(0);

        }
        else
        {
                printf("fork error!");
        }

        return;

}

      子进程中使用_exit(0);输出结果为:

      /media/ext/home$ ./myfork               
      This is father process pid = 17429, count is: 1 (0x7ffc50217e78).
      /media/ext/home$ 

      子进程中的printf打印并没有输出。原因:对于printf,为了输出的效率提高,计算机会将输入的信息存入缓存。最后写入标准输出文件输出。所以这就能解释为什么_exit什么都没有输出,因为它没有将缓存写入标准输出文件就已经退出。

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

void main()
{
        pid_t pid;

        int count = 0;

        pid = fork();

        count++;

        if(pid > 0)
        {

                printf("This is father process pid = %d, count is: %d (%p).\n", getpid(), count, &count);

        }
        else if(pid == 0)
        {

                printf("This is chird process pid = %d, count is: %d (%p).\n", getpid(), ++count, &count);
                _exit(0);   //使用_exit(0)则什么也没有输出,如果给第一个printf加上'\n'的话,那就只会输出第一句话。
                //exit(0);

        }
        else
        {
                printf("fork error!");
        }

        return;

}

      子进程中使用_exit(0);printf中加了“\n”,输出结果为:

      /media/ext/home$ ./myfork               
      This is father process pid = 17442, count is: 1 (0x7ffd4685be68).
      This is chird process pid = 17443, count is: 2 (0x7ffd4685be68).
      /media/ext/home$  

     子进程中的printf又可以正常输出。原因:加入'\n'后可以输出一句话这同样与缓存写入文件有关,printf函数在遇到“\n”换行符时自动的从缓冲区中将记录读出。

      参考:https://www.cnblogs.com/chilumanxi/p/5136105.html ,网友棒棒的!点赞!

6、wait() 与 waitpid() 

      参考:https://blog.csdn.net/dangzhangjing97/article/details/79745880 LInux:进程等待之wait() & waitpid()   ---  拿来主义真香!!!感谢分享!

引用:

https://blog.csdn.net/qq_32095699/article/details/88601494

https://blog.csdn.net/sykpour/article/details/25643861

https://blog.csdn.net/drdairen/article/details/51896141

UNIX环境高级编程

计算机操作系统


总结

未完待续。

猜你喜欢

转载自blog.csdn.net/the_wan/article/details/108170789