APUE读书笔记-01UNIX系统概述

作者: QuietHeart | 来源:发表于2020-03-20 08:51 被阅读0次

    1、简介

    所有的操作系统都为运行于其上的应用程序提供服务。典型的服务包括执行一个新的程序,打开一个文件,读取文件,分配内存空间,获取当前日期时间,等等。本文这里集中描述各种版本的UNIX操作系统所提供的服务。

    本身知识就不是线性方式获取的,所以以严格的近似线性步进方式来说明UNIX、却不引用后面将要使用的一些术语,几乎是不可能的(这也是让人困扰的)。本章从程序设计人员的角度先快速浏览UNIX,并对书中引用的一些术语概念进行简要的说明并给出实例。在以后各章中,将作更详细的说明。本章也对想要熟悉UNIX环境的程序设计人员简要介绍了UNIX提供的各种服务。

    译者注

    原文参考

    参考: APUE2/ch01lev1sec1.html

    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 本身也是一种特殊的应用程序,而相对于命令行环境下的图形环境其实也是一种应用程序,这里就不说了。

    原文参考

    参考: APUE2/ch01lev1sec2.html

    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 ),密码本身也经过了加密的处理。

    译者注

    原文参考

    参考: APUE2/ch01lev1sec3.html

    4、文件和目录

    (1)文件系统

    UNIX文件系统对目录和文件使用层次安排(树状),每一级目录或者文件,都属于该层次树中的某一层的节点。通过由树根开始的路径来表示到每个目录或者文件。例如,目录的起点称为根 ( root ),其名字是一个字符 / ,也表示根目录; /usr 则表示在 / 目录下面的一个文件 usr (或者 usr 也可是一个子目录,UNIX将目录看作特殊的文件)。

    (2)路径

    如前所叙述,0个或多个以斜线分隔的文件名序列(可以任选地以斜线开头)构成路径名( pathname ),以斜线开头的路径形式的名称为绝对路径名(如前面所举的例子 /usr ) ,否则称为相对路径名。绝对路径,可以让我们通过树根开始,给出文件的完整精确名字,直接引用到该文件;而相对路径,就是相对于当前的路径而言的路径,有些时候,采用相对方式的引用,会比绝对路径更灵活。这里,需要注意的是当前路径用 . 表示,另外当前路径的父目录用 .. 表示。

    例如 ./usr ,表示当前路径下的 usr 文件,相对路径中的文件最终都是对应绝对路径中的文件的,不过,具体对应哪个文件,视当前路径而定。

    再例如,相对路径名 doc/memo/joe 指的是文件 joe ,它在目录 memo 中,而 memo 又在目录 doc 中, doc 则应是工作目录中的一个目录项。从该路径名可以看出, docmemo 都应当是目录,但是却不清楚 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

    译者注

    原文参考

    参考: APUE2/ch01lev1sec4.html

    5、输入输出

    (1)文件描述符号

    文字描述符是一个小的非负整数,内核用以标识一个特定进程正在访问(读或者写)的文件。当内核打开一个或创建一个文件时,它就返回一个文件描述符。然后当读、写文件时,就可使用它。

    例如在我们代码上,如下方式体现:

    int fd = open("/usr/test", ...);
    read(fd,...);
    

    这里,表示使用系统调用 open 来打开 /usr/test 路径表示的文件,然后通过返回的文件描述符号 fd ,使用系统调用 read 读取其中的内容。( ... 表示系统调用 openread 其他的参数 ,这里不讨论) 。

    (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)带缓存和不带缓存的输入输出

    这一部分内容后面会提到。大体上,用一个读写的例子来描述,就是:我们使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存,直接从磁盘上被读取;

    而如果我们使用标准输入输出方式进行操作(一般是通过库函数),那么操作的方式是通过一个指向某数据结构的指针,来操作文件,这时候读写的内容会先经过缓存,而这个数据结构(例如 FILE 类型)中就包含了文件描述符号,以及一些其他例如缓存等内容。具体我们可以看系统调用函数 read 以及 fread 函数之间的区别。在标准输入输出方式进行操作的时候,使用 stdin , stdout , stderr 表示标准输入,标准输出,和标准错误。

    译者注

    前面, “使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。

    原文参考

    参考: APUE2/ch01lev1sec5.html

    6、程序和进程

    (1)程序

    程序( program )是存放在磁盘文件中的可执行文件,是我们可以看得见得一个文件。通过使用6个 exec 函数中的一个由内核将程序读入存储器,并使其执行。 8.9节将说明这些 exec 函数。例如 shell 中,我们执行 ls 的时候, ls 实际就是程序名称,它就是一个实际的可执行文件,一般存放在 /usr/bin 目录下面。

    (2)进程和进程 ID

    程序的一次执行实例被称为进程( process )。当程序被加载到内存中执行的时候,它就变成了进程,也就是说,内存中运行该程序的一个实例,这个实例就是进程。一个程序可以对应多个进程,就好比我们同时执行了好多次 ls ,这个时候,每次执行 ls 的时候,都会将这个程序加载到内存中一次,相应就产生了对应着此次执行该程序的实例。每个UNIX进程都一定有一个唯一的数字标识符,称为进程 IDprocess ID ) ,进程 ID 总是一非负整数,用来表示运行的进程。

    综上可知,程序只是一堆可执行代码,“静态”地存放在磁盘上面。而进程就是每次执行程序的时候,被加载到内存中开始运行的“动态”的实例。程序只是一个可执行的文件,也就是我们编写代码之后,编译、链接出来的一个结果,而进程就是这个编译链接出来的结果程序,真正运行的过程。本书后面会对进程的各种控制(创建、终止、开始、停止等)进行详细讲述。

    译者注

    前面, “使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。

    原文参考

    参考: APUE2/ch01lev1sec6.html

    7、错误处理

    当UNIX函数出错时,往常返回一个负值,同时将整型变量 errno 设置为具有特定信息的一个值。例如, open 函数如成功执行则返回一个非负文件描述符,如出错则返回 -1 ,同时设置 errno 。在 open 出错时,有大约15种不同的 errno 值(文件不存在,许可权问题等)。

    这里的 errno 就是一个系统的全局变量。文件 <errno.h> 中定义了变量 errno 以及可以赋与它的各种常数,这些常数都以E开头。关于 errno 的具体信息,可以参见系统的用户手册(例如在我的LINUX上面可以运行 man 7 errno )。POSIX中将它定义为:

    extern int errno;
    

    译者注

    前面, “使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。

    原文参考

    参考: APUE2/ch01lev1sec7.html

    8、用户标识

    (1)用户ID

    前面所述的密码文件中的登录项中的用户 IDuser ID )是个数值,用于向系统标识各个不同的用户。通常每个用户有一个唯一的用户 ID ,不同用户对系统具有不同的权限(例如可以访问哪些文件,执行什么程序等)。用户 ID 为0的用户为根( root )或超级用户( superuser )。在密码文件中,其登录名为 root ,我们称这种用户的特权为超级用户特权,它具有整个系统无上的管理权限。_

    (2)组 ID

    密码文件中的登录项也包括用户的组 IDgroup ID ),它也是一个数值。一般来说,在密码文件中有多个记录项具有相同的组 ID 。在UNIX下,组被用于将若干用户集合到某一共同的课题或部门中去。这种机制允许同组的各个成员之间共享资源(例如文件)。4.5节将讲述到,可以设置文件的许可权使组内所有成员都能存取该文件,而组外用户则不能。

    组文件将组名映射为数字组 ID ,它通常是 /etc/group 。对于用户而言,使用名字比使用数值方便,所以密码文件包含了登录名和用户 ID 之间的映射关系,而组文件则包含了组名和组 ID 之间的映射关系。另外,除了在密码文件中对一个登录名指定一个组 ID 外,某些UNIX版本还允许一个用户属于另外一些组。

    译者注

    前面, “使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。

    原文参考

    参考: APUE2/ch01lev1sec8.html

    9、信号

    信号是通知进程发生某种条件的一种技术。例如,若某一进程执行除法操作,其除数为0,则将名为 SIGFPE 的信号发送给该进程。进程有三种选择处理信号:

    • 忽略该信号: 有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的单元等,因为这些异常产生的后果不确定,所以不推荐使用这种处理方式。
    • 按系统默认方式处理: 对于0除,系统默认方式是终止该进程。其它信号有不同的默认行为。
    • 提供一个处理函数,信号发生时则调用该函数: 使用这种方式,我们将能知道什么时候产生了信号,并按所希望的方式处理它。

    通过系统的用户手册,我们可以了解具体有哪些信号,以及它们的编号(例如我在Linux上运行 man 7 signal 将显示当前Linux支持的信号信息)。我们可以使用 kill 函数,产生特定的信号,发送给指定的进程(当然我们必需是该进程的所有者);也可以通过键盘的方式,敲入特殊按键,产生信号。例如,我们运行 ls 程序之后,立即键入 [Ctrl]+C ,这样会导致 ls 运行完成之前中止,因为 [Ctrl]+C 的意思就是给当前终端上运行的程序发送终止信号(这里我给出的例子其实不太好, ls 命令执行的比较快,如果目录中文件不多的话,这里需要你键入的速度足够快才能看到效果,否则没等你键入, ls 就执行完了,就看不到效果了_)。

    译者注

    前面, “使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。

    原文参考

    参考: APUE2/ch01lev1sec9.html

    10、关于进程时间

    为了衡量一个进程的执行时间,unix使用三个值:

    • Clock time : 运行这个进程所花费的时间,这个时间还依赖于系统上执行的其他进程的数量。一般我们假设系统上没有其他进程运行。
    • User CPU time : 运行这个进程所花费的用户 cpu 时间。也就是执行用户(和内核相对)指令所花费的时间。
    • System CPU time : 运行这个进程所花费的系统(内核) cpu 时间。也就是进程在核心态下所消耗的时间,例如调用系统调用。

    用户 cpu 时间和系统 cpu 时间统称进程的 cpu 时间。进程时间可以使用 clock tick 来衡量,例如一秒钟 50clock tick. 测量一个程序的占用时间可以使用 time 命令。

    这里,再次总结一下, Clock time 应该是我们看到的程序运行到结束的总时间,包括实际运行时间,以及由于调度产生的等待时间;而 User+Systemcpu 时间,也就是除了进程调度和等待等因素后,纯粹的执行时间,也就是占用 cpu 的时间; user 表示在用户空间占用的 cpu 时间, system 表示在内核空间占用的 cpu 时间例如系统调用(一般来说,一次系统调用的时间开销,要比用户空间的一次调用开销大)。

    译者注

    前面, “使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。

    原文参考

    参考: APUE2/ch01lev1sec10.html

    11、关于系统调用和库函数

    对于系统调用和库函数的关系,前面大致介绍过,这里再次对此进行说明。

    (a)系统调用是用户和内核交互的接口,用来请求内核提供相应服务

    原来的系统调用接口是用汇编声明,现在用c语言函数方式声明了,其具体的实现方式就不一定是什么了。这样使得用户只需要像用c语言函数一样地,通过包含其声明的头文件,使用系统调用就行了,而不用关心其内部是怎么实现的。

    (b)c语言的库函数是用c语言实现的,它一般是需要调用系统调用来实现的

    系统调用一般和一些c语言的库函数名称一样,用户可以像使用库函数一样使用系统调用,但是两者不同,可以自己定义库函数来取代原来的库函数,而系统调用是无法取代的。例如 malloc 库函数就是用 sbrk 系统调用实现的。 system 库函数就是用 forkexec 系统调用实现的。

    (c)系统调用接口简洁,并且提供了所有的功能,但是不如库函数友好。而库函数可以简单理解成是对系统调用的“封装”

    注意,一般 man 手册里面,其第2节的内容是系统调用,第3节的内容是库函数(这一点在前面的预备知识中已经提到过)。

    译者注

    前面, “使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。

    原文参考

    参考: APUE2/ch01lev1sec11.html

    总结

    本章简单对UNIX系统进行了一下概要说明。我们对一些我们将要遇到的基本的术语进行了解释,后面将会经常提到并且进一步解释它们。

    译者注

    前面, “使用系统调用 readwrite 等对文件描述符号的文件进行读写的时候,文件内容不经过缓存”,这句话中,当然这里的缓存指的是缓冲,并不是没有缓存了,这里不经过缓存的意思是说:告诉系统调用操作多少,那么它就会尽量操作这些数据然后返回,即使数据小于缓存大小,也返回而不是缓存下来;若传入的数据量大于缓存容量那么就分多次操作,因为超过了缓存大小所以不得不分多次操作了。

    原文参考

    参考: APUE2/ch01lev1sec12.html

    相关文章

      网友评论

        本文标题:APUE读书笔记-01UNIX系统概述

        本文链接:https://www.haomeiwen.com/subject/cbxnshtx.html