Unix之基础知识(一)

本文主体内容来自《UNIX环境高级编程第三版》。

本文主要从UNIX系统的基本知识开始介绍,包括UNIX体系结构以及登录、文件和目录、输入和输出、程序和进程、出错处理、用户标识、信号、时间值、系统调用和库函数等基本概念。


UNIX体系结构

从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。通常将这种软件称为内核(kernel),因为它相对较小,而且位于环境的核心。图1-1显示了UNIX系统的体系结构。

内核的接口被称为系统调用(system call,图1-1中的阴影部分)。公用函数库构建在系统调用接口之上,应用程序既可使用公用函数库,也可使用系统调用。shell 是一个特殊的应用程序,为运行其他应用程序提供了一个接口。

img

图1-1 UNIX操作系统的体系结构

从广义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特性。这里所说的其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等。

例如,Linux是GNU操作系统使用的内核。一些人将这种操作系统称为GNU/Linux操作系统,但是,更常见的是简单地称其为 Linux。


登录

登录名

系统在其口令文件(通常是/etc/passwd文件)中查看登录名。口令文件中的登录项由7个以冒号分隔的字段组成,依次是:登录名、加密口令、数字用户ID、数字组ID、注释字段、起始目录以及shell程序。

invinc-z:x:1000:1000:Invinc-Z:/home/invinc-z:/bin/bash

目前,所有的系统已将加密口令移到另一个文件中。

shell

shell 是一个命令行解释器,它读取用户输入,然后执行命令。shell 的用户输入通常来自于终端(交互式shell),有时则来自于文件(称为shell脚本)。图1-2总结了UNIX系统中常见的shell。

img

图1-2 UNIX系统中常见的shell

系统从口令文件中相应用户登录项的最后一个字段中了解到应该为该登录用户执行哪一个shell。


文件和目录

文件系统

UNIX文件系统是目录和文件的一种层次结构,所有东西的起点是称为根(root)的目录,这个目录的名称是一个字符“/”。

目录(directory)是一个包含目录项的文件。在逻辑上,可以认为每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。文件属性是指文件类型(是普通文件还是目录等)、文件大小、文件所有者、文件权限(其他用户能否访问该文件)以及文件最后的修改时间等。

目录项的逻辑视图与实际存放在磁盘上的方式是不同的。UNIX 文件系统的大多数实现并不在目录项中存放属性,这是因为当一个文件具有多个硬链接时,很难保持多个属性副本之间的同步。

文件名

目录中的各个名字称为文件名(filename)。为了可移植性,POSIX.1 推荐将文件名限制在以下字符集之内:字母(a~z、A~Z)、数字(0~9)、句点(.)、短横线(-)和下划线(_)。

创建新目录时会自动创建了两个文件名:.(称为点)和..(称为点点)。点指向当前目录,点点指向父目录。在最高层次的根目录中,点点与点相同。

路径名

由斜线分隔的一个或多个文件名组成的序列(也可以是斜线开头)构成路径名,以斜线开头的路径名称为绝对路径名,否则称为相对路径名。相对路径名指向相对于当前目录的文件。文件系统根的名字 / 是一个特殊的绝对路径名,它不含文件名。

工作目录

每个进程都有一个工作目录(working directory),有时称其为当前工作目录(current working directory)。所有相对路径名都从工作目录开始解释。进程可以用chdir函数更改其工作目录。

起始目录

登录时,工作目录设置为起始目录(home directory),该起始目录从口令文件(/etc/passwd)中相应用户的登录项中取得。

invinc-z:x:1000:1000:Invinc-Z:/home/invinc-z:/bin/bash


输入和输出

文件描述符

文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。在读、写文件时,可以使用这个文件描述符。

标准输入、标准输出和标准错误

每当运行一个新程序时,所有的 shell 都为其打开 3 个文件描述符,即标准输入、标准输出以及标准错误。如果不做特殊处理,例如就像简单的命令ls,则这3个描述符都链接向终端。大多数shell都提供一种方法,使其中任何一个或所有这3个描述符都能重新定向到某个文件,例如:

ls > file.list

执行ls命令,其标准输出重新定向到名为file.list的文件。

不带缓冲的I/O

函数open、read、write、lseek以及close提供了不带缓冲的I/O。这些函数都使用文件描述符。

标准I/O

标准I/O函数为那些不带缓冲的I/O函数提供了一个带缓冲的接口。使用标准I/O函数无需担心如何选取最佳的缓冲区大小。最熟悉的标准I/O函数是printf。在调用printf的程序中,总是包含<stdio.h>,该头文件包括了所有标准I/O函数的原型。


程序和进程

程序

程序(program)是一个存储在磁盘上某个目录中的可执行文件。内核使用exec函数(7个exec函数之一),将程序读入内存,并执行程序。

进程和进程ID

程序的执行实例被称为进程(process)。某些操作系统用任务(task)表示正在被执行的程序。UNIX系统确保每个进程都有一个唯一的数字标识符,称为进程ID(process ID)。进程 ID总是一个非负整数。

进程控制

有3个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有7种变体,但经常把它们统称为exec函数。)

线程和线程ID

通常,一个进程只有一个控制线程(thread)—某一时刻执行的一组机器指令。对于某些问题,如果有多个控制线程分别作用于它的不同部分,那么解决起来就容易得多。另外,多个控制线程也可以充分利用多处理器系统的并行能力。

一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。因为它们能访问同一存储区,所以各线程在访问共享数据时需要采取同步措施以避免不一致性。

与进程相同,线程也用ID标识。但是,线程ID只在它所属的进程内起作用。一个进程中的线程 ID 在另一个进程中没有意义。当在一进程中对某个特定线程进行处理时,我们可以使用该线程的ID引用它。

控制线程的函数与控制进程的函数类似,但另有一套。


出错处理

当UNIX系统函数出错时,通常会返回一个负值,而且整型变量errno通常被设置为具有特定信息的值。例如,open 函数如果成功执行则返回一个非负文件描述符,如出错则返回−1。在 open出错时,有大约15种不同的errno值(文件不存在、权限问题等)。而有些函数对于出错则使用另一种约定而不是返回负值。例如,大多数返回指向对象指针的函数,在出错时会返回一个null指针。

文件<errno.h>中定义了errno以及可以赋与它的各种常量。这些常量都以字符E开头。

在Linux中,出错常量在errno(3)手册页中列出。

C标准定义了两个函数,它们用于打印出错信息。

1
2
3
#include <string.h>
char *strerror(int errnum);
// 返回值:指向消息字符串的指针

strerror函数将errnum(通常就是errno值)映射为一个出错消息字符串,并且返回此字符串的指针。

perror函数基于errno的当前值,在标准错误上产生一条出错消息,然后返回。

1
2
#include <stdio.h>
void perror(const char *msg);

它首先输出由msg指向的字符串,然后是一个冒号,一个空格,接着是对应于errno值的出错消息,最后是一个换行符。


用户标识

用户ID

口令文件登录项中的用户ID(user ID)是一个数值,它向系统标识各个不同的用户。系统管理员在确定一个用户的登录名的同时,确定其用户ID。用户不能更改其用户ID。通常每个用户有一个唯一的用户 ID。

用户 ID 为 0 的用户为根用户(root)或超级用户(superuser)。在口令文件中,通常有一个登录项,其登录名为 root,我们称这种用户的特权为超级用户特权。

组ID

口令文件登录项也包括用户的组ID(group ID),它是一个数值。组ID也是由系统管理员在指定用户登录名时分配的。一般来说,在口令文件中有多个登录项具有相同的组 ID。组被用于将若干用户集合到项目或部门中去。这种机制允许同组的各个成员之间共享资源(如文件)。

组文件将组名映射为数值的组ID。组文件通常是/etc/group

附属组ID

除了在口令文件中对一个登录名指定一个组ID外,大多数 UNIX系统版本还允许一个用户属于另外一些组。它允许一个用户属于多至16个其他的组。登录时,读文件/etc/group,寻找列有该用户作为其成员的前 16 个记录项就可以得到该用户的附属组ID(supplementary group ID)。


信号

信号(signal)用于通知进程发生了某种情况。例如,若某一进程执行除法操作,其除数为0,则将名为SIGFPE(浮点异常)的信号发送给该进程。进程有以下3种处理信号的方式。

(1)忽略信号。有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的存储单元等,这些异常产生的后果不确定,所以不推荐使用这种处理方式。

(2)按系统默认方式处理。对于除数为0,系统默认方式是终止该进程。

(3)提供一个函数,信号发生时调用该函数,这被称为捕捉该信号。通过提供自编的函数,就能知道什么时候产生了信号,并按期望的方式处理它。

很多情况都会产生信号。终端键盘上有两种产生信号的方法,分别称为中断键(interrupt key,通常是Delete键或Ctrl+C)和退出键(quit key,通常是Ctrl+\),它们被用于中断当前运行的进程。另一种产生信号的方法是调用kill函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当然这样做也有些限制:当向一个进程发送信号时,我们必须是那个进程的所有者或者是超级用户。


时间值

历史上,UNIX系统使用过两种不同的时间值。

(1)日历时间。该值是自协调世界时(Coordinated Universal Time,UTC)1970年1月1日00:00:00这个特定时间以来所经过的秒数累计值(早期的手册称UTC为格林尼治标准时间)。这些时间值可用于记录文件最近一次的修改时间等。

系统基本数据类型time_t用于保存这种时间值。

(2)进程时间。也被称为CPU时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算。每秒钟曾经取为50、60或100个时钟滴答。

系统基本数据类型clock_t保存这种时间值。

当度量一个进程的执行时间时,UNIX系统为一个进程维护了3个进程时间值:

  • 时钟时间;
  • 用户CPU时间;
  • 系统CPU时间。

时钟时间又称为墙上时钟时间(wall clock time),它是进程运行的时间总量,其值与系统中同时运行的进程数有关。

用户CPU时间是执行用户指令所用的时间量。

系统CPU时间是为该进程执行内核程序所经历的时间。例如,每当一个进程执行一个系统服务时,如read或write,在内核内执行该服务所花费的时间就计入该进程的系统CPU时间。用户CPU时间和系统CPU时间之和常被称为CPU时间。


系统调用和库函数

所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call,见图1-1)。

应用程序既可以调用系统调用也可以调用库函数。很多库函数则会调用系统调用。图1-12显示了这种差别。

img

图1-12 C库函数和系统调用之间的差别

系统调用和库函数之间的另一个差别是:系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。

进程控制系统调用(fork、exec 和 wait)通常由用户应用程序直接调用。但是为了简化某些常见的情况,UNIX 系统也提供了一些库函数,如system和popen。