月份: 2014-07

黑夜

黑暗的夜里

湖水黑的发冷

树林里像是有风

蝙蝠倒挂在树上闭着眼睛

乌鸦扇动着翅膀挡住了月亮

连猫头鹰也只睁着一只眼睛

 

我一个人站在湖边

感觉像是被风轻轻的带走

飘过没有倒影的湖面

远处依旧黑暗

看太阳的人

当你看太阳的时候也看见了我

当我看太阳的时候也看见了你

 

 

为什么Linux不需要磁盘碎片整理

linux系统

如果你是个Linux用户,你可能听说过不需要去对你的linux文件系统进行磁盘碎片整理。也许你注意到了,在Liunx安装发布包里没有磁盘碎片整理的工具。为什么会这样?

为了理解为什么Linux文件系统不需要磁盘碎片整——而Windows却需要——你需要理解磁盘碎片产生的原理,以及Linux和Windows文件系统它们之间工作原理的不同之处。

什么是磁盘碎片

很多Windows用户,甚至是没有经验的用户,都深信经常对文件系统进行碎片整理会提高计算机的速度。但并不是很多人知道这其中的原委。

简单的说,磁盘驱动器上有很多扇区,每个扇区都能存放一小段数据。文件,特别是大文件的存储需要占用很多不同的扇区。假设现在你有很多个文件存在的文件系统里,每个文件都会被存储在一系列连续的扇区里。后来你更新了其中的一个文件,它的体积变大了。文件系统会尝试把文件新增的部分存放到紧邻原始文件的扇区里。可不幸的是,它周边已经没连续的足够扇区空间了,文件需要被分割成数段——这些都在自动进行的。当从磁盘上读取这个文件时,磁盘磁头需要跨越数个不同的物理位置来读取各个扇区——这样会使速度降低。

磁盘碎片整理就是小心的移动这些小文件块来减少碎片,让每个文件都能连续的分布在磁盘上。

windows系统

当然,如果是固态硬盘,那情况又不同了,固态硬盘没有机械移动,不应该进行碎片整理——对一个U盘进行碎片整理通常会降低它的寿命。在最新版的Windows系统里,你实际上不需要关心系统上的碎片——Windows会自动替你整理。

Windows文件系统的工作原理

微软老的FAT文件系统——最后一次使用是在Windows 98 和 Window ME上,可如今的U盘上还在使用它——并不会智能的管理文件。当你把一个文件存入FAT文件系统里时,系统会尽量的把它存到靠近磁盘开始的地方。当你存入第二个文件时,它会紧接着第一个文件——一个接着一个。当原始文件体积变大后,它们一定会产生碎片。根本没有留给它们增长的空间。

微软新的NTFS文件系统——使用在装有Windows XP和2000的PC机上——稍微智能了一点。它会在磁盘上在文件周围分配一些“缓冲”的空闲空间,尽管如此,任何Windows用户都会告诉你,经过一段时间的使用后,NTFS文件系统还是会形成碎片。

由于这些文件系统的工作原理,它们注定需要进行碎片整理来保持高性能。微软在它最新的视窗系统里通过在后台运行一个磁盘碎片整理进程来解决这个问题。

windows磁盘碎片整理

Linux文件系统的工作原理

Linux的ext2, ext3, 和 ext4 文件系统 ——Ubuntu和大多数最新的Linux发布版中使用的是ext4——采用了一种更聪明的方法来存放文件。与把多个文件并排放在磁盘上不同,Linux文件系统把所有文件都分散到了整个磁盘上,每两个文件之间都留有相当巨大的空闲空间。当文件被修改、体积增加时,它们通常有足够的空间来扩展。一旦有碎片产生时,文件系统会尝试移动整个文件来消除碎片,所以你不需要一个碎片整理工具。

linux文件系统

因为这种工作方式,当磁盘快要装满时,你开始会发现有碎片开始产生。如果已用空间超过95%(甚至80%),你会发现碎片开始变多。但不管怎样,这个文件系统的设计会使正常情况下不产生碎片。

如果你在Linux系统上遇到了磁盘碎片问题,你很可能需要一个更大的硬盘了。如果你真想整理一个文件系统,这最简单的方式也许是最可靠的方式:把文件从这个分区里考出,删除这些文件,然后考回这些文件。当你把文件考回硬盘时,文件系统会智能为文件分配存储空间。

你可以使用 fsck 命令来查看Linux文件系统上的磁盘碎片情况——在输出结果里寻找“non-contiguous inodes”信息。

15、Linux文件系统的实现

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

Linux文件管理从用户的层面介绍了Linux管理文件的方式。Linux有一个树状结构来组织文件。树的顶端为根目录(/),节点为目录,而末端的叶子为包含数据的文件。当我们给出一个文件的完整路径时,我们从根目录出发,经过沿途各个目录,最终到达文件。

我们可以对文件进行许多操作,比如打开和读写。在Linux文件管理相关命令中,我们看到许多对文件进行操作的命令。它们大都基于对文件的打开和读写操作。比如cat可以打开文件,读取数据,最后在终端显示:

$cat test.txt

 

对于Linux下的程序员来说,了解文件系统的底层组织方式,是深入进行系统编程所必备的。即使是普通的Linux用户,也可以根据相关的内容,设计出更好的系统维护方案。

 

存储设备分区

文件系统的最终目的是把大量数据有组织的放入持久性(persistant)的存储设备中,比如硬盘和磁盘。这些存储设备与内存不同。它们的存储能力具有持久性,不会因为断电而消失;存储量大,但读取速度慢。

 

观察常见存储设备。最开始的区域是MBR,用于Linux开机启动(参考Linux开机启动)。剩余的空间可能分成数个分区(partition)。每个分区有一个相关的分区表(Partition table),记录分区的相关信息。这个分区表是储存在分区之外的。分区表说明了对应分区的起始位置和分区的大小。

 

我们在Windows系统常常看到C分区、D分区等。Linux系统下也可以有多个分区,但都被挂载在同一个文件系统树上。

数据被存入到某个分区中。一个典型的Linux分区(partition)包含有下面各个部分:

 

分区的第一个部分是启动区(Boot block),它主要是为计算机开机服务的。Linux开机启动后,会首先载入MBR,随后MBR从某个硬盘的启动区加载程序。该程序负责进一步的操作系统的加载和启动。为了方便管理,即使某个分区中没有安装操作系统,Linux也会在该分区预留启动区。

启动区之后的是超级区(Super block)。它存储有文件系统的相关信息,包括文件系统的类型,inode的数目,数据块的数目。

随后是多个inodes,它们是实现文件存储的关键。在Linux系统中,一个文件可以分成几个数据块存储,就好像是分散在各地的龙珠一样。为了顺利的收集齐龙珠,我们需要一个“雷达”的指引:该文件对应的inode。每个文件对应一个inode。这个inode中包含多个指针,指向属于该文件各个数据块。当操作系统需要读取文件时,只需要对应inode的”地图”,收集起分散的数据块,就可以收获我们的文件了。

 

最后一部分,就是真正储存数据的数据块们(data blocks)了。

 

inode简介

上面我们看到了存储设备的宏观结构。我们要深入到分区的结构,特别是文件在分区中的存储方式。

文件是文件系统对数据的分割单元。文件系统用目录来组织文件,赋予文件以上下分级的结构。在硬盘上实现这一分级结构的关键,是使用inode来虚拟普通文件和目录文件对象。

 

Linux文件管理中,我们知道,一个文件除了自身的数据之外,还有一个附属信息,即文件的元数据(metadata)。这个元数据用于记录文件的许多信息,比如文件大小,拥有人,所属的组,修改日期等等。元数据并不包含在文件的数据中,而是由操作系统维护的。事实上,这个所谓的元数据就包含在inode中。我们可以用$ls -l filename来查看这些元数据。正如我们上面看到的,inode所占据的区域与数据块的区域不同。每个inode有一个唯一的整数编号(inode number)表示。

 

在保存元数据,inode是“文件”从抽象到具体的关键。正如上一节中提到的,inode储存由一些指针,这些指针指向存储设备中的一些数据块,文件的内容就储存在这些数据块中。当Linux想要打开一个文件时,只需要找到文件对应的inode,然后沿着指针,将所有的数据块收集起来,就可以在内存中组成一个文件的数据了。

数据块在1, 32, 0, …

inode并不是组织文件的唯一方式。最简单的组织文件的方法,是把文件依次顺序的放入存储设备,DVD就采取了类似的方式。但如果有删除操作,删除造成的空余空间夹杂在正常文件之间,很难利用和管理。

复杂的方式可以使用链表,每个数据块都有一个指针,指向属于同一文件的下一个数据块。这样的好处是可以利用零散的空余空间,坏处是对文件的操作必须按照线性方式进行。如果想随机存取,那么必须遍历链表,直到目标位置。由于这一遍历不是在内存进行,所以速度很慢。

FAT系统是将上面链表的指针取出,放入到内存的一个数组中。这样,FAT可以根据内存的索引,迅速的找到一个文件。这样做的主要问题是,索引数组的大小与数据块的总数相同。因此,存储设备很大的话,这个索引数组会比较大。

inode既可以充分利用空间,在内存占据空间不与存储设备相关,解决了上面的问题。但inode也有自己的问题。每个inode能够存储的数据块指针总数是固定的。如果一个文件需要的数据块超过这一总数,inode需要额外的空间来存储多出来的指针。

 

inode示例

在Linux中,我们通过解析路径,根据沿途的目录文件来找到某个文件。目录中的条目除了所包含的文件名,还有对应的inode编号。当我们输入$cat /var/test.txt时,Linux将在根目录文件中找到var这个目录文件的inode编号,然后根据inode合成var的数据。随后,根据var中的记录,找到text.txt的inode编号,沿着inode中的指针,收集数据块,合成text.txt的数据。整个过程中,我们参考了三个inode:根目录文件,var目录文件,text.txt文件的inodes。

在Linux下,可以使用$stat filename,来查询某个文件对应的inode编号。

在存储设备中实际上存储为:

 

当我们读取一个文件时,实际上是在目录中找到了这个文件的inode编号,然后根据inode的指针,把数据块组合起来,放入内存供进一步的处理。当我们写入一个文件时,是分配一个空白inode给该文件,将其inode编号记入该文件所属的目录,然后选取空白的数据块,让inode的指针指像这些数据块,并放入内存中的数据。

 

文件共享

在Linux的进程中,当我们打开一个文件时,返回的是一个文件描述符。这个文件描述符是一个数组的下标,对应数组元素为一个指针。有趣的是,这个指针并没有直接指向文件的inode,而是指向了一个文件表格,再通过该表格,指向加载到内存中的目标文件的inode。如下图,一个进程打开了两个文件。

可以看到,每个文件表格中记录了文件打开的状态(status flags),比如只读,写入等,还记录了每个文件的当前读写位置(offset)。当有两个进程打开同一个文件时,可以有两个文件表格,每个文件表格对应的打开状态和当前位置不同,从而支持一些文件共享的操作,比如同时读取。

要注意的是进程fork之后的情况,子进程将只复制文件描述符的数组,而和父进程共享内核维护的文件表格和inode。此时要特别小心程序的编写。

 

总结

这里概括性的总结了Linux的文件系统。Linux以inode的方式,让数据形成文件。

了解Linux的文件系统,是深入了解操作系Linux原理的重要一步。

14、Linux进程间通信

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

谢谢nonoob纠错

 

我们在Linux信号基础中已经说明,信号可以看作一种粗糙的进程间通信(IPC, interprocess communication)的方式,用以向进程封闭的内存空间传递信息。为了让进程间传递更多的信息量,我们需要其他的进程间通信方式。这些进程间通信方式可以分为两种:

  • 管道(PIPE)机制。在Linux文本流中,我们提到可以使用管道将一个进程的输出和另一个进程的输入连接起来,从而利用文件操作API来管理进程间通信。在shell中,我们经常利用管道将多个进程连接在一起,从而让各个进程协作,实现复杂的功能。
  • 传统IPC (interprocess communication)。我们主要是指消息队列(message queue),信号量(semaphore),共享内存(shared memory)。这些IPC的特点是允许多进程之间共享资源,这与多线程共享heap和global data相类似。由于多进程任务具有并发性 (每个进程包含一个进程,多个进程的话就有多个线程),所以在共享资源的时候也必须解决同步的问题 (参考Linux多线程与同步)。

 

管道与FIFO文件

一个原始的IPC方式是所有的进程通过一个文件交流。比如我在纸(文件)上写下我的名字和年纪。另一个人读这张纸,会知道我的名字和年纪。他也可以在同一张纸上写下他的信息,而当我读这张纸的话,同样也可以知道别人的信息。但是,由于硬盘读写比较慢,所以这个方式效率很低。那么,我们是否可以将这张纸放入内存中以提高读写速度呢?

Linux文本流中,我们已经讲解了如何在shell中使用管道连接多个进程。同样,许多编程语言中,也有一些命令用以实现类似的机制,比如在Python子进程中使用Popen和PIPE,在C语言中也有popen库函数来实现管道 (shell中的管道就是根据此编写的)。管道是由内核管理的一个缓冲区(buffer),相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。


从原理上,管道利用fork机制建立(参考Linux进程基础Linux从程序到进程),从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。

由于基于fork机制,所以管道只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了FIFO方式连接进程。FIFO又叫做命名管道(named PIPE)。

FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来(好像是传送带,一头放货,一头取货),从而保证信息交流的顺序。FIFO只是借用了文件系统(file system, 参考Linux文件管理背景知识)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。

 

传统IPC

这几种传统IPC实际上有很悠久的历史,所以其实现方式也并不完善 (比如说我们需要某个进程负责删除建立的IPC)。一个共同的特征是它们并不使用文件操作的API。对于任何一种IPC来说,你都可以建立多个连接,并使用键值(key)作为识别的方式。我们可以在一个进程中中通过键值来使用的想要那一个连接 (比如多个消息队列,而我们选择使用其中的一个)。键值可以通过某种IPC方式在进程间传递(比如说我们上面说的PIPE,FIFO或者写入文件),也可以在编程的时候内置于程序中。

在几个进程共享键值的情况下,这些传统IPC非常类似于多线程共享资源的方式(参看Linux多线程与同步):

  • semaphore与mutex类似,用于处理同步问题。我们说mutex像是一个只能容纳一个人的洗手间,那么semaphore就像是一个能容纳N个人的洗手间。其实从意义上来说,semaphore就是一个计数锁(我觉得将semaphore翻译成为信号量非常容易让人混淆semaphore与signal),它允许被N个进程获得。当有更多的进程尝试获得semaphore的时候,就必须等待有前面的进程释放锁。当N等于1的时候,semaphore与mutex实现的功能就完全相同。许多编程语言也使用semaphore处理多线程同步的问题。一个semaphore会一直存在在内核中,直到某个进程删除它。
  • 共享内存与多线程共享global data和heap类似。一个进程可以将自己内存空间中的一部分拿出来,允许其它进程读写。当使用共享内存的时候,我们要注意同步的问题。我们可以使用semaphore同步,也可以在共享内存中建立mutex或其它的线程同步变量来同步。由于共享内存允许多个进程直接对同一个内存区域直接操作,所以它是效率最高的IPC方式。

消息队列(message queue)与PIPE相类似。它也是建立一个队列,先放入队列的消息被最先取出。不同的是,消息队列允许多个进程放入消息,也允许多个进程取出消息。每个消息可以带有一个整数识别符(message_type)。你可以通过识别符对消息分类 (极端的情况是将每个消息设置一个不同的识别符)。某个进程从队列中取出消息的时候,可以按照先进先出的顺序取出,也可以只取出符合某个识别符的消息(有多个这样的消息时,同样按照先进先出的顺序取出)。消息队列与PIPE的另一个不同在于它并不使用文件API。最后,一个队列不会自动消失,它会一直存在于内核中,直到某个进程删除该队列。

 

多进程协作可以帮助我们充分利用多核和网络时代带来的优势。多进程可以有效解决计算瓶颈的问题。互联网通信实际上也是一个进程间通信的问题,只不过这多个进程分布于不同的电脑上。网络连接是通过socket实现的。由于socket内容庞大,所以我们不在这里深入。一个小小的注解是,socket也可以用于计算机内部进程间的通信。

 

总结

PIPE, FIFO

semaphore, message queue, shared memory; key

13、Linux多线程与同步

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

典型的UNIX系统都支持一个进程创建多个线程(thread)。在Linux进程基础中提到,Linux以进程为单位组织操作,Linux中的线程也都基于进程。尽管实现方式有异于其它的UNIX系统,但Linux的多线程在逻辑和使用上与真正的多线程并没有差别。

 

多线程

我们先来看一下什么是多线程。在Linux从程序到进程中,我们看到了一个程序在内存中的表示。这个程序的整个运行过程中,只有一个控制权的存在。当函数被调用的时候,该函数获得控制权,成为激活(active)函数,然后运行该函数中的指令。与此同时,其它的函数处于离场状态,并不运行。如下图所示:

Linux从程序到进程

 

我们看到,各个方块之间由箭头连接。各个函数就像是连在一根线上一样,计算机像一条流水线一样执行各个函数中定义的操作。这样的一个程序叫做单线程程序。
多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行。即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行的效果。如下图所示,就是一个多线程的流程:

main()到func3()再到main()构成一个线程,此外func1()和func2()构成另外两个线程。操作系统一般都有一些系统调用来让你将一个函数运行成为一个新的线程。

 

回忆我们在Linux从程序到进程中提到的栈的功能和用途。一个栈,只有最下方的帧可被读写。相应的,也只有该帧对应的那个函数被激活,处于工作状态。为了实现多线程,我们必须绕开栈的限制。为此,创建一个新的线程时,我们为这个线程建一个新的栈。每个栈对应一个线程。当某个栈执行到全部弹出时,对应线程完成任务,并收工。所以,多线程的进程在内存中有多个栈。多个栈之间以一定的空白区域隔开,以备栈的增长。每个线程可调用自己栈最下方的帧中的参数和变量,并与其它线程共享内存中的Text,heap和global data区域。对应上面的例子,我们的进程空间中需要有3个栈。

(要注意的是,对于多线程来说,由于同一个进程空间中存在多个栈,任何一个空白区域被填满都会导致stack overflow的问题。)

 

并发

多线程相当于一个并发(concunrrency)系统。并发系统一般同时执行多个任务。如果多个任务可以共享资源,特别是同时写入某个变量的时候,就需要解决同步的问题。比如说,我们有一个多线程火车售票系统,用全局变量i存储剩余的票数。多个线程不断地卖票(i = i – 1),直到剩余票数为0。所以每个都需要执行如下操作:

复制代码
/*mu is a global mutex*/

while (1) {                        /*infinite loop*/
    if (i != 0) i = i -1
    else {
      printf("no more tickets");
      exit();
    }
}
复制代码

如果只有一个线程执行上面的程序的时候(相当于一个窗口售票),则没有问题。但如果多个线程都执行上面的程序(相当于多个窗口售票), 我们就会出现问题。我们会看到,其根本原因在于同时发生的各个线程都可以对i读取和写入。

我们这里的if结构会给CPU两个指令, 一个是判断是否有剩余的票(i != 0), 一个是卖票 (i = i -1)。某个线程会先判断是否有票(比如说此时i为1),但两个指令之间存在一个时间窗口,其它线程可能在此时间窗口内执行卖票操作(i = i -1),导致该线程卖票的条件不再成立。但该线程由于已经执行过了判断指令,所以无从知道i发生了变化,所以继续执行卖票指令,以至于卖出不存在的票 (i成为负数)。对于一个真实的售票系统来说,这将成为一个严重的错误 (售出了过多的票,火车爆满)。

在并发情况下,指令执行的先后顺序由内核决定。同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清除哪一个会先执行。如果运行的结果依赖于不同线程执行的先后的话,那么就会造成竞争条件(race condition),在这样的状况下,计算机的结果很难预知。我们应该尽量避免竞争条件的形成。最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分隔的一个原子操作(atomic operation),而其它任务不能插入到原子操作中。

 

多线程同步

对于多线程程序来说,同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源 。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。

 

1) 互斥锁

互斥锁是一个特殊的变量,它有锁上(lock)和打开(unlock)两个状态。互斥锁一般被设置成全局变量。打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开。其它想要获得互斥锁的线程,会等待直到互斥锁再次打开的时候。我们可以将互斥锁想像成为一个只能容纳一个人的洗手间,当某个人进入洗手间的时候,可以从里面将洗手间锁上。其它人只能在互斥锁外面等待那个人出来,才能进去。在外面等候的人并没有排队,谁先看到洗手间空了,就可以首先冲进去。

上面的问题很容易使用互斥锁的问题解决,每个线程的程序可以改为:

复制代码
/*mu is a global mutex*/

while (1) {                /*infinite loop*/
  mutex_lock(mu);           /*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/
  if (i != 0) i = i - 1;
  else {
    printf("no more tickets");
    exit();
  }
  mutex_unlock(mu);         /*release mutex, make it unblocked*/
}
复制代码

第一个执行mutex_lock()的线程会先获得mu。其它想要获得mu的线程必须等待,直到第一个线程执行到mutex_unlock()释放mu,才可以获得mu,并继续执行线程。所以线程在mutex_lock()和mutex_unlock()之间的操作时,不会被其它线程影响,就构成了一个原子操作。

需要注意的时候,如果存在某个线程依然使用原先的程序 (即不尝试获得mu,而直接修改i),互斥锁不能阻止该程序修改i,互斥锁就失去了保护资源的意义。所以,互斥锁机制需要程序员自己来写出完善的程序来实现互斥锁的功能。我们下面讲的其它机制也是如此。

 

2) 条件变量

条件变量是另一种常用的变量。它也常常被保存为全局变量,并和互斥锁合作。

 

假设这样一个状况: 有100个工人,每人负责装修一个房间。当有10个房间装修完成的时候,老板就通知相应的十个工人一起去喝啤酒。

我们如何实现呢?老板让工人在装修好房间之后,去检查已经装修好的房间数。但多线程条件下,会有竞争条件的危险。也就是说,其他工人有可能会在该工人装修好房子和检查之间完成工作。采用下面方式解决:

复制代码
/*mu: global mutex, cond: global codition variable, num: global int*/
mutex_lock(mu)

num = num + 1;                      /*worker build the room*/

if (num <= 10) {                     /*worker is within the first 10 to finish*/
    cond_wait(mu, cond);            /*wait*/
    printf("drink beer");
}
else if (num = 11) {                /*workder is the 11th to finish*/
  cond_broadcast(mu, cond);         /*inform the other 9 to wake up*/
}

mutex_unlock(mu);
复制代码

上面使用了条件变量。条件变量除了要和互斥锁配合之外,还需要和另一个全局变量配合(这里的num, 也就是装修好的房间数)。这个全局变量用来构成各个条件。

 

具体思路如下。我们让工人在装修好房间(num = num + 1)之后,去检查已经装修好的房间数( num < 10 )。由于mu被锁上,所以不会有其他工人在此期间装修房间(改变num的值)。如果该工人是前十个完成的人,那么我们就调用cond_wait()函数。
cond_wait()做两件事情,一个是释放mu,从而让别的工人可以建房。另一个是等待,直到cond的通知。这样的话,符合条件的线程就开始等待。

当有通知(第十个房间已经修建好)到达的时候,condwait()会再次锁上mu。线程的恢复运行,执行下一句prinft(“drink beer”) (喝啤酒!)。从这里开始,直到mutex_unlock(),就构成了另一个互斥锁结构。

那么,前面十个调用cond_wait()的线程如何得到的通知呢?我们注意到elif if,即修建好第11个房间的人,负责调用cond_broadcast()。这个函数会给所有调用cond_wait()的线程放送通知,以便让那些线程恢复运行。

 

条件变量特别适用于多个线程等待某个条件的发生。如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。

 

3) 读写锁

读写锁与互斥锁非常相似。r、RW lock有三种状态: 共享读取锁(shared-read), 互斥写入锁(exclusive-write lock), 打开(unlock)。后两种状态与之前的互斥锁两种状态完全相同。

一个unlock的RW lock可以被某个线程获取R锁或者W锁。

如果被一个线程获得R锁,RW lock可以被其它线程继续获得R锁,而不必等待该线程释放R锁。但是,如果此时有其它线程想要获得W锁,它必须等到所有持有共享读取锁的线程释放掉各自的R锁。

如果一个锁被一个线程获得W锁,那么其它线程,无论是想要获取R锁还是W锁,都必须等待该线程释放W锁。

这样,多个线程就可以同时读取共享资源。而具有危险性的写入操作则得到了互斥锁的保护。

 

我们需要同步并发系统,这为程序员编程带来了难度。但是多线程系统可以很好的解决许多IO瓶颈的问题。比如我们监听网络端口。如果我们只有一个线程,那么我们必须监听,接收请求,处理,回复,再监听。如果我们使用多线程系统,则可以让多个线程监听。当我们的某个线程进行处理的时候,我们还可以有其他的线程继续监听,这样,就大大提高了系统的利用率。在数据越来越大,服务器读写操作越来越多的今天,这具有相当的意义。多线程还可以更有效地利用多CPU的环境。

(就像做饭一样,不断切换去处理不同的菜。)

 

本文中的程序采用伪C的写法。不同的语言有不同的函数名(比如mutex_lock)。这里关注的是逻辑上的概念,而不是具体的实现和语言规范。

 

总结

multiple threads, multiple stacks

race condition

mutex, condition variable, RW lock

12、Linux从程序到进程

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

计算机如何执行进程呢?这是计算机运行的核心问题。即使已经编写好程序,但程序是死的。只有活的进程才能产出。我们已经从Linux进程基础中了解了进程。现在我们看一下从程序到进程的漫漫征程。

 

一段程序

下面是一个简单的C程序,假设该程序已经编译好,生成可执行文件vamei.exe。

复制代码
#include <stdio.h>

int glob=0;                                             /*global variable*/

void main(void) {
  int main1=5;                                          /*local variable of main()*/
  int main2;                                            /*local variable of main()*/
  main2 = inner(main1);                                 /* call inner() function */
  printf("From Main: glob: %d \n", glob);
  printf("From Main: main2: %d \n", main2);
}

int inner(int inner1) {                                 /*inner1 is an argument, also local to inner()*/
  int inner2=10;                                        /*local variable of inner()*/
  printf("From inner: glob: %d \n", glob);
  return(inner1+inner2);
}
复制代码

(选取哪一个语言或者具体的语法并不是关键,大部分语言都可以写出类似上面的程序。在看Python教程的读者也可以利用Python的函数结构和print写一个类似的python程序。当然,还可以是C++,Java,Objective-C等等。选用C语言的原因是:它是为UNIX而生的语言。)

 

main()函数中调用了inner()函数。inner()中调用一次printf()以输出。最后,在main()中进行了两次printf()。

注意变量的作用范围。简单地说,变量可以分为全局变量和局部变量。在所有函数之外声明的变量为全局变量,比如glob,在任何时候都可以使用。在函数内定义的变量为局部变量,只能在该函数的作用域(range)内使用,比如说我们在inner()工作的时候不能使用main()函数中声明的main1变量,而在main()中我们无法使用inner()函数中声明的inner2变量。

 

不用太过在意这个程序的具体功能。要点是这个程序的运行过程。下图为该程序的运行过程,以及各个变量的作用范围:

运行流程

进程空间

为了进一步了解上面程序的运行,我们还需要知道,进程如何使用内存。当程序文件运行为进程时,进程在内存中获得空间。这个空间是进程自己的小屋子。

每个进程空间按照如下方式分为不同区域:

内存空间

Text区域用来储存指令(instruction),说明每一步的操作。Global Data用于存放全局变量,栈(Stack)用于存放局部变量,堆(heap)用于存放动态变量 (dynamic variable. 程序利用malloc系统调用,直接从内存中为dynamic variable开辟空间)。Text和Global data在进程一开始的时候就确定了,并在整个进程中保持固定大小。

 

栈(Stack)以帧(stack frame)为单位。当程序调用函数的时候,比如main()函数中调用inner()函数,stack会向下增长一帧。帧中存储该函数的参数和局部变量,以及该函数的返回地址(return address)。此时,计算机将控制权从main()转移到inner(),inner()函数处于激活(active)状态。位于栈最下方的帧,和全局变量一起,构成了当前的环境(context)。激活函数可以从环境中调用需要的变量。典型的编程语言都只允许你使用位于stack最下方的帧 ,而不允许你调用其它的帧 (这也符合stack结构“先进后出”的特征。但也有一些语言允许你调用栈的其它部分,相当于允许你在运行inner()函数的时候调用main()中声明的局部变量,比如Pascal)。当函数又进一步调用另一个函数的时候,一个新的帧会继续增加到栈的下方,控制权转移到新的函数中。当激活函数返回的时候,会从栈中弹出(pop,读取并从栈中删除)该帧,并根据帧中记录的返回地址,将控制权交给返回地址所指向的指令(比如从inner()函数中返回,继续执行main()中赋值给main2的操作)。

下图是栈在运行过程中的变化。箭头表示栈的增长方向。每个方块代表一帧。开始的时候我们有一个为main()服务的帧,随着调用inner(),我们为inner()增加一个帧。在inner()返回时,我们再次只有main()的帧,直到最后main()返回,其返回地址为空,所以进程结束。

stack变化

在进程运行的过程中,通过调用和返回函数,控制权不断在函数间转移。进程可以在调用函数的时候,原函数的帧中保存有在我们离开时的状态,并为新的函数开辟所需的帧空间。在调用函数返回时,该函数的帧所占据的空间随着帧的弹出而清空。进程再次回到原函数的帧中保存的状态,并根据返回地址所指向的指令继续执行。上面过程不断继续,栈不断增长或减小,直到main()返回的时候,栈完全清空,进程结束。

 

当程序中使用malloc的时候,堆(heap)会向上增长,其增长的部分就成为malloc从内存中分配的空间。malloc开辟的空间会一直存在,直到我们用free系统调用来释放,或者进程结束。一个经典的错误是内存泄漏(memory leakage), 就是指我们没有释放不再使用的堆空间,导致堆不断增长,而内存可用空间不断减少。

栈和堆的大小则会随着进程的运行增大或者变小。当栈和堆增长到两者相遇时候,也就是内存空间图中的蓝色区域(unused area)完全消失的时候,再无可用内存。进程会出现栈溢出(stack overflow)的错误,导致进程终止。在现代计算机中,内核一般会为进程分配足够多的蓝色区域,如果清理及时,栈溢出很容易避免。即便如此,内存负荷过大,依然可能出现栈溢出的情况。我们就需要增加物理内存了。

Stack overflow可以说是最出名的计算机错误了,所以才有IT网站(stackoverflow.com)以此为名。

 

在高级语言中,这些内存管理的细节对于用户来说不透明。在编程的时候,我们只需要记住上一节中的变量作用域就可以了。但在想要写出复杂的程序或者debug的时候,我们就需要相关的知识了。

 

进程附加信息

除了上面的信息之外,每个进程还要包括一些进程附加信息,包括PID,PPID,PGID(参考Linux进程基础以及Linux进程关系)等,用来说明进程的身份、进程关系以及其它统计信息。这些信息并不保存在进程的内存空间中。内核会为每个进程在内核自己的空间中分配一个变量(task_struct结构体)以保存上述信息。内核可以通过查看自己空间中的各个进程的附加信息就能知道进程的概况,而不用进入到进程自身的空间 (就好像我们可以通过门牌就可以知道房间的主人是谁一样,而不用打开房门)。每个进程的附加信息中有位置专门用于保存接收到的信号(正如我们在Linux信号基础中所说的“信箱”)。

 

fork & exec

现在,我们可以更加深入地了解fork和exec(参考Linux进程基础)的机制了。当一个程序调用fork的时候,实际上就是将上面的内存空间,包括text, global data, heap和stack,又复制出来一个,构成一个新的进程,并在内核中为改进程创建新的附加信息 (比如新的PID,而PPID为原进程的PID)。此后,两个进程分别地继续运行下去。新的进程和原有进程有相同的运行状态(相同的变量值,相同的instructions…)。我们只能通过进程的附加信息来区分两者。

程序调用exec的时候,进程清空自身内存空间的text, global data, heap和stack,并根据新的程序文件重建text, global data, heap和stack (此时heap和stack大小都为0),并开始运行。

(现代操作系统为了更有效率,改进了管理fork和exec的具体机制,但从逻辑上来说并没有差别。具体机制请参看Linux内核相关书籍)

 

这一篇写了整合了许多东西,所以有些长。这篇文章主要是概念性的,许多细节会根据语言和平台乃至于编译器的不同而有所变化,但大体上,以上的概念适用于所有的计算机进程(无论是Windows还是UNIX)。更加深入的内容,包括线程(thread)、进程间通信(IPC)等,都依赖于这里介绍的内容。

 

总结

函数,变量的作用范围,global/local/dynamic variables

global data, text,

stack, stack frame, return address, stack overflow

heap, malloc, free, memory leakage

进程附加信息, task_struct

fork & exec

11、Linux用户与“最小权限”原则

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

Linux的用户在登录(login)之后,就带有一个用户身份(user ID, UID)和一个组身份(group ID, GID)。在Linux文件管理背景知识中,我们又看到,每个文件又有九位的权限说明,用来指明该文件允许哪些用户执行哪些操作(读、写或者执行)。

(参考Linux文件管理背景知识)

 

一般来说,Linux的用户信息保存在/etc/passwd中,组信息保存在/etc/group中,文件的每一行代表一个用户/组。早期的Linux将密码以名码的形式保存在/etc/passwd中,而现在则多以暗码(也就是加密之后的形式)的形式保存在/etc/shadow中。将密码存储在/etc/shadow中提高了密码的安全性,因为/etc/passwd允许所有人查看,而/etc/shadow只允许root用户查看。

 

进程权限

但是,在Linux中,用户的指令是在进程的范围内进行的。当我们向对某个文件进行操作的时候,我们需要在进程中运行一个程序,在进程中对文件打开,并进行读、写或者执行的操作。因此,我们需要将用户的权限传递给进程,以便进程真正去执行操作。例如我们有一个文件a.txt, 文件中为一个字符串:

Hello world!

我以用户Vamei的身份登录,并在shell中运行如下命令:

$cat a.txt

整个运行过程以及文件读取如下:

 

我们可以看到,整个过程中我们会有两个进程,一个是shell本身(2256),一个是shell复制自身,再运行/bin/cat (9913)。图中的fork, exec, PID可参看Linux进程基础。第二个进程总共对文件系统进行了两次操作,一次是执行(x)文件/bin/cat,另外一次是读取(r)文件a.txt。使用$ls -l 查看这两个文件的权限:

$ls -l /bin/cat

-rwxr-xr-x 1 root root 46764 Apr  1  2012 /bin/cat
$ls -l a.txt

-rw-rw-r– 1 Vamei Vamei 14 Oct  7 09:14 a.txt

从上面可以看到(参考Linux文件管理背景知识),/bin/cat让所有用户都享有执行的权利,而Vamei作为a.txt的拥有者,对a.txt享有读取的权利。

 

让我们进入更多的细节 (The devil is in the details)。在进行这两次操作的时候,尽管用户Vamei拥有相应的权限,但我们发现,真正做工作的是进程9913。我们要让这个进程得到相应的权限。实际上,每个进程会维护有如下6个ID:

真实身份: real UID,       real GID

有效身份: effective UID,  effective GID

存储身份:saved UID,      saved GID

其中,真实身份是我们登录使用的身份,有效身份是当该进程真正去操作文件时所检查的身份,存储身份较为特殊,我们等一下再深入。当进程fork的时候,真实身份和有效身份都会复制给子进程。大部分情况下,真实身份和有效身份都相同。当Linux完成开机启动之后,init进程会执行一个login的子进程。我们将用户名和密码传递给login子进程。login在查询了/etc/passwd和/etc/shadow,并确定了其合法性之后,运行(利用exec)一个shell进程,shell进程真实身份被设置成为该用户的身份。由于此后fork此shell进程的子进程都会继承真实身份,所以该真实身份会持续下去,直到我们登出并以其他身份再次登录(当我们使用su成为root的时候,实际上就是以root身份再次登录,此后真实身份成为root)。

 

最小权限原则

每个进程为什么不简单地只维护真实身份,却选择费尽麻烦地去维护有效身份和存储身份呢?这牵涉到Linux的“最小特权”(least priviledge)的原则。Linux通常希望进程只拥有足够完成其工作的特权,而不希望赋予更多的特权给它。从设计上来说,最简单的是赋予每个进程以super user的特权,这样进程就可以想做什么做什么。然而,这对于系统来说是一个巨大的安全漏洞,特别是在多用户环境下,如果每个用户都享有无限制的特权,就很容易破坏其他用户的文件或者系统本身。“最小特权”就是收缩进程所享有的特权,以防进程滥用特权。

然而,进程的不同阶段可能需要不同的特权。比如一个进程最开始的有效身份是真实身份,但运行到中间的时候,需要以其他的用户身份读入某些配置文件,然后再进行其他的操作。为了防止其他的用户身份被滥用,我们需要在操作之前,让进程的有效身份变更回来成为真实身份。这样,进程需要在两个身份之间变化。
存储身份就是真实身份之外的另一个身份。当我们将一个程序文件执行成为进程的时候,该程序文件的拥有者(owner)和拥有组(owner group)可以被,存储成为进程的存储身份。在随后进程的运行过程中,进程就将可以选择将真实身份或者存储身份复制到有效身份,以拥有真实身份或者存储身份的权限。并不是所有的程序文件在执行的过程都设置存储身份的。需要这么做的程序文件会在其九位(bit)权限的执行位的x改为s。这时,这一位(bit)叫做set UID bit或者set GID bit。

$ls -l /usr/bin/uuidd
-rwsr-sr-x 1 libuuid libuuid 17976 Mar 30  2012 /usr/sbin/uuidd

当我以root(UID), root(GID)的真实身份运行这个程序的时候,由于拥有者(owner)有s位的设定,所以saved UID被设置成为libuuid,saved GID被设置成为libuuid。这样,uuidd的进程就可以在两个身份之间切换。

 

我们通常使用chmod来修改set-UID bit和set-GID bit:

$chmod 4700 file

我们看到,这里的chmod后面不再只是三位的数字。最前面一位用于处理set-UID bit/set-GID bit,它可以被设置成为4/2/1以及或者上面数字的和。4表示为set UID bit, 2表示为set GID bit,1表示为sticky bit (暂时不介绍)。必须要先有x位的基础上,才能设置s位。

 

作为一个Linux用户来说,我们并不需要特别关心上面的机制。但是,当我们去编写一个Linux应用程序的时候,就要注意在程序中实现以上切换(有必要的前提下),以便让我们的程序符合”最小权限”的原则,不给系统留下可能的安全隐患。给你的程序过度的权限的话,就像是吃下去下面的汉堡:

 

容易让人吃伤的汉堡: 过度的“权限”

 

总结

real/effective/saved UID/GID

saved UID/GID bit

“最小权限”原则

10、Linux进程关系

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

Linux的进程相互之间有一定的关系。比如说,在Linux进程基础中,我们看到,每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构。我们在这里讲解进程组和会话,以便以更加丰富的方式了管理进程。

 

进程组 (process group)

每个进程都会属于一个进程组(process group),每个进程组中可以包含多个进程。进程组会有一个进程组领导进程 (process group leader),领导进程的PID (PID见Linux进程基础)成为进程组的ID (process group ID, PGID),以识别进程组。

$ps -o pid,pgid,ppid,comm | cat

  PID  PGID  PPID COMMAND
17763 17763 17751 bash
18534 18534 17763 ps
18535 18534 17763 cat

PID为进程自身的ID,PGID为进程所在的进程组的ID, PPID为进程的父进程ID。从上面的结果,我们可以推测出如下关系:

图中箭头表示父进程通过fork和exec机制产生子进程。ps和cat都是bash的子进程。进程组的领导进程的PID成为进程组ID。领导进程可以先终结。此时进程组依然存在,并持有相同的PGID,直到进程组中最后一个进程终结。

 

我们将一些进程归为进程组的一个重要原因是我们可以将信号发送给一个进程组。进程组中的所有进程都会收到该信号。我们会在下一部分深入讨论这一点。

 

会话 (session)

更进一步,在shell支持工作控制(job control)的前提下,多个进程组还可以构成一个会话 (session)。bash(Bourne-Again shell)支持工作控制,而sh(Bourne shell)并不支持。

会话是由其中的进程建立的,该进程叫做会话的领导进程(session leader)。会话领导进程的PID成为识别会话的SID(session ID)。会话中的每个进程组称为一个工作(job)。会话可以有一个进程组成为会话的前台工作(foreground),而其他的进程组是后台工作(background)。每个会话可以连接一个控制终端(control terminal)。当控制终端有输入输出时,都传递给该会话的前台进程组。由终端产生的信号,比如CTRL+Z, CTRL+\,会传递到前台进程组。

会话的意义在于将多个工作囊括在一个终端,并取其中的一个工作作为前台,来直接接收该终端的输入输出以及终端信号。 其他工作在后台运行。

 

一个命令可以通过在末尾加上&方式让它在后台运行:

$ping localhost > log &

此时终端显示:

[1] 10141

括号中的1表示工作号,而10141为PGID

我们通过如下方式查询更加详细的信息:

$ps -o pid,pgid,ppid,sid,tty,comm

(tty表示控制终端)

 

信号可以通过kill

$kill -SIGTERM -10141

或者

$kill -SIGTERM %1

的方式来发送给工作组。上面的两个命令,一个是发送给PGID(通过在PGID前面加-来表示是一个PGID而不是PID),一个是发送给工作1(%1),两者等价。

 

一个工作可以通过$fg从后台工作变为前台工作:

$cat > log &

$fg %1

当我们运行第一个命令后,由于工作在后台,我们无法对命令进行输入,直到我们将工作带入前台,才能向cat命令输入。在输入完成后,按下CTRL+D来通知shell输入结束。

 

进程组(工作)的概念较为简单易懂。而会话主要是针对一个终端建立的。当我们打开多个终端窗口时,实际上就创建了多个终端会话。每个会话都会有自己的前台工作和后台工作。这样,我们就为进程增加了管理和运行的层次。在没有图形化界面的时代,会话允许用户通过shell进行多层次的进程发起和管理。比如说,我可以通过shell发起多个后台工作,而此时标准输入输出并不被占据,我依然可以继续其它的工作。如今,图形化界面可以帮助我们解决这一需求,但工作组和会话机制依然在Linux的许多地方应用。

 

总结

process group, pgid

session, sid, job, forground, background

fg, kill -pid, &, %

9、Linux信号基础

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

Linux进程基础一文中已经提到,Linux以进程为单位来执行程序。我们可以将计算机看作一个大楼,内核(kernel)是大楼的管理员,进程是大楼的房客。每个进程拥有一个独立的房间(属于进程的内存空间),而每个房间都是不允许该进程之外的人进入。这样,每个进程都只专注于自己干的事情,而不考虑其他进程,同时也不让别的进程看到自己的房间内部。这对于每个进程来说是一种保护机制。(想像一下几百个进程总是要干涉对方,那会有多么混乱,或者几百个进程相互偷窥……)

 

然而,在一些情况,我们需要打破封闭的房间,以便和进程交流信息。比如说,内核发现有一个进程在砸墙(硬件错误),需要让进程意识到这样继续下去会毁了整个大楼。再比如说,我们想让多个进程之间合作。这样,我们就需要一定的通信方式。信号(signal)就是一种向进程传递信息的方式。我们可以将信号想象成大楼的管理员往房间的信箱里塞小纸条。随后进程取出小纸条,会根据纸条上的内容来采取一定的行动,比如灯坏了,提醒进程使用手电。(当然,也可以完全无视这张纸条,然而在失火这样紧急的状况下,无视信号不是个好的选择)。相对于其他的进程间通信方式(interprocess communication, 比如说pipe, shared memory)来说,信号所能传递的信息比较粗糙,只是一个整数。但正是由于传递的信息量少,信号也便于管理和使用。信号因此被经常地用于系统管理相关的任务,比如通知进程终结、中止或者恢复等等。

 

给我一个信号

 

信号是由内核(kernel)管理的。信号的产生方式多种多样,它可以是内核自身产生的,比如出现硬件错误(比如出现分母为0的除法运算,或者出现segmentation fault),内核需要通知某一进程;也可以是其它进程产生的,发送给内核,再由内核传递给目标进程。内核中针对每一个进程都有一个表存储相关信息(房间的信箱)。当内核需要将信号传递给某个进程时,就在该进程相对应的表中的适当位置写入信号(塞入纸条),这样,就生成(generate)了信号。当该进程执行系统调用时,在系统调用完成后退出内核时,都会顺便查看信箱里的信息。如果有信号,进程会执行对应该信号的操作(signal action, 也叫做信号处理signal disposition),此时叫做执行(deliver)信号。从信号的生成到信号的传递的时间,信号处于等待(pending)状态(纸条还没有被查看)。我们同样可以设计程序,让其生成的进程阻塞(block)某些信号,也就是让这些信号始终处于等待的状态,直到进程取消阻塞(unblock)或者无视信号。

 

常见信号

信号所传递的每一个整数都被赋予了特殊的意义,并有一个信号名对应该整数。常见的信号有SIGINT, SIGQUIT, SIGCONT, SIGTSTP, SIGALRM等。这些都是信号的名字。你可以通过

$man 7 signal

来查阅更多的信号。

 

上面几个信号中,

SIGINT   当键盘按下CTRL+C从shell中发出信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作是中断 (INTERRUPT) 该进程。

SIGQUIT  当键盘按下CTRL+\从shell中发出信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作是退出 (QUIT) 该进程。

SIGTSTP  当键盘按下CTRL+Z从shell中发出信号,信号被传递给shell中前台运行的进程,对应该信号的默认操作是暂停 (STOP) 该进程。

SIGCONT  用于通知暂停的进程继续。

SIGALRM  起到定时器的作用,通常是程序在一定的时间之后才生成该信号。

 

在shell中使用信号

下面我们实际应用一下信号。我们在shell中运行ping:

$ping localhost

此时我们可以通过CTRL+Z来将SIGTSTP传递给该进程。shell中显示:

[1]+  Stopped                 ping localhost

我们使用$ps来查询ping进程的PID (PID是ping进程的房间号), 在我的机器中为27397

我们可以在shell中通过$kill命令来向某个进程发出信号:

$kill -SIGCONT  27397

来传递SIGCONT信号给ping进程。

 

信号处理 (signal disposition)

在上面的例子中,所有的信号都采取了对应信号的默认操作。但这并不绝对。当进程决定执行信号的时候,有下面几种可能:

1) 无视(ignore)信号,信号被清除,进程本身不采取任何特殊的操作

2) 默认(default)操作。每个信号对应有一定的默认操作。比如上面SIGCONT用于继续进程。

3) 自定义操作。也叫做获取 (catch) 信号。执行进程中预设的对应于该信号的操作。

进程会采取哪种操作,要根据该进程的程序设计。特别是获取信号的情况,程序往往会设置一些比较长而复杂的操作(通常将这些操作放到一个函数中)。

 

信号常常被用于系统管理,所以它的内容相当庞杂。深入了解信号,需要一定的Linux环境编程知识。

 

总结

信号机制; generate, deliver, pending, blocking

signal action/dispositon; ignore, default action, catch signal

$kill

8、Linux进程基础

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

计算机实际上可以做的事情实质上非常简单,比如计算两个数的和,再比如在内存中寻找到某个地址等等。这些最基础的计算机动作被称为指令(instruction)。所谓的程序(program),就是这样一系列指令的所构成的集合。通过程序,我们可以让计算机完成复杂的操作。程序大多数时候被存储为可执行的文件。这样一个可执行文件就像是一个菜谱,计算机可以按照菜谱作出可口的饭菜。

那么,程序和进程(process)的区别又是什么呢?

进程是程序的一个具体实现。只有食谱没什么用,我们总要按照食谱的指点真正一步步实行,才能做出菜肴。进程是执行程序的过程,类似于按照食谱,真正去做菜的过程。同一个程序可以执行多次,每次都可以在内存中开辟独立的空间来装载,从而产生多个进程。不同的进程还可以拥有各自独立的IO接口。

操作系统的一个重要功能就是为进程提供方便,比如说为进程分配内存空间,管理进程的相关信息等等,就好像是为我们准备好了一个精美的厨房。

 

看一眼进程

首先,我们可以使用$ps命令来查询正在运行的进程,比如$ps -eo pid,comm,cmd,下图为执行结果:

(-e表示列出全部进程,-o pid,comm,cmd表示我们需要PID,COMMAND,CMD信息)

 

每一行代表了一个进程。每一行又分为三列。第一列PID(process IDentity)是一个整数,每一个进程都有一个唯一的PID来代表自己的身份,进程也可以根据PID来识别其他的进程。第二列COMMAND是这个进程的简称。第三列CMD是进程所对应的程序以及运行时所带的参数。

(第三列有一些由中括号[]括起来的。它们是kernel的一部分功能,被打扮成进程的样子以方便操作系统管理。我们不必考虑它们。)

 

我们看第一行,PID为1,名字为init。这个进程是执行/bin/init这一文件(程序)生成的。当Linux启动的时候,init是系统创建的第一个进程,这一进程会一直存在,直到我们关闭计算机。这一进程有特殊的重要性,我们会不断提到它。

 

如何创建一个进程

实际上,当计算机开机的时候,内核(kernel)只建立了一个init进程。Linux kernel并不提供直接建立新进程的系统调用。剩下的所有进程都是init进程通过fork机制建立的。新的进程要通过老的进程复制自身得到,这就是fork。fork是一个系统调用。进程存活于内存中。每个进程都在内存中分配有属于自己的一片空间 (address space)。当进程fork的时候,Linux在内存中开辟出一片新的内存空间给新的进程,并将老的进程空间中的内容复制到新的空间中,此后两个进程同时运行。

老进程成为新进程的父进程(parent process),而相应的,新进程就是老的进程的子进程(child process)。一个进程除了有一个PID之外,还会有一个PPID(parent PID)来存储的父进程PID。如果我们循着PPID不断向上追溯的话,总会发现其源头是init进程。所以说,所有的进程也构成一个以init为根的树状结构。

如下,我们查询当前shell下的进程:

 

root@vamei:~# ps -o pid,ppid,cmd
  PID  PPID CMD
16935  3101 sudo -i
16939 16935 -bash
23774 16939 ps -o pid,ppid,cmd

我们可以看到,第二个进程bash是第一个进程sudo的子进程,而第三个进程ps是第二个进程的子进程。

 

还可以用$pstree命令来显示整个进程树:

复制代码
init─┬─NetworkManager─┬─dhclient
     │                └─2*[{NetworkManager}]
     ├─accounts-daemon───{accounts-daemon}
     ├─acpid
     ├─apache2─┬─apache2
     │         └─2*[apache2───26*[{apache2}]]
     ├─at-spi-bus-laun───2*[{at-spi-bus-laun}]
     ├─atd
     ├─avahi-daemon───avahi-daemon
     ├─bluetoothd
     ├─colord───2*[{colord}]
     ├─console-kit-dae───64*[{console-kit-dae}]
     ├─cron
     ├─cupsd───2*[dbus]
     ├─2*[dbus-daemon]
     ├─dbus-launch
     ├─dconf-service───2*[{dconf-service}]
     ├─dropbox───15*[{dropbox}]
     ├─firefox───27*[{firefox}]
     ├─gconfd-2
     ├─geoclue-master
     ├─6*[getty]
     ├─gnome-keyring-d───7*[{gnome-keyring-d}]
     ├─gnome-terminal─┬─bash
     │                ├─bash───pstree
     │                ├─gnome-pty-helpe
     │                ├─sh───R───{R}
     │                └─3*[{gnome-terminal}]
复制代码

 

fork通常作为一个函数被调用。这个函数会有两次返回,将子进程的PID返回给父进程,0返回给子进程。实际上,子进程总可以查询自己的PPID来知道自己的父进程是谁,这样,一对父进程和子进程就可以随时查询对方。

通常在调用fork函数之后,程序会设计一个if选择结构。当PID等于0时,说明该进程为子进程,那么让它执行某些指令,比如说使用exec库函数(library function)读取另一个程序文件,并在当前的进程空间执行 (这实际上是我们使用fork的一大目的: 为某一程序创建进程);而当PID为一个正整数时,说明为父进程,则执行另外一些指令。由此,就可以在子进程建立之后,让它执行与父进程不同的功能。

 

子进程的终结(termination)

当子进程终结时,它会通知父进程,并清空自己所占据的内存,并在kernel里留下自己的退出信息(exit code,如果顺利运行,为0;如果有错误或异常状况,为>0的整数)。在这个信息里,会解释该进程为什么退出。父进程在得知子进程终结时,有责任对该子进程使用wait系统调用。这个wait函数能从kernel中取出子进程的退出信息,并清空该信息在kernel中所占据的空间。但是,如果父进程早于子进程终结,子进程就会成为一个孤儿(orphand)进程。孤儿进程会被过继给init进程,init进程也就成了该进程的父进程。init进程负责该子进程终结时调用wait函数。

当然,一个糟糕的程序也完全可能造成子进程的退出信息滞留在kernel中的状况(父进程不对子进程调用wait函数),这样的情况下,子进程成为僵尸(zombie)进程。当大量僵尸进程积累时,内存空间会被挤占。

 

进程与线程(thread)

尽管在UNIX中,进程与线程是有联系但不同的两个东西,但在Linux中,线程只是一种特殊的进程。多个线程之间可以共享内存空间和IO接口。所以,进程是Linux程序的唯一的实现方式。

 

总结

程序,进程,PID,内存空间

子进程,父进程,PPID,fork, wait

7、Linux文本流

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

 

文本流

文件用于数据的存储,相当于一个个存储数据的房子。我们之前说,所谓的数据是0或者1的序列,但严格来说,Linux以字节(byte)来作为数据的单位,也就是说这个序列每八位(bit)为一个单位(八位二进制对应的十进制范围为0到255)。使用ASCII编码,可以将这样一个字节转换成为字符。所以,在Linux中,我们所说的数据,完全可以用字符表达出来,也就是说文本(text)的形式。

实际上,如果以bit为单位处理字符的话,机器会更容易读懂和传输,效率会更高。但为什么Linux依然以字节为单位进行处理呢?原因在于,相对于以bit为单位处理数据,以byte为单位可以更容易将数据转化为字符。相对于枯燥的0和1,字符更容易被人读懂 (human readable)。然而,并不是所有的数据都是设计来让人读懂的,比如可执行文件包含的各种字符对于人来说并没有什么意义 (因为可执行文件是为了让机器读懂的)。但Linux依然以字节为单位处理所有文件,这是为了让所有文件能够共用一套接口 (virtual file system),从而减少Linux设计的复杂度。

(“everything is a file”是通常所流传的UNIX设计的哲学之一,但Linus对此作出纠正,改为“everything is a stream of bytes”。)

然而,数据不是在找到了自己的房子(file)之后就永远的定居下来。它往往要被读入到内存 (就像是到办公室上班),或者被传送到外部设备(好像去酒店休假),或者搬到别的房子中。在这样的搬迁过程中,数据像是一个个排着队走路的人流,我们叫它文本流(text stream,或者byte stream)。然而,计算机不同设备之间的连接方法差异很大,从内存到文件的连接像是爬山,从内存到外设像是游过一条河。为此,Linux还定义了流 (stream),以此作为修建连接各处的公路的标准。Stream的好处在于,无论你是从内存到外设,还是从内存到文件,所有的公路都是相同的 (至于公路下面是石头还是土地,都可以不用操心)。

我们再回味一下“everything is a stream of bytes”这句话。信息包含在文本流中,不断在计算机的各个组件之间流动,不断地接受计算机的加工,最终成为用户所需要的某种服务。

(说句题外话,如果看过骇客帝国的话,一定会对文本流印象深刻。)

 

标准输入,标准输出,标准错误与重新定向

当Linux执行一个程序的时候,会自动打开三个流,标准输入(standard input),标准输出(standard output),标准错误(standard error)。比如说你打开命令行的时候,默认情况下,命令行的标准输入连接到键盘,标准输出和标准错误都连接到屏幕。对于一个程序来说,尽管它总会打开这三个流,但它会根据需要使用,并不是一定要使用。

想象一下敲击一个

$ls

键盘敲击的文本流(“ls\n”,\n是回车时输入的字符,表示换行)命令行 (命令行实际上也是一个程序)。命令行随后调用/bin/ls得到结果(“a.txt”),最后这个输出的文本流(“a.txt”)流到屏幕,显示出来,比如说:

a.txt

假设说我们不想让文本流流到屏幕,而是流到另一个文件,我们可以采用重新定向(redirect)的机制。

$ls > a.txt

重新定向标准输出。这里的>就是提醒命令行,让它知道我现在想变换文本流的方向了,我们不让标准输出输出到屏幕,而是要到a.txt这个文件 (好像火车轨道换轨)。此时,计算机会新建一个a.txt的文件,并将命令行的标准输出指向这个文件。

有另一个符号:

$ls >> a.txt

这里>>的作用也是重新定向标准输出。如果a.txt已经存在的话,ls产生的文本流会附加在a.txt的结尾,而不会像>那样每次都新建a.txt。

 

我们下面介绍命令echo:

$echo IamVamei

echo的作用是将文本流导向标准输出。在这里,echo的作用就是将IamVamei输出到屏幕上。如果是

$echo IamVamei > a.txt

a.txt中就会有IamVamei这个文本。

 

我们也可以用<符号来改变标准输入。比如cat命令,它可以从标准输入读入文本流,并输出到标准输出:

$cat < a.txt

我们将cat标准输入指向a.txt,文本会从文件流到cat,然后再输出到屏幕上。当然,我们还可以同时重新定向标准输出:

$cat < a.txt > b.txt

这样,a.txt的内容就复制到了b.txt中。

 

我们还可以使用>&来同时重新定向标准输出和标准错误。假设我们并没有一个目录void。那么

$cd void > a.txt

会在屏幕上返回错误信息。因为此时标准错误依然指向屏幕。当我们使用:

$cd void >& a.txt

错误信息被导向a.txt。

 

如果只想重新定向标准错误,可以使用2>:

$cd void 2> a.txt > b.txt

标准错误对应的总是2号,所以有以上写法。标准错误输出到a.txt,标准输出输出到b.txt。

 

管道 (pipe)

理解了以上的内容之后,管道的概念就易如反掌。管道可以将一个命令的输出导向另一个命令的输入,从而让两个(或者更多命令)像流水线一样连续工作,不断地处理文本流。在命令行中,我们用|表示管道:

$cat < a.txt | wc

wc命令代表word count,用于统计文本中的行、词以及字符的总数。a.txt中的文本先流到cat,然后从cat的标准输出流到wc的标准输入,从而让wc知道自己要处理的是a.txt这个字符串。

 

Linux的各个命令实际上高度专业化,并尽量相互独立。每一个都只专注于一个小的功能。但通过pipe,我们可以将这些功能合在一起,实现一些复杂的目的。

 

总结

文本流,标准输入,标准输出,标准错误

cat, echo, wc

>, >>, <, |