Linux多任务编程(一)---任务、进程、线程

来源:互联网 发布:淘宝卖家咨询电话 编辑:程序博客网 时间:2024/04/29 12:12

Linux下多任务介绍

  首先,先简单的介绍一下什么叫多任务系统?任务、进程、线程分别是什么?它们之间的区别是什么?,从而可以宏观的了解一下这三者,然后再针对每一个仔细的讲解。

   什么叫多任务系统?多任务系统指可以同一时间内运行多个应用程序,每个应用程序被称作一个任务

   任务定义:任务是一个逻辑概念,指由一个软件完成的任务,或者是一系列共同达到某一目的的操作。

   进程定义:进程是指一个具有独立功能的程序在某个数据集上的一次动态执行过程,它是系统进行资源分配和调度的最小单元。

   线程定义:线程是进程内独立的一条运行路线,是处理器调度的最小单元,也可以成为轻量级进程。

   看了定义,有点晕,还是通俗的说一下它们的区别吧。①通常一个任务是一个程序的一次执行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务就是进程或线程。②一个进程可以拥有多个线程,每个线程必须有一个父进程。

任务

    任务是一个逻辑概念,指由一个软件完成的任务,或者是一系列共同达到某一目的的操作。通常一个任务是一个程序的一次执行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务就是进程或线程。例如,一个杀毒软件的一次运行是一个任务,目的是从各种病毒的侵害中保护计算机系统,这个任务包含多个独立功能的子任务(进程或线程),包括实时监控功能、定时查杀功能、防火墙功能及用户交互功能等。任务、进程和线程之间的关系如图1所示

   

进程

进程的基本概念

   进程是指一个具有独立功能的程序在某个数据集上的一次动态执行过程,它是系统进行资源分配和调度的基本单元。一次任务的运行可以并发激活多个进程,这些进程相互合作来完成该任务的一个最终目标。

   进程具有并发性、动态性、交互性、独立性和异步性等主要特性。

   进程和程序是有本质区别的:程序是静态的一段代码,是一些保存在非易失性存储器的指令的有序集合,没有任何执行的概念;而进程是一个动态的概念,它是程序执行的过程,包括动态创建、调度和消亡的整个过程,它是程序执行和资源管理的最小单位。

Linux下的进程结构

  进程不但包括程序的指令和数据,而且包括程序计数器和处理器的所有寄存器及存储临时数据的进程堆栈,因此,正在执行的进程包括处理器当前的一切活动。

  因为linux是一个多任务多进程的操作系统,所以其他的进程必须等到系统将处理器使用权分配给自己之后才能运行。当正在运行的进程等待其他的系统资源时,linux内核将取得处理器的控制权,并将处理器分配给其他正在等待的进程,他按照内核中的调度算法决定将处理器分配给哪一个进程,也就是说,内核不会让处理器闲着。

  内核将所有进程存放在双向循环链表(进程链表)中,其中链表的头是 init_task 描述符。链表的每一项都是类型为 task_struct,称为进程描述符的结构,该结构包含了一个进程相关的所有信息,定义在<include/linux/sched.h>文件中。task_struct内核结构比较大,它能完整的描述一个进程,如进程的状态、进程的基本信息、进程标识符、内存相关信息、父进程相关信息、与进程相关的终端信息、当前工作目录、打开的文件信息、所接收的信号信息等。

  下面详细讲解task_struct结构中最为重要的两个域:state(进程状态)和pid(进程标识符)。如果想具体了解task_struct,请点这里。

 (1)进程状态

   Linux中的进程有以下几种状态。

   ● 运行状态(TASK_RUNNING):进程当前正在运行,或者正在运行队列中等待调度。

   ● 可中断的阻塞状态(TASK_INTERRUPTIBLE):进程处于阻塞(睡眠)状态,正在等待某些事件发生或能够占用某些资源。处在这种状态下的进程可以被信号中断。接收到信号或被显式的唤醒呼叫(如调用 wake_up 系列宏:wake_up、wake_up_interruptible等)唤醒之后,进程将转变为 TASK_RUNNING 状态。

   ● 不可中断的阻塞状态(TASK_UNINTERRUPTIBLE):此进程状态类似于可中断的阻塞状态(TASK_INTERRUPTIBLE),只是它不会处理信号,把信号传递到这种状态下的进程不能改变它的状态。在一些特定的情况下(进程必须等待,直到某些不能被中断的事件发生),这种状态是很有用的。只有在它所等待的事件发生时,进程才被显示的唤醒呼叫唤醒。

   ● 可终止的阻塞状态(TASK_KILLABLE):该状态的运行机制类似于TASK_UNINTERRUPTIBLE,只不过处在该状态下的进程可以响应致命信号。它可以替代有效但可能无法终止的不可中断的阻塞状态(TASK_UNINTERRUPTIBLE),以及易于唤醒但安全性欠佳的可中断的阻塞状态TASK_INTERRUPTIBLE)。

   ● 暂停状态(TASK_STOPPED):进程的执行被暂停,当进程收到 SIGSTOP、SIGSTP、SIGTTIN、SIGTTOU等信号时,就会进入暂停状态。

   ● 跟踪状态(TASK_TRACED):进程的执行被调试器暂停。当一个进程被另一个监控时(如调试器使用ptrace()系统调用监控测试程序),任何信号都可以把这个进程置于跟踪状态。

   ● 僵尸状态(EXIT_ZOMBIE):进程运行结束,父进程尚未使用 wait 函数族(如调用 waitpid()函数)等系统调用来“收尸”,即等待父进程销毁它。处在该状态下的进程“尸体”已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的推出状态等信息供其他进程收集。

   ● 僵尸撤销状态(EXIT_DEAD):这是最终状态,父进程调用 wait 函数族“收尸”后,进程彻底由系统删除。

   它们之间的转换关系如图2所示:

    

    进程可以使用 set_task_state 和 set_current_state 宏来改变指定进程的状态信息和当前进程的状态。

(2)进程标识符

   Linux内核通过唯一的进程标识符 PID 来标识每个进程(就和文件描述符一样)。PID存放在进程描述符的 pid 字段中,新创建的 PID 通常是前一个进程的 PID 加1,不过PID的值有上限(最大值=PID_MAX_DEFAULT-1,通常为32767),读者可以查看/proc/sys/kernel/pid_max 来确定该系统的进程数上限。

   当系统启动后,内核通常作为某一个进程的代表。一个指向task_struct的宏current用来记录正在运行的进程。current经常作为进程描述符结构指针的形式出现在内核代码中,例如,current->pid 表示处理器正在执行的进程的PID。当系统需要查看所有的进程时,则调用for_each_process()宏,这将比系统搜索数组的速度要快的多。

   在Linux中获得当前进程号的(PID)和父进程号(PPID)的系统调用函数分别为 getpid() 和 getppid()。

进程的创建、执行、终止

  (1)进程的创建和执行

   咱们首先得知道啥是创建,啥是执行哈!我刚开始看的时候没懂。创建进程就是产生一个新的进程,这个大家都知道。而进程的执行,前边讲进程的的定义的时候,就说了正在运行的子任务,说白了,进程执行也就是让产生的这个进程干点什么事,别占着那啥不拉那啥。

   许多操作系统提供的都是产生进程的机制,也就是说,首先在新的地址空间里创建进程、读入可执行文件,最后再开始执行。Linux 中进程的创建很特别,它把上述的步骤分解到两个单独的函数中去执行:fork()函数和exec函数族。首先,fork()函数通过复制当前进程创建一个子进程(注意此时资源还没有被复制过来,去了解一下写时复制页技术吧),子进程于父进程的区别仅仅在于不同的PID、PPID和某些资源及统计量。exec函数族负责读取可执行文件并将其载入地址空间开始运行。

  (2)进程的终止

   进程终结也需要很多繁琐的工作,系统必须保证回收进程所占用的资源,并通知父进程。Linux首先把终止的进程设置为僵尸状态,这时,进程无法投入运行,它的存在只为父进程提供信息,申请死亡。父进程得到信息后,开始调用 wait 函数族,最后终止子进程,子进程占用的所有资源被全部释放。

进程的内存结构

   Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该地址空间是大小为 4GB的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理地址),而且,更重要的是,用户程序可以使用比实际物理内存更大的地址空间。

  4GB的进程地址空间会被分成两个部分:用户空间与内核空间。用户地址空间是从0到3GB(0xC000 0000),内核地址空间占据3GB到4GB。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。只有用户进程使用系统调用(代表用户进程在内核态执行)时可以访问到内核空间。每当进程切换时,用户空间就跟着变化;而内核空间由内核负责映射,它不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。每个进程的用户空间都是完全独立、互不相干的。进程的虚拟内存空间如图3所示,其中用户空间包括以下几个功能区域:

   ●  只读段: 包含程序代码(.init和.text)和只读数据(.rodata)。

   ●  数据段: 存放的是全局变量和静态变量。其中可读可写数据段(.data)存放已初始化的全局变量和静态变量,BSS数据段(.bss)存放未初始化的全局变量和静态变量。

   ●  堆: 由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。

   ●  堆栈: 存放动态分配的数据,一般由程序员动态分配和释放。若程序员不释放,程序结束时可能由操作系统回收。

   ●  共享库的内存映射区域: 这是Linux动态链接器和其他共享代码库代码的映射区域。

   


    由于在Linux系统中每一个进程都会有/proc文件系统下与之对应的一个目录(如将init进程的相关信息在/proc/1 目录下的文件中描述),因此通过 proc 文件系统可以查看某个进程的地址空间的映射情况。例如,运行一个应用程序,如果它的进程号为13703,则输入“ cat  /proc/13703/maps”命令,可以查看该进程的内存映射情况。

线程

   前面已经讲到,进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段、代码段和堆栈段,这就造成了进程在进行切换等动作时需要较复杂的上下文切换等动作。为了进一步减少处理机的空闲时间,支持多处理器及减少上下文切换开销,进程在演化中出现了另一个概念---线程。它是进程内独立的一条运行路线,是处理器调度的最小单元,也可以称为轻量级线程。线程可以对进程的内存空间和资源分配进行访问,并与同一进程中的其他线程共享。因此,线程的上下文切换的开销比创建进程小得多。

   一个进程可以拥有多个线程,每个线程必须有一个父进程。线程不拥有系统资源,它只具有运行时所必须的一些数据结构,如堆栈/寄存器与线程控制块(TCB),线程与其父进程的其他进程共享该进程所拥有的全部资源。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他进程带来影响。由此可知,多线程中的同步是一个非常重要的问题。在多线程系统中,进程与线程的关系如图4所示

 

   在Linux系统中,线程分为3种:①用户线程 ②轻量级线程 ③内核线程 

   下一节:http://blog.csdn.net/mybelief321/article/details/9054405