Linux进程和线程的基础与管理

09-06

一.进程的基本概念

程序是为了完成某种任务而设计的软件,比如vi是程序。什么是进程呢? 进程就是运行中的程序。一个运行着程序,可能有多个进程。比如Web服务器是Apache服务器,当管理员启动服务后,可能会有好多人来访问,也就是说许多用户同时请求httpd,Apache服务器将会创建多个httpd进程来对其进行服务。

首先我们看看进程的定义。进程是一个具有独立功能的程序关于某个数据集合的一次可以并发执行的运行活动,是处于活动状态的计算机程序。进程作为构成系统的基本细胞,不仅是系统内部独立运行的实体,而且是独立竞争资源的基本实体。了解进程的本质,对于理解、描述和设计操作系统有着极为重要的意义。了解进程的活动、状态,也有利于编制复杂程序。

二.进程的属性

进程的定义:一个进程是一个程序的一次执行的过程;程序是静态的,它是一些保存在磁盘上的可执行的代码和数据集合;进程是一个动态的概念,它是Linux系统的基本的调度单位。

一个进程由如下元素组成:

  • 程序读取的上下文,它表示程序读取执行的状态。
  • 程序当前执行的目录。
  • 程序服务的文件和目录。
  • 程序访问的权限。
  • 内存和其他分配给进程的系统资源。

Linux进程中最知名的属性就是它的进程号(Process Idenity Number,PID)和它的父进程号(Parent Process ID,PPID)。PID、PPID都是非零正整数。一个PID唯一地标识一个进程。一个进程创建新进程称为创建了子进程(Child Process)。相反地,创建子进程的进程称为父进程。所有进程追溯其祖先最终都会落到进号为1的进程身上,这个进程叫做init进程,是内核自举后第一个启动的进程。init进程扮演终结父进程的角色。因为init进程永远不会终止,所以系统总是可以确信它的存在,并在必要的时候以它为参照。如果某个进程它在衍生出来的全部子进程结束之前被终止,就会出现必须以init为参照的情况。此时那些失去了父进程的子进程就都会以init作为它们的父进程。如果执行一下ps-af命令,可以列出许多父进程ID为1的进程来。Linux提供了一条pstree命令,允许用户查看系统内正在运行的各个进程之间的继承关系。直接在命令行中输入pstree即可,程序会以树状结构方式列出系统中正在运行的各进程之间的继承关系。

三.理解Linux下进程的结构

Linux中一个进程在内存里有三部分数据,就是“数据段”、“堆栈段”、“代码段”。基于I386兼容的中央处理器,都有上述三种段寄存器,以方便操作系统的运行,如下图所示。


代码段

数据段

堆栈段

代码段是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。而数据段则存放程序的全局变量、常数及动态数据分配的数据空间。堆栈段存放的就是子进程的返回地址、子程序的参数及程序的局部变量。堆栈段包含在进程控制块PCB(Process Control Block)中。PCB处于进程核心堆栈的底部,不需要额外分配空间。

四.进程状态

现在我们来看看,进程在生存周期中的各种状态及状态的转换。下面是Linux系统的进程状态模型的各种状态。

  • 用户状态:进程在用户状态下运行的状态。
  • 内核状态:进程在内核状态下运行的状态。
  • 内存中就绪:进程没有执行,但处于就绪状态,只要内核调度它,就可以执行。
  • 内存中睡眠:进程正在睡眠并且进程存储在内存中,没有被交换到SWAP设备。
  • 就绪且换出:进程处于就绪状态,但是必须把它换入内存,内核才能再次调度它运行。
  • 睡眠且换出:进程正在睡眠,且被换出内存。
  • 被抢先:进程从内核状态返回用户状态时,内核抢先于它做了上下文切换,调度了另一个进程。原先这个进程就处于被抢先状态。
  • 僵死状态(zombie):进程调用exit结束,进程不再存在,但在进程表项中仍有记录,该记录可由父进程收集。

现在我们从进程的创建到退出来看看进程的状态转化。需要说明的是,进程在它的生命周期里并不一定要经历所有状态。

五.Linux进程的创建

fork函数在Linux下产生新的进程的系统调用,这个函数名是英文中“分叉”的意思。为什么取这个名字呢? 因为一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就“分叉”了,所以这个名字取得很形象。fork的语法如下所示:

复制代码

代码如下:

#include <unistd.h>
#include <sys/types.h>
pid_t fork();

在Linux网络编程中经常用到fork()系统调用。例如在一个客户机/Web服务器构建的网络环境中,Web服务器往往可以满足许多客户端的请求。如果一个客户机要访问Web服务器,需要发送一个请求,此时由服务器生成一个父进程,然后父进程通过fork()系统调用产生一个子进程,此时客户机的请求由子进程完成。父进程可以再度回到等待状态不断服务其他客户端。原理如下图所示。

Linux进程和线程的基础与管理

有一个更简单的执行其他程序的函数system,参数string传递给一个命令解释器(一般为sh)执行,即string被解释为一条命令,由sh执行该命令。若参数string为一个空指针,则检查命令解释器是否存在。该命令可以和同命令行下的命令形式相同,但由于命令作为一个参数放在系统调用中,应注意编译时对特殊意义字符的处理。命令的查找是按PATH环境变量的定义执行的。命令所生成的后果一般不会对父进程编程造成影响。返回值:当参数为空指针时,只有当命令解释器有效时返回值为非零。若参数不为空指针,返回值为该命令的返回状态(同waitpid())的返回值。命令无效或语法错误则返回非零值,所执行的命令被终止。其他情况则返回-1.它是一个较高层的函数,实际上相当于在shell下执行一条命令,除了system之外,系统调用exec来执行一个可执行文件,来代替当前进程的执行映像。系统调用exit的功能是终止发出调用的进程。sleep函数调用用来指定进程挂起的秒数。wait函数族用来等待和控制进程。poppen函数和system函数类似,区别是它用管道方式处理输出。

父进程和子进程的关系是管理和被管理的关系,当父进程终止时,子进程也随之而终止。但子进程终止时,父进程并不一定终止。比如httpd服务器运行时,我们可以杀掉其子进程,父进程并不会因为子进程的终止而终止。

六.进程的管理

1.启动进程

输入需要运行的程序的程序名,执行一个程序,其实也就是启动了一个进程。在Linux系统中,每个进程都具有一个进程号(PID),用于系统识别和调度进程。启动一个进程有两个主要途径:手工启动和调度启动,后者是事先进行设置,根据用户要求自动启动。由用户输入命令,直接启动一个进程便是手工启动进程。但手工启动进程又可以分为很多种,根据启动的进程类型不同;性质不同,实际结果也不一样。

(1)前台启动

前台启动是手工启动一个进程的最常用的方式。用户键入一个命令“df”,就已经启动了一个进程,而且是一个前台的进程。这时候系统其实已经处于多进程状态。有许多运行在后台的、系统启动时就已经自动启动的进程正在悄悄运行着。有的用户在键入“df”命令以后赶紧使用“ps -x”查看,却没有看到df进程,会觉得很奇怪。其实这里因为df这个进程结束太快,使用ps查看时该进程已经执行结束了。如果启动一个比较耗时的进程,例如在根命令下运行:find,然后使用ps aux查看,就会看到在里面有一个find进程。

(2)后台启动

直接从后台手工启动一个进程用得比较小一些,除非是该进程甚为耗时,且用户也不急着需要结果。假设用户要启动一个需要长时间运行的格式化文本文件的进程,为了不使整个shell在格式化过程中都处于“瘫痪”状态,从后台启动这个进程是明智的选择。

2.进程调度

当需要中断一个前台进程的时候,通常使用Ctrl+C组合键。但是对于一个后台进程,就不是一个组合键所能解决的了,这时就必须使用kill命令。该命令可以终止后台进程。至于终止后台进程的原因有很多,或许是该进程占用的CPU时间过多;或许是该进程已经挂死。这种情况是经常发生的。kill命令的工作原理是:向Linux系统的内核发送一个系统操作信号和某个程序的进程标识号,然后系统内核就可以对进程标识号指定的进程进行操作。

七.Linux的第一个进程:init

init是Linux系统执行的第一个进程,进程ID为1,是系统所有进程的起点,主要用来执行一些开机初始化脚本和监视进程。Linux系统在完成核内引导以后就开始运行init程序,init程序需要读取配置文件/etc/inittab。Inittab是一个不可执行的文本文件,它由若干行命令所组成。

在RHEL 4系统中,inittab配置文件的内容如下所示:

复制代码

代码如下:

#
#inittab
#
#
#author
#
#Default runlevel.the runlevels used by rhs are:
#0 - halt (do not set initdefault to this)
#1 - single user mode
#2 - multiuser,without nfs (the same as 3, if you do not haver networking)
#3 - full multiuser mode
#4 - unused
#5 - X11
#6 - reboot (do not set initdefault to this)
#
//表示当前缺省运行级别为5,启动系统进入图形化界面
id:5:initdefault:
//启动时自动执行/etc/rc.d/rc.sysinit脚本
#system initialization.
si::sysinit:/etc/rc.d/rc.sysinit
10:0:wait:/etc/rc.d/rc 0
11:1:wait:/etc/rc.d/rc 1
12:2:wait:/etc/rc.d/rc 2
13:3:wait:/etc/rc.d/rc 3
14:4:wait:/etc/rc.d/rc 4
//当运行级别为5时,以5为参数运行/etc/rc.d/rc脚本,init将等待其返回
15:5:wait:/etc/rc.d/rc 5
16:6:wait:/etc/rc.d/rc 6
//在启动过程中允许按[ctrl-alt-delete]重启系统
#trap ctrl-alt-delete
ca::ctrlaltdel:/sbin/shutdown -t3 -r now
#
..................................
#
//在运行级别2、3、4、5以上ttyX为参数执行/sbin/mingetty程序,打开ttyX终端用于用户登录,如果进程退出则再次运行mingetty程序
#run gettys in standard runlevels
1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6
//在级别5上运行xdm程序,提供xdm图形方式登录界面,并在退出时重新执行
x:5:respawn:/etc/x11/prefdm -nodaemon
#run xdm in runleverl 5

Inittab配置文件每行的基本格式如下。
id:runlevels:action:procees

其中某些部分可以为空,下面我们逐一介绍。

1.id

1~2个字符,配置行的惟一标识,在配置文件中不能重复。

2.runlevels

配置行适用的运行级别,在这里可填入多个运行级别,比如12345或者35等。

Linux有7个运行级别:

0:关机
1:单用户字符界面
2:不具备网络文件系统(NFS)功能的多用户字符界面
3:具有网络功能的多用户字符界面
4:保留不用
5:具有网络功能的图形用户界面
6:重新启动系统

3.action

init有如下几种行为,如下表所示。

init行为


行为

描述

respawn

启动并监视第4项指定的process,若process终止则重启它

wait

执行第4项指定的process,并等待它执行完备

once

执行第4项指定的process

boot

不论在哪个执行等级,系统启动时都会运行第4项指定的process

bootwait

不论在哪个执行等级,系统启动时都会运行第4项指定的process,且一直等它执行完备

off

关闭任何动作,相当于忽略该配置行

ondemand

进入ondemand执行等级时,执行第4项指定的process

initdefault

系统启动后进入的执行等级,该行不需要指定process

sysinit

不论在哪个执行等级,系统会在执行boot及bootwait之前执行第4项指定的process

powerwait

当系统的供电不足时执行第4项指定的process,且一直等它执行完备

powerfailnow

当系统的供电严重不足时执行第4项指定的process

ctrlaltdel

当用户按下ctrl+alt+del 时执行的操作

kbrequest

当用户按下特殊的组合键时执行第4项指定的process,此组合键需在keymaps文件定义

4.process

Process为init执行的进程,这些进程都保存在目录/etc/rc.d/rcX中,其中的X代表运行级别,rc程序接收X参数,然后运行/etc/rc.d/rc.X下面的程序。使用如下命令可以查看/etc/rc.d目录内容。

复制代码

代码如下:

#ls –l /etc/rc.d/
total 112
drwxr-xr-x 2 root root 4096 3/15 14:44 init.d
-rxwr-xr-x 1 root root 2352 2004-3-17 rc
drwxr-xr-x 2 root root 4096 3/15 14:44 rc0.d
drwxr-xr-x 2 root root 4096 3/15 14:44 rc1.d
drwxr-xr-x 2 root root 4096 3/15 14:44 rc2.d
drwxr-xr-x 2 root root 4096 3/15 14:44 rc3.d
drwxr-xr-x 2 root root 4096 3/15 14:44 rc4.d
drwxr-xr-x 2 root root 4096 3/15 14:44 rc5.d
drwxr-xr-x 2 root root 4096 3/15 14:44 rc6.d
-rxwr-xr-x 1 root root 2200 2004-3-17 rc.local
-rxwr-xr-x 1 root root 2352 2004-3-17 rc.sysinit
…………

使用如下命令查看/etc/rc.d/rc5.d的内容。

复制代码

代码如下:

#ls –l /etc/rc.d/rc5.d

这些文件都是符号链接,以S打头的标识启动该程序,而以K打头的标识终止该程序,后面的数字标识执行顺序,越小越先执行,剩下的标识程序名。系统启动或者切换到该运行级别时会执行以S打头的程序,系统切换到该运行级别时会执行以K打头的程序。

这个目录下的程序可通过chkconfig程序进行管理,当然这个目录下的程序需要符合一定规范,如果了解shell编程,可以查看这些符号链接所指向的程序的源码。

init也是一个进程,和普通的进程具有一样的属性。比如修改了/etc/inittab,想让修改马上生效,可通过运行“kill-SIGHUP 1”来实现,也可通过运行“init q”来实现。

八.Linux的线程简介

1.Linux线程的定义

线程(thread)是在共享内存空间中并发的多道执行路径,它们共享一个进程的资源,如文件描述和信号处理。在两个普通进程(非线程)间进行切换时,内核准备从一个进程的上下文切换到另一个进程的上下文要花费很大的开销。这里上下文切换的主要任务是保存老进程CPU状态并加载新进程的保存状态,用新进程的内存映像替换进程的内存映像。线程允许你的进程在几个正在运行的任务之间进行切换,而不必执行前面提到的完整的上下文。另外本文介绍的线程是针对POSIX线程的,也就是Pthread。也因为Linux对它的支持最好,相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。也可以将线程和轻量级进程(LWP)视为等同的,但其实在不同的系统/实现中有不同的解释,LWP更恰当的解释为一个虚拟CPU或内核的线程。它可以帮助用户态线程实现一些特殊的功能。Pthread是一种标准化模型,它用来把一个程序分成一组能够同时执行的任务。

2.什么场合使用Pthread,即线程

(1)在返回前阻塞的I/O任务能够使用一个线程处理I/O,同时继续执行其他处理任务。
(2)在有一个或多个任务受不确定性事件,比如网络通信的可获得性影响的场合,能够使用线程处理这些异步事件,同时继续执行正常的处理。
(3)如果某些程序功能比其他的功能更重要,可以使用线程以保证所有功能都出现,但那些时间密集型的功能具有更高的优先级。

以上三点可以归纳为:在检查程序中潜在的并行性时,也就是说在要找出能够同时执行任务时使用Pthread。上面已经介绍了,Linux进程模型提供了执行多个进程的能力,已经可以进行并行或并发编程,可是纯种能够让你对多个任务的控制程序更好、使用资源更少,因为一个单一的资源,如全局变量,可以由多个线程共享。而且,在拥有多个处理器的系统上,多线程应用会比用多个进程实现的应用执行速度更快。

3.Linux进程和线程的发展

1999年1月发布的Linux 2.2内核中,进程是通过系统调用fork创建的,新的进程是原来进程的子进程。需要说明的是,在2.2.x版本中,不存在真正意义上的线程(thread)。Linux中常用的线程Pthread实际上是通过进程来模拟的。也就是说Linux中的线程也是通过fork创建的,是“轻”进程。Linux 2.2只默认允许4096个进程/线程同时运行。高端系统同时要服务上千个用户,所以这显然是一个问题,它一度是阻碍Linux进入企业级市场的一大因素。

2001年1月发布的Linux 2.4内核消除了这个限制,并且允许在系统运行中动态调整进程数上限。因此,进程数现在只受制于物理内存的多少。在高端服务器上,即使安装了512MB内存,现在也能轻而易举地同时支持1万6千个进程。

2003年12月发布的2.6内核,进程调度经过重新编写,去掉了以前版本中效率不高的算法。以前,为了决定下一步要运行哪一个任务,进程调度程序要查看每一个准备好的任务,并且经过计算机来决定哪一个任务相对来更为重要。进程标识号(PID)的数目也从32000升到10亿。内核内部的大改变之一就是Linux的线程框架被重写,以使NPTL(Native POSIX Thread Library)可以运行于其上。对于运行负荷繁重的线程应用的Pentium Pro及更先进的处理器而言,这是一个主要的性能提升,也是企业级应用中的很多高端系统一直以来所期待的。线程框架的改变包含Linux线程空间中的许多新的概念,包括线程组、线程各自的本地存储区、POSIX风格信号,以及其他改变。改进后的多线程和内存管理技术有助于更好地运行大型多媒体应用软件。

4.总结

线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在对称处理器的计算机上运行,而进程则可以跨机器迁移。另外,进程可以拥有资源,线程共享进程拥有的资源。进程间的切换必须保存在进程控制块PCB(Process Control Block)中。同一个进程的多个线程间的切换不用那么麻烦。最后一个实例来作为本文的结束:当你在一台Linux PC上打开两个OICQ,每一个OICQ是一个进程;而当你在一个OICQ上和多人聊天时,每一个聊天窗口就是一个线程。