APUE读书笔记-01UNIX系统概述
1、简介
所有的操作系统都为运行于其上的应用程序提供服务。典型的服务包括执行一个新的程序,打开一个文件,读取文件,分配内存空间,获取当前日期时间,等等。本文这里集中描述各种版本的UNIX操作系统所提供的服务。
本身知识就不是线性方式获取的,所以以严格的近似线性步进方式来说明UNIX、却不引用后面将要使用的一些术语,几乎是不可能的(这也是让人困扰的)。本章从程序设计人员的角度先快速浏览UNIX,并对书中引用的一些术语概念进行简要的说明并给出实例。在以后各章中,将作更详细的说明。本章也对想要熟悉UNIX环境的程序设计人员简要介绍了UNIX提供的各种服务。
译者注
原文参考
2、UNIX体系结构
严格来说,操作系统就是一个可以控制计算机硬件资源,以及为运行于其中的程序提供运行环境的软件。大体来说,我们可以将这个软件称作内核,因为它是处于整套环境核心的那个部分。下图就展示了这个体系结构。
UNIX操作系统体系结构
+------------------------+
| applications a|
| +-------------------x p|
| |s shell / p|
| |h+---------------/ l|
| |e| system calls | i|
| |l| +-----------+ | c|
| |l| | Kernel | | a|
| | | +-----------+ | t|
| | | system calls | i|
| | /---------------\ o|
| |/library routines \ n|
| x-------------------x s|
+------------------------+
内核的接口层被称作系统调用(从代码角度上看系统调用就好似函数,例如用于文件打开的 open
,读取的 read
,写入的 write
等),通过它们定义 UNIX
系统能够提供功能。
通用的库函数构建在系统调用接口之上(也就是类似 C
库等方便编程的程序 API
库其实最终会调用到系统调用,例如 fopen
函数,可以简单看做对系统调用的封装),应用程序可以构建在库函数之上也可以构建在系统调用之上(也就是,我们编写的应用程序,可以调用库函数,例如 strcpy
;也可以直接调用系统调用函数,例如 open
;来实现特定的功能)。
shell
是一个特殊的应用程序,它提供了运行其他程序的界面。
综上,大体来说,操作系统就是内核,提供了基本的功能;其他的软件使得我们可以用更人性化的方式使用计算机。其他的软件包括系统工具,应用程序,shells,通用函数库,等等。例如:Linux就是GNU下的操作系统的内核。有些人们喜欢将其称作GNU/Linux操作系统,但是一般更多的是简称其为Linux。尽管这不是严格准确地,但是它大致表达了所要表达的意思就够了。
译者注
这里,关于
shell 是一个特殊的应用程序,它提供了运行其他程序的界面。
类似微软的命令行 dos
操作系统, dos
下的命令就是一个个独立的应用程序,而 dos
本身是为用户提供的可以运行应用程序的“程序”;前面的说法不一定准确,因为 dos
本身究竟是操作系统还是操作系统本人还不清楚,但是不会影响理解,其实想说的是:UNIX系统下的 shell
提供了命令行交互方式操作环境,用户在其中敲入的命令就是一个个独立的应用程序,而提供这套交互环境的 shell
本身也是一种特殊的应用程序,而相对于命令行环境下的图形环境其实也是一种应用程序,这里就不说了。
原文参考
3、登陆
当我们登陆UNIX的时候,我们需要输入用户名称和密码。然后系统从一个密码文件中搜索我们输入的登陆名称,检查输入的密码是否匹配。这个密码文件一般是 /etc/passwd
,其中的每一行包含了当前系统的用户名称等信息,各种信息用 :
分割。
一个大致的例子如下:
sar:x:205:105:Stephen Rago:/home/sar:/bin/ksh
这一行分别表示了:
登陆名称(sar),加密密码(用X表示),用户ID(即User ID,205),组ID(即group ID,105),注释(一些描述信息可以没有),用户主目录(/home/sar),登陆shell(也就是登陆时将启动的第一个程序,作为shell,这里是/bin/ksh)。
需要注意的是,由于安全性原因,当前系统一般都将实际加密的密码移到另外一个地方了(例如 /etc/shadow
),密码本身也经过了加密的处理。
译者注
原文参考
4、文件和目录
(1)文件系统
UNIX文件系统对目录和文件使用层次安排(树状),每一级目录或者文件,都属于该层次树中的某一层的节点。通过由树根开始的路径来表示到每个目录或者文件。例如,目录的起点称为根 ( root
),其名字是一个字符 /
,也表示根目录; /usr
则表示在 /
目录下面的一个文件 usr
(或者 usr
也可是一个子目录,UNIX将目录看作特殊的文件)。
(2)路径
如前所叙述,0个或多个以斜线分隔的文件名序列(可以任选地以斜线开头)构成路径名( pathname
),以斜线开头的路径形式的名称为绝对路径名(如前面所举的例子 /usr
) ,否则称为相对路径名。绝对路径,可以让我们通过树根开始,给出文件的完整精确名字,直接引用到该文件;而相对路径,就是相对于当前的路径而言的路径,有些时候,采用相对方式的引用,会比绝对路径更灵活。这里,需要注意的是当前路径用 .
表示,另外当前路径的父目录用 ..
表示。
例如 ./usr
,表示当前路径下的 usr
文件,相对路径中的文件最终都是对应绝对路径中的文件的,不过,具体对应哪个文件,视当前路径而定。
再例如,相对路径名 doc/memo/joe
指的是文件 joe
,它在目录 memo
中,而 memo
又在目录 doc
中, doc
则应是工作目录中的一个目录项。从该路径名可以看出, doc
和 memo
都应当是目录,但是却不清楚 joe
是文件还是目录。路径名 /urs/lib/lint
是一个绝对路径名,它指的是文件 (或目录) lint
,而 lint
在目录 lib
中, lib
则在目录 usr
中, usr
则在根目录中。
(3)工作目录
每个进程都有一个工作目录,有时称为当前工作目录。所有相对路径名都从工作目录开始解释(也就是一工作目录作为当前目录)。进程可以用 chdir
函数更改工作目录。
(4)家目录( Home Directory
,有时又称起始目录,主目录)
表示用户刚刚登陆的时候,设置的当前路径。这个路径其实就是密码文件(例如 /etc/passwd
)中指定的用户主目录。如果用户登陆的时候直接进入 shell
命令行界面,那么可以通过命令 pwd
来查看当前的路径。
参考:
http://book.chinaunix.net/special/ebook/addisonWesley/APUE2/0201433079/ch01lev1sec4.html
译者注
原文参考
5、输入输出
(1)文件描述符号
文字描述符是一个小的非负整数,内核用以标识一个特定进程正在访问(读或者写)的文件。当内核打开一个或创建一个文件时,它就返回一个文件描述符。然后当读、写文件时,就可使用它。
例如在我们代码上,如下方式体现:
int fd = open("/usr/test", ...);
read(fd,...);
这里,表示使用系统调用 open
来打开 /usr/test
路径表示的文件,然后通过返回的文件描述符号 fd
,使用系统调用 read
读取其中的内容。( ...
表示系统调用 open
和 read
其他的参数 ,这里不讨论) 。
(2)标准输入、标准输出和标准错误
一般,每当运行一个新程序时,所有的shell都为其打开三个文件描述符:标准输入、标准输出以及标准错误。三个文件描述符号会被设置为引用(也就是被定向)某种文件(例如文本文件,或者设备文件)。一个直观的理解就是,一般来说,标准输入表示我们的键盘,我们键盘敲入字符就是标准输入中的字符了;标准输出就是我们看到的屏幕,程序的输出通过屏幕显示出来,这个屏幕显示出来的就是标准输出的内容;标准错误和标准输出差不多,就是当程序出错的时候,产生的错误输出也通过屏幕显示出来,这个时候显示的内容属于标准错误的内容。以上说的是默认的情况,大多数shell都提供一种方法,使任何一个或所有这三个描述符都能重新定向到某一个文件。
(a)例如对于输出重定向
$ls > file.list
如果直接执行 ls
命令,将会在当前标准输出上显示当前目录下所有的文件,而这里其标准输出重新定向到名为 file.list
的文件上。这个时候,我们就看不到程序 ls
的输出在屏幕上显示了,而是直接输出到了 file.list
文件里。标准输出的文件描述符号默认为1。
对于标准错误输出
$ls 2> file.list
表示,将 ls
出错时候的标准错误显示到 file.list
里面。标准错误输出的文件描述符号默认为2。
再者:
$ls 2>&1
表示,将 ls
的标准错误重定向到标准输出上。
而:
$ls &>file.list
表示,将 ls
的标准输出和标准错误都定向到文件 file.list
上。
以上,是对输出的重定向,如果 >
后面的文件当前存在,那么就会被清空,否则新建一个文件。如果想要追加内容到已有文件而不是清空,那么就用 >>
代替 >
。
(b)再给出一个标准输入重定向的例子
$xxx <fileinput
表示,将 xxx
的标准输入重新定向到文件中。这里 xxx
表示某个程序,它需要从用户键盘输入读取数据,而这样做之后,就直接将 fileinput
的内容作为用户键盘输入了。标准输入的文件描述符号默认为0。
(3)带缓存和不带缓存的输入输出
这一部分内容后面会提到。大体上,用一个读写的例子来描述,就是:我们使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存,直接从磁盘上被读取;
而如果我们使用标准输入输出方式进行操作(一般是通过库函数),那么操作的方式是通过一个指向某数据结构的指针,来操作文件,这时候读写的内容会先经过缓存,而这个数据结构(例如 FILE
类型)中就包含了文件描述符号,以及一些其他例如缓存等内容。具体我们可以看系统调用函数 read
以及 fread
函数之间的区别。在标准输入输出方式进行操作的时候,使用 stdin
, stdout
, stderr
表示标准输入,标准输出,和标准错误。
译者注
前面, “使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。
原文参考
6、程序和进程
(1)程序
程序( program
)是存放在磁盘文件中的可执行文件,是我们可以看得见得一个文件。通过使用6个 exec
函数中的一个由内核将程序读入存储器,并使其执行。 8.9节将说明这些 exec
函数。例如 shell
中,我们执行 ls
的时候, ls
实际就是程序名称,它就是一个实际的可执行文件,一般存放在 /usr/bin
目录下面。
(2)进程和进程 ID
程序的一次执行实例被称为进程( process
)。当程序被加载到内存中执行的时候,它就变成了进程,也就是说,内存中运行该程序的一个实例,这个实例就是进程。一个程序可以对应多个进程,就好比我们同时执行了好多次 ls
,这个时候,每次执行 ls
的时候,都会将这个程序加载到内存中一次,相应就产生了对应着此次执行该程序的实例。每个UNIX进程都一定有一个唯一的数字标识符,称为进程 ID
( process ID
) ,进程 ID
总是一非负整数,用来表示运行的进程。
综上可知,程序只是一堆可执行代码,“静态”地存放在磁盘上面。而进程就是每次执行程序的时候,被加载到内存中开始运行的“动态”的实例。程序只是一个可执行的文件,也就是我们编写代码之后,编译、链接出来的一个结果,而进程就是这个编译链接出来的结果程序,真正运行的过程。本书后面会对进程的各种控制(创建、终止、开始、停止等)进行详细讲述。
译者注
前面, “使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。
原文参考
7、错误处理
当UNIX函数出错时,往常返回一个负值,同时将整型变量 errno
设置为具有特定信息的一个值。例如, open
函数如成功执行则返回一个非负文件描述符,如出错则返回 -1
,同时设置 errno
。在 open
出错时,有大约15种不同的 errno
值(文件不存在,许可权问题等)。
这里的 errno
就是一个系统的全局变量。文件 <errno.h>
中定义了变量 errno
以及可以赋与它的各种常数,这些常数都以E开头。关于 errno
的具体信息,可以参见系统的用户手册(例如在我的LINUX上面可以运行 man 7 errno
)。POSIX中将它定义为:
extern int errno;
译者注
前面, “使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。
原文参考
8、用户标识
(1)用户ID
前面所述的密码文件中的登录项中的用户 ID
( user ID
)是个数值,用于向系统标识各个不同的用户。通常每个用户有一个唯一的用户 ID
,不同用户对系统具有不同的权限(例如可以访问哪些文件,执行什么程序等)。用户 ID
为0的用户为根( root
)或超级用户( superuser
)。在密码文件中,其登录名为 root
,我们称这种用户的特权为超级用户特权,它具有整个系统无上的管理权限。_
(2)组 ID
密码文件中的登录项也包括用户的组 ID
( group ID
),它也是一个数值。一般来说,在密码文件中有多个记录项具有相同的组 ID
。在UNIX下,组被用于将若干用户集合到某一共同的课题或部门中去。这种机制允许同组的各个成员之间共享资源(例如文件)。4.5节将讲述到,可以设置文件的许可权使组内所有成员都能存取该文件,而组外用户则不能。
组文件将组名映射为数字组 ID
,它通常是 /etc/group
。对于用户而言,使用名字比使用数值方便,所以密码文件包含了登录名和用户 ID
之间的映射关系,而组文件则包含了组名和组 ID
之间的映射关系。另外,除了在密码文件中对一个登录名指定一个组 ID
外,某些UNIX版本还允许一个用户属于另外一些组。
译者注
前面, “使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。
原文参考
9、信号
信号是通知进程发生某种条件的一种技术。例如,若某一进程执行除法操作,其除数为0,则将名为 SIGFPE
的信号发送给该进程。进程有三种选择处理信号:
- 忽略该信号: 有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的单元等,因为这些异常产生的后果不确定,所以不推荐使用这种处理方式。
- 按系统默认方式处理: 对于0除,系统默认方式是终止该进程。其它信号有不同的默认行为。
- 提供一个处理函数,信号发生时则调用该函数: 使用这种方式,我们将能知道什么时候产生了信号,并按所希望的方式处理它。
通过系统的用户手册,我们可以了解具体有哪些信号,以及它们的编号(例如我在Linux上运行 man 7 signal
将显示当前Linux支持的信号信息)。我们可以使用 kill
函数,产生特定的信号,发送给指定的进程(当然我们必需是该进程的所有者);也可以通过键盘的方式,敲入特殊按键,产生信号。例如,我们运行 ls
程序之后,立即键入 [Ctrl]+C
,这样会导致 ls
运行完成之前中止,因为 [Ctrl]+C
的意思就是给当前终端上运行的程序发送终止信号(这里我给出的例子其实不太好, ls
命令执行的比较快,如果目录中文件不多的话,这里需要你键入的速度足够快才能看到效果,否则没等你键入, ls
就执行完了,就看不到效果了_)。
译者注
前面, “使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。
原文参考
10、关于进程时间
为了衡量一个进程的执行时间,unix使用三个值:
-
Clock time
: 运行这个进程所花费的时间,这个时间还依赖于系统上执行的其他进程的数量。一般我们假设系统上没有其他进程运行。 -
User CPU time
: 运行这个进程所花费的用户cpu
时间。也就是执行用户(和内核相对)指令所花费的时间。 -
System CPU time
: 运行这个进程所花费的系统(内核)cpu
时间。也就是进程在核心态下所消耗的时间,例如调用系统调用。
用户 cpu
时间和系统 cpu
时间统称进程的 cpu
时间。进程时间可以使用 clock tick
来衡量,例如一秒钟 50
个 clock tick.
测量一个程序的占用时间可以使用 time
命令。
这里,再次总结一下, Clock time
应该是我们看到的程序运行到结束的总时间,包括实际运行时间,以及由于调度产生的等待时间;而 User+System
为 cpu
时间,也就是除了进程调度和等待等因素后,纯粹的执行时间,也就是占用 cpu
的时间; user
表示在用户空间占用的 cpu
时间, system
表示在内核空间占用的 cpu
时间例如系统调用(一般来说,一次系统调用的时间开销,要比用户空间的一次调用开销大)。
译者注
前面, “使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。
原文参考
11、关于系统调用和库函数
对于系统调用和库函数的关系,前面大致介绍过,这里再次对此进行说明。
(a)系统调用是用户和内核交互的接口,用来请求内核提供相应服务
原来的系统调用接口是用汇编声明,现在用c语言函数方式声明了,其具体的实现方式就不一定是什么了。这样使得用户只需要像用c语言函数一样地,通过包含其声明的头文件,使用系统调用就行了,而不用关心其内部是怎么实现的。
(b)c语言的库函数是用c语言实现的,它一般是需要调用系统调用来实现的
系统调用一般和一些c语言的库函数名称一样,用户可以像使用库函数一样使用系统调用,但是两者不同,可以自己定义库函数来取代原来的库函数,而系统调用是无法取代的。例如 malloc
库函数就是用 sbrk
系统调用实现的。 system
库函数就是用 fork
和 exec
系统调用实现的。
(c)系统调用接口简洁,并且提供了所有的功能,但是不如库函数友好。而库函数可以简单理解成是对系统调用的“封装”
注意,一般 man
手册里面,其第2节的内容是系统调用,第3节的内容是库函数(这一点在前面的预备知识中已经提到过)。
译者注
前面, “使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。
原文参考
总结
本章简单对UNIX系统进行了一下概要说明。我们对一些我们将要遇到的基本的术语进行了解释,后面将会经常提到并且进一步解释它们。
译者注
前面, “使用系统调用 read
, write
等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。