UNIX基础知识

​1.1  引言

全部操作系统都为它们所执行的程序提供服务。典型的服务包含:执行新程序、打开文件、读文件、分配存储区以及获得当前时间等,本书集中阐述不同版本号的UNIX操作系统所提供的服务。

想要按严格的先后顺序介绍UNIX,而不超前引用尚未介绍过的术语,这差点儿是不可能的(可能也会令人厌烦)。本章从程序猿的角度高速浏览UNIX,对书中引用的一些术语和概念进行简要的说明并给出实例。在以后各章中,将对这些概念做更具体的说明。对于初涉UNIX环境的程序猿,本章还简要介绍了UNIX提供的各种服务。

1.2  UNIX体系结构

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

内核的接口被称为系统调用(system call,图1-1

中的阴影部分)。公用函数库构建在系统调用接口之上,应用程序既可使用公用函数库,也可使用系统调用。(我们将在1.11节对系统调用和库函数做很多其它说明。)shell是一个特殊的应用程序,为执行其它应用程序提供了一个接口。


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

比如,Linux是GNU操作系统使用的内核。一些人将这样的操作系统称为GNU/Linux操作系统,可是,更常见的是简单地称其为Linux。尽管这样的表达方法在严格意义上讲并不对,但鉴于“操作系统”这个词的双重含义,这样的叫法还是能够理解的(这样的叫法更简洁)。

1.3  登录

1.登录名

用户在登录UNIX系统时,先键入登录名,然后键入口令。系统在其口令文件(一般是/etc/ passwd文件)中查看登录名。口令文件里的登录项由7个以冒号分隔的字段组成,依次是:登录名、加密口令、数字用户ID(205)、数字组ID(105)、凝视字段、起始文件夹(/home/sar)以及shell程序(/bin/ksh)。


sar:x:205:105:StephenRago:/home/sar:/bin/ksh


眼下,全部的系统已将加密口令移到还有一个文件里。第6章将说明这样的文件以及訪问它们的函数。


2.shell

用户登录后,系统通常先显示一些系统信息,然后用户就能够向shell程序键入命令。(当用户登录时,某些系统启动一个视窗管理程序,但终于总会有一个shell程序执行在一个视窗中)。shell是一个命令行解释器,它读取用户输入,然后执行命令。shell的用户输入通常来自于终端(交互式shell),有时则来自于文件(称为shell脚本)。图1-2总结了UNIX系统中常见的shell。


系统从口令文件里对应用户登录项的最后一个字段中了解到应该为该登录用户运行哪一个shell。图1-2 UNIX系统中常见的shell

自V7以来,由Steve Bourne在贝尔实验室开发的Bourne shell得到了广泛应用,差点儿每个现有的UNIX系统都提供Bourne shell,其控制流结构类似于Algol 68。


C shell是由Bill Joy在伯克利开发的,全部BSD版本号都提供这样的shell。另外,AT&T的System V/386 R3.2和System V R4(SVR4)也提供C shell(下一章将对这些不同版本号的UNIX系统做很多其它说明)。C shell是在第6版shell而非Bourne shell的基础上构造的,其控制流类似于C语言,它支持Bourne shell没有的一些特色功能,比如作业控制、历史机制以及命令行编辑等。


Korn shell是Bourne shell的后继者,它首先在SVR4中提供。Korn shell是由贝尔实验室的David Korn开发的,在大多数UNIX系统上执行,但在SVR4之前,通常它须要另行购买,所以没有其它两种shell流行。它与Bourne shell向上兼容,并具有使C shell广泛得到应用的一些特色功能,包含作业控制以及命令行编辑等。


Bourne-again shell是GNU shell,全部Linux系统都提供这样的shell。它的设计遵循POSIX标准,同一时候也保留了与Bourne shell的兼容性。它支持C shell和Korn shell两者的特色      功能。

TENEX C shell是C shell的加强版本号。它从TENEX操作系统(1972年BBN公司开发)借鉴了非常多特色,比如命令完备。TENEX C shell在C shell基础上添加了非常多特性,常被用来替换C shell。


POSIX 1003.2标准对shell进行了标准化。这项规范基于Korn shell和Bourne shell的特性。


不同的Linux系统使用不同的默认shell。一些Linux默认使用Bourne-again shell。另外一些使用BSD的对Bourne shell的替代品dash(Debian Almquistshell,最早由KennethAlmquist开发,并在后来移植入Linux)。FreeBSD的默认用户shell衍生于Almquist shell。Mac OS X的默认shell是Bourne-again shell。


Solaries继承了BSD和System V两者,它提供了图1-2中所看到的的全部shell。在因特网上能够找到shell的自由移植版软件。


本书将使用这样的形式的凝视来描写叙述历史凝视,并对不同的UNIX系统的实现进行比較。当我们了解到历史缘由后,会更好地理解採用某种特定实现技术的原因。


本书将使用非常多交互式shell实例来运行所开发的程序,这些实例使用了Bourne shell、Korn shell和Bourne-again shell通用的功能。

1.4  文件和文件夹

1.文件系统

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


文件夹(directory)是一个包括文件夹项的文件。在逻辑上,能够觉得每一个文件夹项都包括一个文件名称,同一时候还包括说明该文件属性的信息。文件属性是指文件类型(是普通文件还是文件夹等)、文件大小、文件全部者、文件权限(其它用户是否能訪问该文件)以及文件最后的改动时间等。stat和fstat函数返回包括全部文件属性的一个信息结构。第4章将具体说明文件的各种属性。


文件夹项的逻辑视图与实际存放在磁盘上的方式是不同的。UNIX文件系统的大多数实现并不在文件夹项中存放属性,这是由于当一个文件具有多个硬链接时,非常难保持多个属性副本之间的同步。这一点将在第4章讨论硬链接时理解得更明晰。

2.文件名称

文件夹中的各个名字称为文件名称(filename)。仅仅有斜线(/)和空字符这两个字符不能出如今文件名称中。斜线用来分隔构成路径名的各文件名称,空字符则用来终止一个路径名。虽然如此,好的习惯还是仅仅使用经常使用印刷字符的一个子集作为文件名称字符(假设在文件名称中使用了某些shell的特殊字符,则必须使用shell的引號机制来引用文件名称,这会带来非常多麻烦)。其实,为了可移植性,POSIX.1推荐将文件名称限制在下面字符集之内:字母(a~z、A~Z)、数字(0~9)、句点(.)、短横线(-)和下划线(_)。


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


Research UNIX System和某些早期UNIX System V的文件系统限制文件名称的最大长度为14个字符,BSD版本号则将这样的限制扩展为255个字符。现今,差点儿全部商业化的UNIX文件系统都支持超过255个字符的文件名称。

3.路径名

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


实例不难列出一个文件夹中全部文件的名字,

图1-3是ls(1)命令的简要实现。

#include"apue.h"

#include<dirent.h>

int

main(int argc, char*argv[])

{

   DIR                *dp;

   struct dirent    *dirp;

   if (argc != 2)

        err_quit("usage: lsdirectory_name");

   if((dp = opendir(argv[1])) == NULL)

        err_sys("can't open %s", argv[1]);

   while ((dirp = readdir(dp)) != NULL)

        printf("%s\n",dirp->d_name);

   closedir(dp);

   exit(0);

}



ls(1)这样的表示方法是UNIX系统的惯用方法,用以引用UNIX系统手冊中的一个特定项。ls(1)引用第一部分中的ls项。各部分通经常使用数字1~8编号,在每一个部分中的各项则按字母顺序排列。在本书中始终假定你有自己所使用的UNIX系统的手冊。


早期的UNIX系统把8个部分都集中在一本《UNIX程序猿手冊》(UNIXProgrammer’sManual)中。随着页数的添加,如今的趋势是把这些部分分别安排在不同的手冊中,比如用户手冊、程序猿手冊以及系统管理员手冊等。


一些UNIX系统用大写字母把某一部分手冊进一步分成若干小部分,比如,AT&T[1990e]中的全部标准I/O函数都被指明位于3S部分中,比如fopen(3S)。还有一些UNIX系统不用数字而是用字母将手冊分成若干部分,如用C表示命令部分等。


现今,大多数手冊都以电子文档形式提供。假设用的是联机手冊,则可用以下的命令查看ls命令手冊页:


man 1 ls



man -s1 ls


图1-3仅仅打印一个文件夹中各个文件的名字,不显示其它信息,假设该源文件名称为myls.c,则能够用以下的命令对其进行编译,编译结果是生成默认名为a.out的可运行文件里。


cc myls.c


历史上,cc(1)是C编译器。在配置了GNU C编译系统的系统中,C编译器是gcc(1)。当中,cc通常链接至gcc。


演示样例输出例如以下:


$ ./a.out /dev

.

..

cdrom

stderr

stdout

stdin

fd

sda4

sda3

sda2

sda1

sda

tty2

tty1

console

tty

zero

null

                  非常多行未显示

mem

$ ./a.out /etc/ssl/private

can't open /etc/ssl/private:Permission denied

$ ./a.out/dev/tty

can't open/dev/tty: Not a directory


本书将以下面方式表示输入的命令及其输出:输入的字符以等宽粗体表示,程序输出则以上面所看到的的等宽字体表示。对输出的凝视以中文宋体表示。输入之前的美元符号($)是shell的提示符,本书总是将shell提示符表示为$。


注意,myls程序列出的文件夹中的文件名称不是以字母顺序列出的,而ls命令通常是按字母顺序打印文件夹项。

在这个20行的程序中,有非常多细节须要考虑。


  • 首先,当中包括了一个头文件apue.h。本书中差点儿每个程序都包括此头文件。它包括了某些标准系统头文件,定义了很多常量及函数原型,这些都将用于本书的各个实例中,附录B列出了这一头文件。

  • 接下来,我们包括了一个系统头文件dirent.h,以便使用opendir和readdir的函数原型,以及dirent结构的定义。在其它一些系统里,这些定义被分成多个头文件。比方,在Ubuntu12.04中,/usr/include/dirent.h声明了函数原型,而且包括bits/dirent.h,后者定义了dirent结构(真正存放在/usr/include/x86_64- linux-gnu/bits下)。

  • main函数的声明使用了ISO C标准所使用的风格(下一章将对ISO C标准进行很多其它说明)。

  • 程序获取命令行的第1个參数argv[1]作为要列出其各个文件夹项的文件夹名。第7章将说明main函数怎样被调用,程序怎样存取命令行參数和环境变量。

  • 由于各种不同UNIX系统文件夹项的实际格式是不一样的,所以使用函数opendir、readdir和closedir对文件夹进行处理。

  • opendir函数返回指向DIR结构的指针,我们将该指针传送给readdir函数。我们并不关心DIR结构中包括了什么。然后,在循环中调用readdir来读每一个文件夹项。它返回一个指向dirent结构的指针,而当文件夹中已无文件夹项可读时则返回null指针。在dirent结构中取出的仅仅是每一个文件夹项的名字(d_name)。使用该名字,此后就可调用stat函数(见4.2节)以获得该文件的全部属性。

  • 程序调用了两个自编的函数对错误进行处理:err_sys和err_quit。从上面的输出中能够看到,err_sys函数打印一条消息(“Permission denied”或“Not a directory”),说明遇到了什么类型的错误。这两个出错处理函数在附录B中说明,1.7节将很多其它地叙述出错处理。


·     当程序将结束时,它以參数0调用函数exit。函数exit终止程序。按惯例,參数0的意思是正常结束,參数值1~255则表示出错。8.5节将说明一个程序(如shell或我们所编写的程序)怎样获得它所运行的还有一个程序的exit状态。              


4.工作文件夹

每一个进程都有一个工作文件夹(working directory),有时称其为当前工作文件夹(current working directory)。全部相对路径名都从工作文件夹開始解释。进程能够用chdir函数更改其工作文件夹。


比如,相对路径名doc/memo/joe指的是当前工作文件夹中的doc文件夹中的memo文件夹中的文件(或文件夹)joe。从该路径名能够看出,doc和memo都应当是文件夹,可是却不能分辨joe是文件还是文件夹。路径名/urs/lib/lint是一个绝对路径名,它指的是根文件夹中的usr文件夹中的lib文件夹中的文件(或文件夹)lint。

5.起始文件夹

登录时,工作文件夹设置为起始文件夹(home directory),该起始文件夹从口令文件(见1.3节)中对应用户的登录项中取得。

1.5  输入和输出

1.文件描写叙述符

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

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

按惯例,每当执行一个新程序时,全部的shell都为其打开3个文件描写叙述符,即标准输入(standard input)、标准输出(standard output)以及标准错误(standard error)。假设不做特殊处理,比如就像简单的命令ls,则这3个描写叙述符都链接向终端。大多数shell都提供一种方法,使当中不论什么一个或全部这3个描写叙述符都能又一次定向到某个文件,比如:


ls > file.list


运行ls命令,其标准输出又一次定向到名为file.list的文件。

3.不带缓冲的I/O

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

实例


假设愿意从标准输入读,并向标准输出写,则图1-4中所看到的的程序可用于复制任一UNIX普通文件。


#include"apue.h"

#define    BUFFSIZE   4096

int

main(void)

{

   int    n;

   char    buf[BUFFSIZE];

   while((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)

        if (write(STDOUT_FILENO, buf, n) !=n)

             err_sys("writeerror");

   if(n < 0)

        err_sys("read error");

   exit(0);

}


头文件<unistd.h>(apue.h中包括了此头文件)及两个常量STDIN_FILENO和STDOUT_FILENO是POSIX标准的一部分(下一章将对此做很多其它的说明)。头文件<unistd.h>包括了非常多UNIX系统服务的函数原型,比如图1-4程序中调用的read和write。


两个常量STDIN_FILENO和STDOUT_FILENO定义在<unistd.h>头文件里,它们指定了标准输入和标准输出的文件描写叙述符。在POSIX标准中,它们的值各自是0和1,可是考虑到可读性,我们将使用这些名字来表示这些常量。


3.9节将具体讨论BUFFSIZE常量,说明它的各种不同值将怎样影响程序的效率。可是无论该常量的值怎样,此程序总能复制任一UNIX普通文件。

read函数返回读取的字节数,此值用作要写的字节数。当到达输入文件的尾端时,read返回0,程序停止运行。假设发生了一个读错误,read返回−1。出错时大多数系统函数返回−1。


假设将该程序编译成标准名称的a.out文件,并下面列方式运行它:


./a.out > data


那么标准输入是终端,标准输出则又一次定向至文件data,标准错误也是终端。假设此输出文件并不存在,则shell会创建它。该程序将用户键入的各行拷贝到标准输出,键入文件结束符(一般是Ctrl+D)时,将终止本次复制。


若下面列方式运行该程序:


./a.out< infile > outfile

会将名为infile文件的内容拷贝到名为outfile的文件里。                                                            



第3章将更具体地说明不带缓冲的I/O函数。

4.标准I/O

标准I/O函数为那些不带缓冲的I/O函数提供了一个带缓冲的接口。使用标准I/O函数无需操心怎样选取最佳的缓冲区大小,如图1-4中的BUFFSIZE常量的大小。使用标准I/O函数还简化了对输入行的处理(经常发生在UNIX的应用程序中)。比如,fgets函数读取一个完整的行,而read函数读取指定字节数。在5.4节中我们将了解到,标准I/O函数库提供了使我们可以控制该库所使用的缓冲风格的函数。


我们最熟悉的标准I/O函数是printf。在调用printf的程序中,总是包括<stdio.h>(在本书中,该头文件包括在apue.h中),该头文件包括了全部标准I/O函数的原型。


实例


图1-5程序的功能类似于前一个调用了read和write的程序,5.8节将对此程序进行更具体的说明。它将标准输入拷贝到标准输出,也就能复制任一UNIX普通文件。


#include"apue.h"

int

main(void)

{

   int       c;

    while ((c = getc(stdin)) != EOF)

         if (putc(c, stdout) == EOF)

              err_sys("outputerror");

    if (ferror(stdin))

         err_sys("input error");

    exit(0);

}


函数getc一次读取一个字符,然后函数putc将此字符写到标准输出。读到输入的最后一个字节时,getc返回常量EOF(该常量在<stdio.h>中定义)。标准I/O常量stdin和stdout也在头文件<stdio.h>中定义,它们分别表示标准输入和标准输出。                                                


1.6  程序和进程

1.程序


程序(program)是一个存储在磁盘上某个文件夹中的可运行文件。内核使用exec函数(7个exec函数之中的一个),将程序读入内存,并运行程序。8.10节将说明这些exec函数。


2.进程和进程ID

程序的运行实例被称为进程(process)。本书的每一页差点儿都会使用这一术语。某些操作系统用任务(task)表示正在被运行的程序。

UNIX系统确保每一个进程都有一个唯一的数字标识符,称为进程ID(process ID)。进程ID总是一个非负整数。

实例

图1-6程序用于打印进程ID。


#include"apue.h"

int

main(void)

{

   printf("hello world from process ID%ld\n", (long)getpid());

   exit(0);

}


假设将该程序编译成a.out文件,然后运行它,则有:


$ ./a.out

helloworld from process ID 851

$ ./a.out

helloworld from process ID 854


此程序执行时,它调用函数getpid得到其进程ID。我们将会在后面看到,getpid返回一个pid_t数据类型。我们不知道它的大小,仅知道的是标准会保证它能保存在一个长整型中。由于我们必须在printf函数中指定须要打印的每个变量的大小,所以我们必须把它的值强制转换为它可能会用到的最大的数据类型(这里是长整型)。尽管大多数进程ID能够用整型表示,但用长整型能够提高可移植性。

3.进程控制

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

实例


UNIX系统的进程控制功能能够用一个简单的程序说明(见图1-7)。该程序从标准输入读取命令,然后运行这些命令。它类似于shell程序的基本实施部分。


#include"apue.h"

#include<sys/wait.h>

int

main(void)

{

   char    buf[MAXLINE];    /* from apue.h */

   pid_t  pid;

   int     status;

   printf("%% ");    /* print prompt (printf requires %% toprint %) */

   while (fgets(buf, MAXLINE, stdin) != NULL){

        if (buf[strlen(buf) - 1] == '\n') 
             buf[strlen(buf) - 1] = 0;/* replace newline with null */

        if ((pid = fork()) < 0) {
             err_sys("forkerror");

        } else if (pid == 0) {        /* child */

             execlp(buf, buf, (char *)0);

             err_ret("couldn't execute: %s",buf);

             exit(127);

        }

        /* parent */

        if ((pid = waitpid(pid, &status, 0)) <0)

             err_sys("waitpid error");

        printf("%% ");

   }

   exit(0);

}


在这个30行的程序中,有非常多功能须要考虑。


  • 用标准I/O函数fgets从标准输入一次读取一行。当键入文件结束符(一般是Ctrl+D)作为行的第一个字符时,fgets返回一个null指针,于是循环停止,进程也就终止。第18章将说明全部特殊的终端字符(文件结束、退格字符、整行擦除等),以及怎样改变它们。


  • 由于fgets返回的每一行都以换行符终止,后随一个null字节,因此用标准C函数strlen计算此字符串的长度,然后用一个null字节替换换行符。这样做是由于execlp函数要求的參数是以null结束的而不是以换行符结束的。


  • 调用fork创建一个新进程。新进程是调用进程的一个副本,我们称调用进程为父进程,新创建的进程为子进程。fork对父进程返回新的子进程的进程ID(一个非负整数),对子进程则返回0。由于fork创建一个新进程,所以说它被调用一次(由父进程),但返回两次(分别在父进程中和在子进程中)。

  • 在子进程中,调用execlp以运行从标准输入读入的命令。这就用新的程序文件替换了子进程原先运行的程序文件。fork和尾随其后的exec两者的组合就是某些操作系统所称的产生(spawn)一个新进程。在UNIX系统中,这两部分分离成两个独立的函数。第8章将对这些函数进行很多其它说明。

  • 子进程调用execlp运行新程序文件,而父进程希望等待子进程终止,这是通过调用waitpid实现的,其參数指定要等待的进程(即pid參数是子进程ID)。waitpid函数返回子进程的终止状态(status变量)。在我们这个简单的程序中,没有使用该值。假设须要,能够用此值准确地判定子进程是怎样终止的。

  • 该程序的最主要限制是不能向所运行的命令传递參数。比如不能指定要列出文件夹项的文件夹名,仅仅能对工作文件夹运行ls命令。为了传递參数,先要分析输入行,然后用某种约定把參数分开(可能使用空格或制表符),再将分隔后的各个參数传递给execlp函数。虽然如此,此程序仍可用来说明UNIX系统的进程控制功能。


假设执行此程序,将得到下列结果。注意,该程序使用了一个不同的提示符(%),以差别于shell的提示符。


$ ./a.out

% date

SatJan 21 19:42:07 EST 2012

% who

sar       console  Jan 1  14:59

sar       ttys000   Jan 1 14:59

sar       ttys001   Jan 15 15:28

% pwd

/home/sar/bk/apue/3e

% ls

Makefile

a.out

shell1.c

% ^D                                      键入文件结束符

$                                          常规的shell提示符

                                                                                 ^D表示一个控制字符。控制字符是特殊字符,其构成方法是:在键盘上按下控制键——通常被标记为Control或Ctrl,同一时候按还有一个键。Ctrl+D或^D是默认的文件结束符。在第18章中讨论终端I/O时,会介绍很多其它的控制字符。

4.线程和线程ID

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

一个进程内的全部线程共享同一地址空间、文件描写叙述符、栈以及与进程相关的属性。由于它们能訪问同一存储区,所以各线程在訪问共享数据时须要採取同步措施以避免不一致性。

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

控制线程的函数与控制进程的函数类似,但另有一套。线程模型是在进程模型建立非常久之后才被引入到UNIX系统中的,然而这两种模型之间存在复杂的交互,在第12章中,我们会对此进行说明。

1.7  出错处理

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


文件<errno.h>中定义了errno以及能够赋与它的各种常量。这些常量都以字符E开头。另外,UNIX系统手冊第2部分的第1页,intro(2)列出了全部这些出错常量。比如,若errno等于常量EACCES,表示产生了权限问题(比如,没有足够的权限打开请求文件)。


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


POSIX和ISO C将errno定义为一个符号,它扩展成为一个可改动的整形左值(lvalue)。它能够是一个包括出错编号的整数,也能够是一个返回出错编号指针的函数。曾经使用的定义是:


externint errno;


可是在支持线程的环境中,多个线程共享进程地址空间,每一个线程都有属于它自己的局部errno以避免一个线程干扰还有一个线程。比如,Linux支持多线程存取errno,将其定义为:


extern int *__errno_location(void);

#define errno (*__errno_location())


对于errno应当注意两条规则。第一条规则是:假设没有出错,其值不会被例程清除。因此,仅当函数的返回值指明出错时,才检验其值。第二条规则是:不论什么函数都不会将errno值设置为0,并且在<errno.h>中定义的全部常量都不为0。

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


#include  <string.h>


char  *strerror(int errnum);


返回值:指向消息字符串的指针


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

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


#include  <stdio.h>


void  perror(const char *msg);


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

实例


图1-8程序显示了这两个出错函数的用法。


#include"apue.h"

#include<errno.h>

int

main(int argc,char *argv[])

{

   fprintf(stderr,"EACCES: %s\n", strerror(EACCES));

   errno = ENOENT;

   perror(argv[0]);

   exit(0);

}


假设将此程序编译成文件a.out,然后运行它,则有


$ ./a.out

EACCES:Permission denied


./a.out: No such file or directory



注意,我们将程序名(argv[0],其值是./a.out)作为參数传递给perror。这是一个标准的UNIX惯例。使用这样的方法,在程序作为管道的一部分运行时,比如:


prog1< inputfile | prog2 | prog3 > outputfile


我们就能分清3个程序中的哪一个产生了一条特定的出错消息。                                                            


本书中的全部实例基本上都不直接调用strerror或perror,而是使用附录B中的出错函数。该附录中的出错函数使我们仅仅用一条C语句就可利用ISO C的可变參数表功能处理出错情况。

出错恢复

可将在<errno.h>中定义的各种出错分成两类:致命性的和非致命性的。对于致命性的错误,无法运行恢复动作。最多能做的是在用户屏幕上打印出一条出错消息或者将一条出错消息写入日志文件里,然后退出。对于非致命性的出错,有时能够较妥善地进行处理。大多数非致命性出错是临时的(如资源短缺),当系统中的活动较少时,这样的出错非常可能不会发生。


与资源相关的非致命性出错包含:EAGAIN、ENFILE、ENOBUFS、ENOLCK、ENOSPC、EWOULDBLOCK,有时ENOMEM也是非致命性出错。当EBUSY指明共享资源正在使用时,也可将它作为非致命性出错处理。当EINTR中断一个慢速系统调用时,可将它作为非致命性出错处理(在10.5节对此会进行很多其它说明)。


对于资源相关的非致命性出错的典型恢复操作是延迟一段时间,然后重试。这样的技术可应用于其它情况。比如,如果出错表明一个网络连接不再起作用,那么应用程序能够採用这样的方法,在短时间延迟后,尝试重建该连接。一些应用使用指数补偿算法,在每次迭代中等待更长时间。


终于,由应用的开发人员决定在哪些情况下应用程序能够从出错中恢复。假设能够採用一种合理的恢复策略,那么能够避免应用程序异常终止,进而就能改善应用程序的健壮性。

1.8  用户标识

1.用户ID

口令文件登录项中的用户ID(user ID)是一个数值,它向系统标识各个不同的用户。系统管理员在确定一个用户的登录名的同一时候,确定其用户ID。用户不能更改其用户ID。通常每一个用户有一个唯一的用户ID。以下将介绍内核怎样使用用户ID来检验该用户是否有运行某些操作的权限。


用户ID为0的用户为根用户(root)或超级用户(superuser)。在口令文件里,通常有一个登录项,其登录名为root,我们称这样的用户的特权为超级用户特权。我们将在第4章中看到,假设一个进程具有超级用户特权,则大多数文件权限检查都不再进行。某些操作系统功能仅仅向超级用户提供,超级用户对系统有自由的支配权。


Mac OS Xclient版本号交由用户使用时,禁用超级用户账户,server版本号则可使用该账户。在Apple的站点能够找到使用说明,它告知怎样才干使用该账户。參见http://support. apple.com/kb/HT1528。


2.组ID

口令文件登录项也包含用户的组ID(group ID),它是一个数值。组ID也是由系统管理员在指定用户登录名时分配的。一般来说,在口令文件里有多个登录项具有同样的组ID。组被用于将若干用户集合到项目或部门中去。这样的机制同意同组的各个成员之间共享资源(如文件)。4.5节将介绍能够通过设置文件的权限使组内全部成员都能訪问该文件,而组外用户不能訪问。


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


使用数值的用户ID和数值的组ID设置权限是历史上形成的。对于磁盘上的每一个文件,文件系统都存储该文件全部者的用户ID和组ID。存储这两个值仅仅需4个字节(假定每一个都以双字节的整型值存放)。假设使用完整ASCII登录名和组名,则需很多其它的磁盘空间。另外,在检验权限期间,比較字符串较之比較整型数更消耗时间。


可是对于用户而言,使用名字比使用数值方便,所以口令文件包括了登录名和用户ID之间的映射关系,而组文件则包括了组名和组ID之间的映射关系。比如,ls -l命令使用口令文件将数值的用户ID映射为登录名,从而打印出文件全部者的登录名。


早期的UNIX系统使用16位整型数表示用户ID和组ID。现今的UNIX系统使用32位整型数表示用户ID和组ID。


实例

图1-9程序用于打印用户ID和组ID。

#include"apue.h"

int

main(void)

{

   printf("uid = %d, gid = %d\n",getuid(), getgid());

  exit(0);

}


程序调用getuid和getgid以返回用户ID和组ID。执行该程序的结果例如以下:


$ ./a.out


uid = 205, gid = 105


3.附属组ID

除了在口令文件里对一个登录名指定一个组ID外,大多数 UNIX系统版本号还同意一个用户属于另外一些组。这一功能是从4.2BSD開始的,它同意一个用户属于多至16个其它的组。登录时,读文件/etc/group,寻找列有该用户作为其成员的前16个记录项就能够得到该用户的附属组ID(supplementary group ID)。在下一章将说明,POSIX要求系统至少应支持8个附属组,实际上大多数系统至少支持16个附属组。

1.9  信号

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


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


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


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


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

实例

回顾一下主要的shell实例(见图1-7程序)。假设调用此程序,然后按下中断键,则运行此程序的进程终止。产生这样的后果的原因是:对于此信号(SIGINT)的系统默认动作是终止进程。该进程没有告诉系统内核应该怎样处理此信号,所以系统按默认方式终止该进程。


为了能捕捉到此信号,程序须要调用signal函数,当中指定了当产生SIGINT信号时要调用的函数的名字。函数名为sig_int,当其被调用时,仅仅是打印一条消息,然后打印一个新提示符。在图1-7程序中加入了11行,构成了图1-10程序(加入的11行以行首的+号指示)。


#include "apue.h"

#include <sys/wait.h>

+ static voidsig_int(int);    /* our signal-catchingfunction */

+

int

main(void)

{

     char buf[MAXLINE]; /* from apue.h */

     pid_t pid;

     int   status;

+      if(signal(SIGINT, sig_int) == SIG_ERR)

+           err_sys("signal error");

+  

     printf("%%"); /* print prompt (printf requires %% to print %) */

     while(fgets(buf, MAXLINE, stdin) != NULL) {

          if (buf[strlen(buf) - 1] == '\n')

               buf[strlen(buf) - 1] = 0; /* replacenewline with null */

          if ((pid = fork()) < 0) {

               err_sys("forkerror");

          } else if (pid == 0) {  /* child */

               execlp(buf, buf, (char *)0);

               err_ret("couldn'texecute: %s", buf);

               exit(127);

          }

          /* parent */

          if ((pid = waitpid(pid, &status, 0))< 0)

               err_sys("waitpid error");

          printf("%% ");

     }

     exit(0);

}
+

+ void

+ sig_int(intsigno)

+ {

+      printf("interrupt\n%%");

+ }



由于大多数重要的应用程序都对信号进行处理,所以第10章将具体介绍信号。                    

1.10  时间值

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


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

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


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

系统基本数据类型clock_t保存这样的时间值。2.5.4节将说明怎样用sysconf函数得到每秒的时钟滴答数。


当度量一个进程的运行时间时(见3.9节),UNIX系统为一个进程维护了3个进程时间值:

·     时钟时间;

·     用户CPU时间;

·     系统CPU时间。


时钟时间又称为墙上时钟时间(wall clock time),它是进程执行的时间总量,其值与系统中同一时候执行的进程数有关。每当在本书中提到时钟时间时,都是在系统中没有其它活动时进行度量的。


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


要取得任一进程的时钟时间、用户时间和系统时间是非常easy的—仅仅要运行命令time(1),其參数是要度量其运行时间的命令,比如:


$ cd /usr/include

$ time -p grep_POSIX_SOURCE */*.h > /dev/null

real    om0.81s

user    om0.11s

sys     om0.07s


time命令的输出格式与所使用的shell有关,其原因是某些shell并不执行/usr/bin/time,而是使用一个内置函数測量命令执行所使用的时间。


8.17节将说明一个执行进程怎样取得这3个时间。关于时间和日期的一般说明见6.10节。


1.11  系统调用和库函数

全部的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本号的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call,见图1-1)。ResearchUNIX系统第7版提供了约50个系统调用,4.4BSD提供了约110个系统调用,而SVR4则提供了约120个系统调用。详细数字在不同操作系统版本号中会不同,新近的大多数系统大大添加了支持的系统调用的个数。Linux 3.2.0提供了380个系统调用,FreeBSD8.0提供的系统调用超过450个。


系统调用接口总是在《UNIX程序猿手冊》的第2部分中说明,是用C语言定义的,与详细系统怎样调用一个系统调用的实现技术无关。这与非常多早期的操作系统不同,那些系统按传统方式用机器的汇编语言定义内核入口点。


UNIX所使用的技术是为每一个系统调用在标准C库中设置一个具有相同名字的函数。用户进程用标准C调用序列来调用这些函数,然后,函数又用系统所要求的技术调用对应的内核服务。比如,函数可将一个或多个C參数送入通用寄存器,然后运行某个产生软中断进入内核的机器指令。从应用角度考虑,可将系统调用视为C函数。


《UNIX程序猿手冊》的第3部分定义了程序猿能够使用的通用库函数。尽管这些函数可能会调用一个或多个内核的系统调用,可是它们并非内核的入口点。比如,printf函数会调用write系统调用以输出一个字符串,但函数strcpy(复制一个字符串)和atoi(将ASCII转换为整数)并不使用不论什么内核的系统调用。


从实现者的角度来看,系统调用和库函数之间有根本的差别,但从用户角度来看,其差别并不重要。在本书中,系统调用和库函数都以C函数的形式出现,两者都为应用程序提供服务。可是,我们应当理解,假设希望的话,我们能够替换库函数,可是系统调用一般是不能被替换的。


以存储空间分配函数malloc为例。有多种方法能够进行存储空间分配及与其相关的无用空间回收操作(最佳适应、首次适应等),并不存在对全部程序都最优的一种技术。UNIX系统调用中处理存储空间分配的是sbrk(2),它不是一个通用的存储器管理器。它按指定字节数添加或降低进程地址空间。怎样管理该地址空间却取决于进程。存储空间分配函数malloc(3)实现一种特定类型的分配。假设我们不喜欢其操作方式,则能够定义自己的malloc函数,它非常可能将使用sbrk系统调用。其实,有非常多软件包,它们使用sbrk系统调用实现自己的存储空间分配算法。图1-11显示了应用程序、malloc函数以及sbrk系统调用之间的关系。


从中可见,两者职责不同,内核中的系统调用分配一块空间给进程,而库函数malloc则在用户层次管理这一空间。


还有一个可说明系统调用和库函数之间区别的样例是,UNIX系统提供的推断当前时间和日期的接口。一些操作系统分别提供了一个返回时间的系统调用和还有一个返回日期的系统调用。不论什么特殊的处理,比如正常时制和夏令时之间的转换,由内核处理或要求人为干预。UNIX系统则不同,它仅仅提供一个系统调用,该系统调用返回自协调世界时1970年1月1日零时这个特定时间以来所经过的秒数。对该值的不论什么解释,比如将其变换成人们可读的、适用于本地时区的时间和日期,都留给用户进程进行处理。在标准C库中,提供了若干例程以处理大多数情况。这些库函数处理各种细节,如各种夏令时算法等。


应用程序既能够调用系统调用也能够调用库函数。非常多库函数则会调用系统调用。图1-12显示了这样的区别。


系统调用和库函数之间的还有一个区别是:系统调用通常提供一种最小接口,而库函数通常提供比較复杂的功能。我们从sbrk系统调用和malloc库函数之间的区别中能够看到这一点。当我们比較不带缓冲的I/O函数(见第3章)和标准I/O函数(见第5章)时,还将看到这样的区别。

进程控制系统调用(fork、exec和wait)通常由用户应用程序直接调用(请回顾图1-7中的基本shell)。可是为了简化某些常见的情况,UNIX系统也提供了一些库函数,如system和popen。8.13节将说明system函数的一种实现,它使用主要的进程控制系统调用。10.18节还将强化这一实例以正确地处理信号。


为使读者了解大多数程序猿应用的UNIX系统接口,我们不得不既说明系统调用,又介绍某些库函数。比如,若仅仅描写叙述sbrk系统调用,那么就会忽略非常多应用程序使用的malloc库函数。本书除了必需要区分两者时,对系统调用和库函数都使用函数(function)这一术语来表示。

1.12  小结


本章高速浏览了UNIX系统。说明了某些以后会多次用到的基本术语,介绍了一些小的UNIX程序实例。读者能够从中大概了解到本书其余部分将要介绍的内容。






你可能感兴趣的:(unix)