做的比较好的个人网站,网页打不开是什么问题,赣州信息港人才频道,移动端网站建设公司文章目录引言UNIX体系结构登录登录名shell文件和目录文件系统文件名路径名工作目录起始目录输入和输出文件描述符标准输入、标准输出和标准错误不带缓冲的IO标准IO程序和进程程序进程和进程ID进程控制线程和线程ID出错处理出错恢复用户标识用户ID组ID附属组ID信号时间值系统调用…
文章目录引言UNIX体系结构登录登录名shell文件和目录文件系统文件名路径名工作目录起始目录输入和输出文件描述符标准输入、标准输出和标准错误不带缓冲的IO标准IO程序和进程程序进程和进程ID进程控制线程和线程ID出错处理出错恢复用户标识用户ID组ID附属组ID信号时间值系统调用和库函数概述区别一例一例二小结区别二引言
所有操作系统都为它们所运行的程序提供服务。典型的服务包括
执行新程序、打开文件、读文件、分配存储区获得当前时间…
本书集中阐述不同版本的UNIX操作系统所提供的服务。
MyNote将专注于Linux的。
本章从程序员的角度快速浏览UNIX对书中引用的一些术语和概念进行简要的说明并给出实例。在以后各章中将对这些概念做更详细的说明。
对于初涉UNIX环境的程序员本章还简要介绍了UNIX提供的各种服务。
UNIX体系结构
从严格意义上说可将操作系统定义为一种软件它控制计算机硬件资源提供程序运行环境。我们通常将这种软件称为内核kernel因为它相对较小而且位于环境的核心。下图显示了UNIX 系统的体系结构。 内核的接口被称为系统调用system call图中的阴影部分。公用函数库构建在系统调用接口之上应用程序既可使用公用函数库也可使用系统调用。shell是一个特殊的应用程序为运行其他应用程序提供了一个接口。
从广义上说操作系统包括了内核和一些其他软件这些软件使得计算机能够发挥作用并使计算机具有自己的特性。这里所说的其他软件包括系统实用程序system utility、应用程序、shell 以及公用函数库等。
例如Linux是GNU操作系统使用的内核。一些人将这种操作系统称为GNU/Linux操作系统但是更常见的是简单地称其为Linux。虽然这种表达方法在严格意义上讲并不正确但鉴于“操作系统”这个词的双重含义这种叫法还是可以理解的这样的叫法更简洁。
登录
登录名
用户在登录UNIX系统时先键入登录名然后键入口令。
系统在其口令文件通常是/etc/passwd文件中查看登录名。
口令文件中的登录项由7个以冒号分隔的字段组成依次是
登录名、加密口令、数字用户ID(205)、数字组ID (105)、注释字段、起始目录(/home/sarshell程序/bin/ksh)。
sar:x:205:105:Stephen Rago:/home/sar:/bin/ksh目前所有的系统已将加密口令移到另一个文件中。第6章将说明这种文件以及访问它们的函数。 etcEditable Text Configuration shell
用户登录后系统通常先显示一些系统信息然后用户就可以向shell程序键入命令。(当用户登录时某些系统启动一个视窗管理程序但最终总会有一个shell程序运行在一个视窗中。
shell是一个命令行解释器它读取用户输入然后执行命令。shell 的用户输入通常来自于终端交互式shell有时则来自于文件称为shell脚本。
下表总结了UNIX系统中常见的shell。
NamePathLinux 3.2.0Bourne shell/bin/sh•Bourne-again shell/bin/bash•C shell/bin/cshoptionalKorn shell/bin/kshoptionalTENEX C shell/bin/tcshoptional
文件和目录
文件系统
UNIX文件系统是目录和文件的一种层次结构所有东西的起点是称为根root的目录这个目录的名称是一个字符“/”。
目录directory是一个包含目录项的文件。在逻辑上可以认为每个目录项都包含 一个文件名 说明该文件属性的信息 文件类型是普通文件还是目录等、 文件大小、 文件所有者、 文件权限其他用户能否访问该文件 文件最后的修改时间 …
stat和fstat函数返回包含所有文件属性的一个信息结构。
第4章将详细说明文件的各种属性。
文件名
目录中的各个名字称为文件名filename。只有斜线/和空字符这两个字符不能出现在文件名中。斜线用来分隔构成路径名的各文件名空字符则用来终止一个路径名。尽管如此好的习惯还是只使用常用印刷字符的一个子集作为文件名字符如果在文件名中使用了某些shell的特殊字符则必须使用shell的引号机制来引用文件名这会带来很多麻烦)。
事实上为了可移植性POSIX.1推荐将文件名限制在以下字符集之内字母a ~ z、A ~ Z、数字0 ~ 9、句点.、短横线-和下划线_。
创建新目录时会自动创建了两个文件名
.称为点点指向当前目录..称为点点点点指向父目录。
在最高层次的根目录中点点与点相同。 注意空字符‘\0’和空格符(’ )是分别不同的字符。 路径名
由斜线分隔的一个或多个文件名组成的序列也可以斜线开头构成路径名pathname以斜线开头的路径名称为绝对路径名absolute pathname否则称为相对路径名relative pathname)。相对路径名指向相对于当前目录的文件。
文件系统根的名字/是一个特殊的绝对路径名它不包含文件名。
实例ls命令的简要实现
#include apue.h//1.
#include dirent.h//2. 为了使用opendir和readdir的函数原型以及 dirent 结构的定义int
main(int argc, char *argv[])//3.4.
{DIR *dp;//6.struct dirent *dirp;if (argc ! 2)err_quit(usage: ls directory_name);if ((dp opendir(argv[1])) NULL)//5.6.err_sys(cant open %s, argv[1]);//7.while ((dirp readdir(dp)) ! NULL)//5.6.printf(%s\n, dirp-d_name);closedir(dp);//5.exit(0);//8.
}这个程序中有很多细节需要考虑 首先其中包含了一个头文件apue.h。本书中几乎每一个程序都包含此头文件。它包含了某些标准系统头文件定义了许多常量及函数原型这些都将用于本书的各个实例中附录B列出了这一头文件源码。 接下来我们包含了一个系统头文件dirent.h以便使用opendir和readdir的函数原型以及 dirent 结构的定义。在其他一些系统里这些定义被分成多个头文件。比如在Ubuntu 12.04中/usr/include/dirent.h声明了函数原型并且包含bits/dirent.h后者定义了dirent结构真正存放在/usrlinclude/x86_64-linux-gnu/bits 下)。MyNoteCentOS的存在/usr/include/dirent.h 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章以获得该文件的所有属性。 程序调用了两个自编的函数对错误进行处理err_sys 和err_quit。如果用户无权限访问该目录或目录不存在err_sys 函数打印一条消息“Permission denied”或“Not a directory”说明遇到了什么类型的错误。这两个出错处理函数在附录B中说明随后将更多地叙述出错处理。 当程序将结束时它以参数0调用函数exit。函数exit终止程序。按惯例参数0的意思是正常结束参数值1~255则表示出错。第8章将说明一个程序如 shell 或我们所编写的程序如何获得它所执行的另一个程序的exit状态。
工作目录
每个进程都有一个工作目录working directory有时称其为当前工作目录current working directory。所有相对路径名都从工作目录开始解释。进程可以用chdir函数更改其工作目录。
例如 相对路径名doc/memo/joe指的是当前工作目录中的doc目录中的memo目录中的文件或目录joe。从该路径名可以看出doc和memo都应当是目录但是却不能分辨joe是文件还是目录。 路径名/urs/lib/lint是一个绝对路径名它指的是根目录中的usr目录中的lib目录中的文件或目录lint.
起始目录
登录时工作目录设置为起始目录home directory该起始目录从口令文件/etc/passwd中相应用户的登录项中取得。
输入和输出
文件描述符
文件描述符file descriptor通常是一个小的非负整数内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时它都返回一个文件描述符。在读、写文件时可以使用这个文件描述符。
标准输入、标准输出和标准错误
按惯例每当运行一个新程序时所有的shell都为其打开3个文件描述符即标准输入standard input、标准输出standard output以及标准错误standard error。
如果不做特殊处理例如就像简单的命令ls则这3个描述符都链接向终端。大多数shell都提供一种方法使其中任何一个或所有这3个描述符都能重新定向到某个文件。例如
ls file.list执行ls命令其标准输出重新定向到名为file.list的文件。
不带缓冲的IO
函数open、read、write、lseek 以及close提供了不带缓冲的IO。这些函数都使用文件描述符。
实例如果愿意从标准输入读并向标准输出写下面程序可用于复制任一UNIX普通文件。
#include apue.h//1.#define BUFFSIZE 4096int
main(void)
{int n;char buf[BUFFSIZE];//3.while ((n read(STDIN_FILENO, buf, BUFFSIZE)) 0)//1.2.3if (write(STDOUT_FILENO, buf, n) ! n)//1.2.err_sys(write error);if (n 0)err_sys(read error);exit(0);
} 头文件unistd.h apue.h中包含了此头文件及两个常量STDIN_FILENO和STDOUT.FILENO是 POSTX标准的一部分下一章将对此做更多的说明。头文件unistd.h包含了很多UNIX系统服务的函数原型例如上例程序中调用的read和write。 两个常量STDIN_FILENO和STDOUT_FILENO定义在unistd.h头文件中它们指定了标准输入和标准输出的文件描述符。在POSIX标准中它们的值分别是0和1但是考虑到可读性我们将使用这些名字来表示这些常量。 第3章将详细讨论BUFFSIZE常量说明它的各种不同值将如何影响程序的效率。但是不管该常量的值如何此程序总能复制任一UNIX普通文件。 read函数返回读取的字节数此值用作要写的字节数。当到达输入文件的尾端时read返回0程序停止执行。如果发生了一个读错误read返回-1。出错时大多数系统函数返回-1。
如果将该程序编译成标准名称的a.out文件并以下列方式执行它:
./a.out data那么标准输入是终端标准输出则重新定向至文件 data标准错误也是终端。如果此输出文件并不存在则shell 会创建它。该程序将用户键入的各行复制到标准输出键入文件结束符通常是CtrlD时将终止本次复制。
若以下列方式执行该程序
./ a.out infile outfile会将名为infile文件的内容复制到名为outfile的文件中。
第3章将更详细地说明不带缓冲的IO函数。
标准IO
标准IO函数为那些不带缓冲的IO函数提供了一个带缓冲的接口。使用标准I/O函数无需担心如何选取最佳的缓冲区大小。使用标准IO函数还简化了对输入行的处理常常发生在UNIX的应用程序中)。例如fgets函数读取一个完整的行而read函数读取指定字节数。
在第5章中我们将了解到标准IO函数库提供了使我们能够控制该库所使用的缓冲风格的函数。
我们最熟悉的标准IO函数是printf。在调用printf 的程序中总是包含stdio.h在本书中该头文件包含在apue.h中该头文件包括了所有标准IO函数的原型。
实例下面程序的功能类似于前一个调用了read和 write的程序第5章将对此程序进行更详细的说明。它将标准输入复制到标准输出也就能复制任一UNIX普通文件。
#include apue.hint
main(void)
{int c;while ((c getc(stdin)) ! EOF)if (putc(c, stdout) EOF)err_sys(output error);if (ferror(stdin))err_sys(input error);exit(0);
}函数getc一次读取一个字符然后函数putc将此字符写到标准输出。读到输入的最后一个字节时getc返回常量EOF该常量在stdio.h中定义。标准IO常量stdin和 stdout也在头文件stdio.h中定义它们分别表示标准输入和标准输出。
程序和进程
程序
程序 program是一个存储在磁盘上某个目录中的可执行文件。内核使用exec函数7个exec函数之一将程序读入内存并执行程序。第8章将说明这些exec函数。
进程和进程ID
程序的执行实例被称为进程 process)。本书的每一页几乎都会使用这一术语。某些操作系统用任务( task)表示正在被执行的程序。
UNIX系统确保每个进程都有一个唯一的数字标识符称为进程ID(process ID)。进程ID总是一个非负整数。
#include apue.hint
main(void)
{printf(hello world from process ID %ld\n, (long)getpid());exit(0);
}此程序运行时它调用函数 getpid 得到其进程ID。我们将会在后面看到getpid返回一个pid_t数据类型。我们不知道它的大小仅知道的是标准会保证它能保存在一个长整型中。因为我们必须在printf函数中指定需要打印的每一个变量的大小所以我们必须把它的值强制转换为它可能会用到的最大的数据类型这里是长整型)。
虽然大多数进程ID可以用整型表示但用长整型可以提高可移植性。
进程控制
有3个用于进程控制的主要函数fork、exec和waitpid。exec函数有7种变体但经常把它们统称为exec函数。
实例UNIX系统的进程控制功能可以用一个简单的程序说明。该程序从标准输入读取命令然后执行这些命令。它类似于shell程序的基本实施部分。
#include apue.h
#include sys/wait.hint
main(void)
{char buf[MAXLINE]; /* from apue.h */pid_t pid;int status;printf(%% ); /* print prompt (printf requires %% to print %) */while (fgets(buf, MAXLINE, stdin) ! NULL) {//1.if (buf[strlen(buf) - 1] \n)//2.buf[strlen(buf) - 1] 0; /* replace newline with null */if ((pid fork()) 0) {//3.err_sys(fork error);} else if (pid 0) { /* child */execlp(buf, buf, (char *)0);//4.err_ret(couldnt execute: %s, buf);exit(127);}/* parent */if ((pid waitpid(pid, status, 0)) 0)//5.err_sys(waitpid error);printf(%% );}exit(0);
}实验过程
[jallenlocalhost intro]$ ./shell1
% date
2021年 05月 10日 星期一 12:21:54 PDT
% ls
getcputc hello.c Makefile shell1 shell2.c uidgid
getcputc.c ls1 mycat shell1.c testerror uidgid.c
hello ls1.c mycat.c shell2 testerror.c
% pwd
/home/jallen/Desktop/apue.3e/intro
% who
jallen tty1 2021-05-10 07:19 (:0)
jallen pts/0 2021-05-10 07:58 (:0.0)
% [jallenlocalhost intro]$ ^C
[jallenlocalhost intro]$ 用标准IO函数fgets从标准输入一次读取一行。当键入文件结束符通常是CtrlD作为行的第一个字符时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系统的进程控制功能。
线程和线程ID
通常一个进程只有一个控制线程thread——某一时刻执行的一组机器指令。对于某些问题如果有多个控制线程分别作用于它的不同部分那么解决起来就容易得多。另外多个控制线程也可以充分利用多处理器系统的并行能力MyNote多线程优势。
一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。因为它们能访问同一存储区所以各线程在访问共享数据时需要采取同步措施以避免不一致性MyNote利用多线程优势注意事项。
与进程相同线程也用ID标识。但是线程ID只在它所属的进程内起作用。一个进程中的线程ID在另一个进程中没有意义。当在一进程中对某个特定线程进行处理时我们可以使用该线程的ID引用它。
控制线程的函数与控制进程的函数类似但另有一套。线程模型是在进程模型建立很久之后才被引入到UNIX系统中的然而这两种模型之间存在复杂的交互在第12章中我们会对此进行说明。
出错处理
当UNIX系统函数出错时通常会返回一个负值而且整型变量errno通常被设置为具有特定信息的值。例如open 函数如果成功执行则返回一个非负文件描述符如出错则返回-1。
在open出错时有大约15种不同的errno值文件不存在、权限问题等。而有些函数对于出错则使用另一种约定而不是返回负值。例如大多数返回指向对象指针的函数在出错时会返回一个null指针。
文件errno.h中定义了errno以及可以赋与它的各种常量。这些常量都以字符E开头。
man errnoPOSIX和ISO C将errno定义为一个符号它扩展成为一个可修改的整形左值lvalue。它可以是一个包含出错编号的整数也可以是一个返回出错编号指针的函数。以前使用的定义是
extern int errno;但是在支持线程的环境中多个线程共享进程地址空间每个线程都有属于它自己的局部errno以避免一个线程干扰另一个线程。例如Linux支持多线程存取errno将其定义为
extern int *__errno_location (void);
#define errno (*_errno_1ocation())MyNote不懂具体怎么存取。
对于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值的出错消息最后是一个换行符。
实例显示了这两个出错函数的使用方法。
#include apue.h
#include errno.hint
main(int argc, char *argv[])
{fprintf(stderr, EACCES: %s\n, strerror(EACCES));errno ENOENT;perror(argv[0]);exit(0);
}输出结果
[jallenlocalhost intro]$ ./testerror
EACCES: Permission denied
./testerror: No such file or directory注意我们将程序名argv[0]其值是. /a.out作为参数传递给perror。这是一个标准的UNIX惯例。使用这种方法在程序作为管道的一部分执行时例如
prog1 inputfile | prog2 | prog3 outputfile我们就能分清3个程序中的哪一个产生了一条特定的出错消息。
本书中的所有实例基本上都不直接调用strerror或perror而是使用附录B中的出错函数。该附录中的出错函数使我们只用一条C语句就可利用ISOC的可变参数表功能处理出错情况。
出错恢复
可将在errno.h中定义的各种出错分成两类
致命性的。对于致命性的错误无法执行恢复动作。最多能做的是在用户屏幕上打印出一条出错消息或者将一条出错消息写入日志文件中然后退出。非致命性的。对于非致命性的出错有时可以较妥善地进行处理。大多数非致命性出错是暂时的如资源短缺当系统中的活动较少时这种出错很可能不会发生。
与资源相关的非致命性出错包括
EAGAIN、ENFILE、ENOBUFS、ENOLCK、ENOSPC、EWOULDBLOCK有时ENOMEM也是非致命性出错。当EBUSY指明共享资源正在使用时也可将它作为非致命性出错处理。当EINTR中断一个慢速系统调用时可将它作为非致命性出错处理第10章内容。
对于资源相关的非致命性出错的典型恢复操作是延迟一段时间然后重试。这种技术可应用于其他情况。例如假设出错表明一个网络连接不再起作用那么应用程序可以采用这种方法在短时间延迟后尝试重建该连接。一些应用使用指数补偿算法在每次迭代中等待更长时间。
最终由应用的开发者决定在哪些情况下应用程序可以从出错中恢复。如果能够采用一种合理的恢复策略那么可以避免应用程序异常终止进而就能改善应用程序的健壮性。
用户标识
用户ID
口令文件登录项中的用户ID user ID是一个数值它向系统标识各个不同的用户。系统管理员在确定一个用户的登录名的同时确定其用户ID。用户不能更改其用户ID。通常每个用户有一个唯一的用户ID。
下面将介绍内核如何使用用户ID来检验该用户是否有执行某些操作的权限。
用户ID为0的用户为根用户root或超级用户superuser)。在口令文件中通常有一个登录项其登录名为root我们称这种用户的特权为超级用户特权。我们将在第4章中看到如果一个进程具有超级用户特权则大多数文件权限检查都不再进行。
某些操作系统功能只向超级用户提供超级用户对系统有自由的支配权。
组ID
口令文件登录项也包括用户的组IDgroup ID它是一个数值。组ID也是由系统管理员在指定用户登录名时分配的。一般来说在口令文件中有多个登录项具有相同的组ID。组被用于将若干用户集合到项目或部门中去。这种机制允许同组的各个成员之间共享资源如文件)。第4章将介绍可以通过设置文件的权限使组内所有成员都能访问该文件而组外用户不能访问。
组文件将组名映射为数值的组ID。组文件通常是/etc/group。
使用数值的用户ID和数值的组ID设置权限是历史上形成的。对于磁盘上的每个文件文件系统都存储该文件所有者的用户ID和组ID。存储这两个值只需4个字节假定每个都以双字节的整型值存放。如果使用完整ASCII登录名和组名则需更多的磁盘空间。另外在检验权限期间比较字符串较之比较整型数更消耗时间。
但是对于用户而言使用名字比使用数值方便所以口令文件包含了登录名和用户ID之间的映射关系而组文件则包含了组名和组D之间的映射关系。例如ls -l命令使用口令文件将数值的用户ID映射为登录名从而打印出文件所有者的登录名。
实例打印用户ID和组ID。
#include apue.hint
main(void)
{printf(uid %d, gid %d\n, getuid(), getgid());exit(0);
}附属组ID
除了在口令文件中对一个登录名指定一个组ID外大多数UNIX系统版本还允许一个用户属于另外一些组。
这一功能是从4.2BSD开始的它允许一个用户属于多至16个其他的组。登录时读文件/etc/group寻找列有该用户作为其成员的前16个记录项就可以得到该用户的附属组ID supplementary group ID。
在下一章将说明POSIX要求系统至少应支持8个附属组实际上大多数系统至少支持16个附属组。
信号
信号signal用于通知进程发生了某种情况。例如若某一进程执行除法操作其除数为0则将名为SIGFPE浮点异常的信号发送给该进程。进程有以下3种处理信号的方式
忽略信号。有些信号表示硬件异常例如除以0或访问进程地址空间以外的存储单元等因为这些异常产生的后果不确定所以不推荐使用这种处理方式。按系统默认方式处理。对于除数为0系统默认方式是终止该进程。提供一个函数信号发生时调用该函数这被称为捕捉该信号。通过提供自编的函数我们就能知道什么时候产生了信号并按期望的方式处理它。MyNote自定义处理
很多情况都会产生信号。终端键盘上有两种产生信号的方法它们被用于中断当前运行的进程分别称为
中断键interrupt key通常是Delete键或CtrlC退出键quit key通常是Ctrl\
另一种产生信号的方法是调用kill函数。在一个进程中调用此函数就可向另一个进程发送一个信号。
当然这样做也有些限制:当向一个进程发送信号时我们必须是那个进程的所有者或者是超级用户。
实例
回忆一下基本的shell实例在进程控制章节。如果调用此程序然后按下中断键则执行此程序的进程终止。产生这种后果的原因是对于此信号SIGINT的系统默认动作是终止进程。该进程没有告诉系统内核应该如何处理此信号所以系统按默认方式终止该进程。
为了能捕捉到此信号程序需要调用signal函数其中指定了当产生STGINT信号时要调用的函数的名字。函数名为sig_int当其被调用时只是打印一条消息然后打印一个新提示符。
#include apue.h
#include sys/wait.hstatic void sig_int(int); /* our signal-catching function */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; /* replace newline with null */if ((pid fork()) 0) {err_sys(fork error);} else if (pid 0) { /* child */execlp(buf, buf, (char *)0);err_ret(couldnt execute: %s, buf);exit(127);}/* parent */if ((pid waitpid(pid, status, 0)) 0)err_sys(waitpid error);printf(%% );}exit(0);
}void
sig_int(int signo)
{printf(interrupt\n%% );
}
实验过程
[jallenlocalhost intro]$ ./shell2
% ls
getcputc hello.c Makefile shell1 shell2.c uidgid
getcputc.c ls1 mycat shell1.c testerror uidgid.c
hello ls1.c mycat.c shell2 testerror.c
% date
2021年 05月 10日 星期一 12:25:06 PDT
% ^Cinterrupt
^C% interrupt
^C% interrupt
date
2021年 05月 10日 星期一 12:25:17 PDT
% % ls
getcputc hello.c Makefile shell1 shell2.c uidgid
getcputc.c ls1 mycat shell1.c testerror uidgid.c
hello ls1.c mycat.c shell2 testerror.c
%
运行程序后键盘输入Ctrl D便会打印“interrupt”但程序并未结束还能运行。 在函数的返回类型前加上static就是静态函数。其特性如下 静态函数只能在声明它的文件中可见其他文件不能引用该函数不同的文件可以使用相同名字的静态函数互不影响 Link 时间值
历史上UNIX系统使用过两种不同的时间值。 日历时间。该值是自协调世界时Coordinated Universal TimeUTC1970年1月1日00:00:00这个特定时间以来所经过的秒数累计值早期的手册称UTC为格林尼治标准时间。这些时间值可用于记录文件最近一次的修改时间等。系统基本数据类型time_t用于保存这种时间值。 进程时间。也被称为CPU时间用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算。每秒钟曾经取为50、60或100个时钟滴答。系统基本数据类型clock_t保存这种时间值。第2章将说明如何用sysconf函数得到每秒的时钟滴答数。
当度量一个进程的执行时间时第3章UNIX系统为一个进程维护了3个进程时间值:
时钟时间又称为墙上时钟时间wall clock time它是进程运行的时间总量其值与系统中同时运行的进程数有关。每当在本书中提到时钟时间时都是在系统中没有其他活动时进行度量的。用户CPU时间是执行用户指令所用的时间量。系统CPU时间是为该进程执行内核程序所经历的时间。例如每当一个进程执行一个系统服务时如read或write在内核内执行该服务所花费的时间就计入该进程的系统CPU时间。
用户CPU时间和系统CPU时间之和常被称为CPU时间进程时间。
要取得任一进程的时钟时间、用户时间和系统时间是很容易的——只要执行命令time(1)其参数是要度量其执行时间的命令例如
$ cd /usr/include
$ time -p grep _POSIX_SOURCE */*.h /dev/null
real 0m0.81s
user 0m0.11s
sys 0m0.07stime命令的输出格式与所使用的shell有关其原因是某些shell并不运行/usr/bin/time而是使用一个内置函数测量命令运行所使用的时间。
第8章将说明一个运行进程如何取得这3个时间。关于时间和日期的一般说明见第6章。
系统调用和库函数
概述
所有的操作系统都提供多种服务的入口点由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点这些入口点被称为系统调用system call。Linux 3.2.0提供了380个系统调用FreeBSD 8.0提供的系统调用超过450个。 系统调用接口总是在《UNIX程序员手册》的第2部分中说明是用C语言定义的与具体系统如何调用一个系统调用的实现技术无关。这与很多早期的操作系统不同那些系统按传统方式用机器的汇编语言定义内核入口点。
UNIX所使用的技术是为每个系统调用在标准C库中设置一个具有同样名字的函数。用户进程用标准C调用序列来调用这些函数然后函数又用系统所要求的技术调用相应的内核服务。例如函数可将一个或多个C参数送入通用寄存器然后执行某个产生软中断进入内核的机器指令。从应用角度考虑可将系统调用视为C函数。
《UNIX程序员手册》的第3部分定义了程序员可以使用的通用库函数。虽然这些函数可能会调用一个或多个内核的系统调用但是它们并不是内核的入口点。例如printf 函数会调用write系统调用以输出一个字符串但函数strcpy复制一个字符串和atoi将ASCII转换为整数并不使用任何内核的系统调用。MyNote库函数可能不使用任何内核的系统调用。
区别一
从实现者的角度来看系统调用和库函数之间有根本的区别但从用户角度来看其区别并不重要。在本书中系统调用和库函数都以C函数的形式出现两者都为应用程序提供服务。但是我们应当理解如果希望的话我们可以替换库函数但是系统调用通常是不能被替换的。
例一
以存储空间分配函数malloc为例。有多种方法可以进行存储空间分配及与其相关的无用空间回收操作最佳适应、首次适应等)并不存在对所有程序都最优的一种技术。
UNIX系统调用中处理存储空间分配的是sbrk它不是一个通用的存储器管理器。它按指定字节数增加或减少进程地址空间。如何管理该地址空间却取决于进程。
存储空间分配函数malloc实现一种特定类型的分配。如果我们不喜欢其操作方式则可以定义自己的malloc函数它很可能将使用sbrk 系统调用。malloc可以自定义
事实上有很多软件包它们使用sbrk系统调用实现自己的存储空间分配算法。下图显示了应用程序、malloc函数以及sbrk系统调用之间的关系。 MyNote个人理解库函数是应用程序与系统调用之间中间层可有更多操作空间。
从中可见两者职责不同内核中的系统调用分配一块空间给进程而库函数malloc则在用户层次管理这一空间。
例二
另一个可说明系统调用和库函数之间差别的例子是UNIX系统提供的判断当前时间和日期的接口。
一些操作系统分别提供了一个返回时间的系统调用和另一个返回日期的系统调用。任何特殊的处理例如正常时制和夏令时之间的转换由内核处理或要求人为干预。UNIX系统则不同它只提供一个系统调用该系统调用返回自协调世界时1970年1月1日零时这个特定时间以来所经过的秒数。
对该值的任何解释例如将其变换成人们可读的、适用于本地时区的时间和日期都留给用户进程进行处理。在标准C库中提供了若干例程以处理大多数情况。这些库函数处理各种细节如各种夏令时算法等。
小结
应用程序既可以调用系统调用也可以调用库函数。很多库函数则会调用系统调用。下图显示这种差别。 区别二
系统调用和库函数之间的另一个差别是系统调用通常提供一种最小接口而库函数通常提供比较复杂的功能。我们从sbrk系统调用和malloc库函数之间的差别中可以看到这一点。当我们比较不带缓冲的I/O函数见第3章和标准IO函数(见第5章时还将看到这种差别。
进程控制系统调用fork、exec和 wait通常由用户应用程序直接调用。但是为了简化某些常见的情况UNIX系统也提供了一些库函数如 system和popen。第8章将说明system函数的一种实现它使用基本的进程控制系统调用。第10章还将强化这一实例以正确地处理信号。
MyNote系统调用简单少功能库函数复杂多功能。
为使读者了解大多数程序员应用的UNIX系统接口我们不得不既说明系统调用又介绍某些库函数。例如若只描述sbrk系统调用那么就会忽略很多应用程序使用的malloc库函数。本书除了必须要区分两者时对系统调用和库函数都使用函数function这一术语来表示。