UNIX环境高级编程第一章 读书笔记
一、UNIX体系结构
内核:可将操作系统定位为一种软件,它控制计算机硬件资源,提供程序运行环境,将这种软件称为内核,因为它相对较小,而且位于环境的核心。
系统调用:内核的接口被称为系统调用。
公用函数库:公用函数库构建在系统调用接口之上。
应用程序可以使用公用函数库,或者可以使用系统调用。
shell:shell是一个特殊的应用程序,为运行其他应用程序提供了一个接口。
操作系统的组成:从严格意义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特征。
其他软件:包括系统实用程序、应用程序、shell以及公用函数库。
二、登录
1. 登录名
用户在登录UNIX系统时,先键入登录名,然后输入口令。系统在其口令文件(/etc/passwd)中查看登录名。口令文件中的登录项由7个冒号分隔的字段组成,依次是:登录名,口令文件,加密口令、数字用户ID(205),数字组ID(105),注释字段,起始目录(/home/sar)以及shell程序(/bin/ksh)。
例如:sar:x:205:105:Stephen Rago:/home/sar:/bin/ksh
其中:sar为登录名,x为口令,205为数字用户ID,105为数字组ID,Stephen Rago为注解字段,:/home/sar为起始目录,:/bin/ksh为shell程序2. shell
用户登录后,系统通常先显示一些系统信息,然后用户就可以向shell程序键入命令。shell是一个命令行解释器,它读取用户输入,然后执行命令。shell的用户输入通常来自于终端,有时则来自于文件(称为shell脚本)。
三、文件和目录
1.文件系统
UNIX文件系统是目录和文件的一种层次安排,所有东西的起点称为根(root),这个目录的名称是一个字符“/”。
目录(directory)是一个包含目录项的文件,在逻辑上,可以认为每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。
文件属性包含:文件类型,文件长度,文件所有者,文件的许可权(其他用户能否能访问该文件),文件最后的修改时间等。stat函数和fstat函数返回一个包含所有文件属性的信息结构。
目录项的逻辑视图与实际存放在磁盘上的方式是不同的。
2. 文件名
目录中的各个名字称为文件名(filename)。斜线(/)和空操作符(null)这两个字符不能出现在文件名中。斜线用来分隔构成路径名的各文件名,空操作符则用来终止一个路径名。尽管如此,好的习惯还是只使用印刷字符的一个子集作为文件名字符。(如果在文件名中使用了某些shell特殊字符,则必须使用shell的引号机制来引用文件名)。
当创建一个新目录时会自动创建了两个文件名:.(称为点)和..(称为点点)。点指向当前目录,点点指向父目录。在最高层次的根目录中,点点与点相同。
现今,几乎所有商业化的UNIX文件系统都支持超过255个字符的文件名。
3. 路径名
由斜线分隔的一个或多个文件名组成的序列(也可以斜线开头)构成路径名(pathname),以斜线开头的路径名称为绝对路径名(absolute pathname),否则称为相对路径名(relative pathname)。
相对路径名指向相对于当前目录的文件。
文件系统根的名字(/)是一个特殊的绝对路径名,它不包含文件名。
4. 工作目录
每个进程都有一个工作目录(working directory),有时称其为当前工作目录(current working directory)。所有相对路径名都从工作目录开始解释。进程可以用chdir函数更改其工作目录。
5. 起始目录
登录时,工作目录设置为起始目录(home directory),该起始目录从口令文件中相应用户登录项获得。
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: ls directory_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);
}
四、输入和输出
1.文件描述符
文件描述符通常是一个小的非负整数,内核用以表示一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新的文件时,它都返回一个文件描述符。在读、写文件时,都可以使用这个文件描述符。
2.标准输入、标准输出和标准错误
每当运行一个新的程序时,所有的shell都会为其打开三个文件描述符,即标准输出(standard output)、标准输出(standard input)和标准错误(standard error)。如果不做特殊处理,则这三个描述符都会链接到终端。
3.不带缓冲的I/O
函数open、read、write、lseek以及close提供了不带有缓冲的I/O,这些函数都使用了文件描述符。
4.标准I/O
标准I/O函数为那些不带缓冲的I/O函数提供了带缓冲的接口。使用标准I/O函数无需担心选取最佳的缓冲区大小。使用标准I/O函数还简化了对输入行的处理。
利用标准输入和输出对文件进行操作的简单实现和实现结果
#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("write error");
if (n < 0)
err_sys("read error");
exit(0);
}
将标准输入输出到标准输出的简单实现和实现结果
#include "apue.h"
int
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);
}
五、程序与进程
1.程序
程序是一个存储在磁盘上的某个目录的可执行文件。内核使用exec函数,将程序读入内存,并执行程序。
2.进程和进程ID
进程:程序的执行实例。
进程ID:UNIX系统确保每个进程都有一个唯一标识,称为进程ID。进程ID总是一个非负整数。3.进程控制
三个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有7种变体,但统称为exec函数)。
4.线程和线程ID
线程:某一时刻执行的一组机器指令。
通常,一个进程只有一个控制线程。对于某些问题,如果有多个控制线程分别作用于它的不同部分,那么解决起来就容易得多。另外,多个控制线程也可以充分利用多处理器系统的并行能力。
一个进程的所有线程共享同一地址空间、文件描述符、栈以及进程相关的属性,各线程在访问共享数据时采用同步措施以避免不一致性。
线程ID:用来唯一标识一个进程内的线程。由于线程只存在于进程中,所以线程ID只在所在进程内起作用。一个进程中的线程ID在另一个进程中是没有意义的。当一个进程对某个特定线程进行处理时,我们可以使用该线程ID引用它。
打印进程ID的简单实现和实验结果
#include "apue.h"
int
main(void)
{
printf("hello world from process ID %ld\n", (long)getpid());
exit(0);
}
从标准输入读命令并执行的简单实现和实验结果
#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 %% 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("couldn’t execute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
六、出错处理
当UNIX系统产生错误时,通常会返回一个负值,而且整形变量error通常被设置为具有特定信息的值。
在头文件 < error.h > 中定义了errno以及可以赋与它的各种变量。这些变量都以字符E开头。在支持线程的环境中,多个线程共享进程地址空间,每个线程都有属于它自己的局部errno以避免一个线程干扰另一个线程。
对于errno应当注意的两个规则:
1.如果没有出错,errno的值不会被例程清除。因此,仅当函数的返回值指明出错时,才验证其值。
2.任何函数都不会将errno的值设置为0,而且在 < errno.h > 中定义的所有常量都不为0。C标准中定义两个函数用于打印出错信息。
#include < string.h >
char *strerror(int errrno);#include < stdio.h >
void perror(const char *msg);strerror函数将errno映射为一个出错消息字符串,并且返回此字符串的指针。
perror函数基于errno的当前值,在标准错误上产生一条出错消息,然后返回。它首先输出msg指向的字符串,然后是一个冒号,一个空格,接着是对应于errno值的出错信息,最后是一个换行符。
例如 “./a.out: No such file or directory”出错恢复
可以将 < errno.h >中的各种出错分为两类:致命性的和非致命性的。
1.对于致命性的错误,无法执行恢复操作。最多能做的只是在用户屏幕上打印出错信息或者将其写入出错日志文件,然后退出。
2.对于非致命性的错误,这些错误大多是暂时的,能够妥善的进行处理,而且当系统中活动较少时,这种错误有时候可能不会发生。对于资源相关的非致命错误的典型恢复操作是延迟一段时间,然后重试。
采用合理的恢复策略,可以避免应用程序异常终止,进而改善应用程序健壮性。
上述两个出错函数的简单使用和实验结果
#include "apue.h"
#include <errno.h>
int
main(int argc, char *argv[])
{
/*use strerror*/
fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
errno = ENOENT;
/*use perror*/
perror(argv[0]);
exit(0);
}
七、用户标识
1.用户ID
口令文件登陆项中的用户ID(user ID)是一个数值,它向系统标识各个不同的用户。系统管理员在确定一个用户的登录名的同时,确定其用户ID。用户不能更改其用户ID。通常每个用户只有一个唯一的用户ID。
用户ID为0的用户称为根用户(root)或者超级用户(superuser)。在口令文件中,通常有一个登陆项,其登录名为root,我们称这种用户特权为超级用户特权,如果一个进程具有超级用户特权,则大多数文件权限检查都不再进行。某些系统功能只向超级用户提供,超级用户对系统由*的支配权。
2.组ID
口令文件登陆项也包括用户的组ID(group ID),它是一个数值,组ID也是由系统管理员在指定系统登录名时分配的。一般来说,口令文件中有多个登录项具有相同的组ID。组被用于将若干的用户集合到项目或部门中去。这种机制允许同组的各个成员之间共享资源。
组文件将组名映射为数值的组ID。组文件通常是/etc/group。
对于磁盘上的每个文件,文件系统都存储该文件所有者的用户ID和组ID。用户ID和组ID均使用整形数值进行存储。如果使用完整的ASCII登录名和组名,则需要更多的磁盘空间。另外,在检验权限期间,比较字符串较之比较整形数更消耗时间。
但是对于用户而言,使用名字比使用数值更方便,所以口令文件包含了登录名和用户ID之间的映射关系,而组文件则包含了组名和组ID之间的映射关系。
3.附属组ID
除了在口令文件中对一个登录名制定一个组ID外,大多数UNIX系统版本还允许一个用户属于另一些组。+它允许一个用户属于多至16个其他组。登录时,读文件/etc/group,寻找列有该用户作为其成员的前16个记录项就可以得到该用户的属性组ID(supplementary group ID)。
打印用户ID和组ID的简单实现和实验结果
#include "apue.h"
int
main(void)
{
printf("uid = %d, gid = %d\n", getuid(), getgid());
exit(0);
}
八、信号
信号:用来通知进程发生了某种情况。
进程有以下三种处理信号的方式:
1.忽略信号。
2.按系统默认方式处理。
3.提供一个函数,信号发生时调用该函数,这被称为捕捉信号。通过提供自编函数,我们就知道什么时候产生了信号,并按期望的方式处理它。终端键盘上有两种产生信号的方法,分别称为中断健(interrupt key,通常是Delete键或Ctrl+C)和退出键(quit key,通常是Ctrl+\),他们被用于中断当前运行的进程。
另外,调用kill函数也可以产生信号,在一个进程中调用此函数就可向另一个进程发送一个信号。但是,当一个进程发送信号时,我们必须是那个进程的所有者或者超级用户。
从标准输入读命令并执行的实现和实现结果
#include "apue.h"
#include <sys/wait.h>
static 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("couldn’t 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%% ");
}
九、时间值
UNIX系统使用过两种不同的时间值:
1.历史时间。该值是自协调世界时间(Coordinated Universal Time,UTC)1970年1月1日00:00:00这个特定时间以来所经历的秒数累加值。这些时间值可用于记录文件最近一次的修改时间等。
系统基本数据类型time_t用于保存这种事件值。
2.进程时间。也被称为CPU时间,用以度量进程使用的*处理器资源。进程时间以时钟滴答计算。每秒曾经取为50、60或100个时间滴答。
系统基本数据类型clock_t用于保存这种时间值。当度量一个进程的执行时间时,UNIX系统为一个进程维护了3个进程时间值:
—–时钟时间:又称为墙上时钟时间(wall clock time),它是进程运行的时间总量,其值与系统中同时运行的进程数有关。
—–用户CPU时间:执行用户指令所用时间量。
—–系统CPU时间:为该进程执行内核程序所经历的时间。取得任一进程的时钟时间、用户时间和系统时间是很容易——只要执行命令time(1),其参数是要度量其执行时间的命令。
十、系统调用和库函数
系统调用:所有的操作系统都提供多种服务的人口点,由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call)。
库函数:是把函数放到库里,供别人使用的一种方式。.方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。调用的时候把它所在的文件名用#include<>加到里面就可以了。一般是放到lib文件里的。UNIX所使用的技术是为每个系统调用在标准C库中设置一个具有同样名字的函数。用户进程用标准C调用序列来调用这些函数,然后函数又用系统所要求的技术调用来相应的内核服务。
应用程序既可以调用系统调用也可以调用库函数。许多库函数则会调用系统调用。
系统调用和C库函数之间的区别
1.系统调用直接在系统内核层次上对资源和进程之间进行管理,包括增加,减少等,而C库函数则只是运行在用户管理这一空间,由函数进行对资源进一步管理的操作。
2.系统调用通常提供一个最小的接口,而库函数通常提供比较复杂的功能。