APUE Unix 基础知识

《UNIX环境高级编程》第1章笔记

Unix 体系结构

enter description here

可以将操作系统定义为一种软件,其控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核( kemel),因为它相对较小, 而且位于环境的核心。

内核的接口被称为系统调用( system call)。公用函数库构建在系统调用接口之上,应用程序既可使用公用函数库,也可使用系统调用。

登录

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

shell是一个命令行解释器,它读取用户输入,然后执行命令。 shell的用户输入通常来自 于终端(交互式 shell),有时则来自于文件(称为 shell i脚本)。

文件和目录

文件系统

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

文件名

目录中的各个名字称为文件名( filename)。只有斜线线(/)和空字符这两个字符不能出现在 文件名中。斜线用来分隔构成路径名的各文件名,空字符则用来终止一个路径名。

路径名

由斜线分隔的一个或多个文件名组成的序列(也可以斜线开头)构成路径名( pathname),以斜线 开头的路径名称为绝对路径名( absolute pathname),否则称为相对路径名( relative pathname)。

工作目录

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

起始目录

登录时,工作目录设置为起始目录(home directory)

ls(l) 命令的简要实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "apue.h"  //用于本书各个实例 包含某些标准头文件,定义了很多常量和函数模型
#include <dirent.h> // 使用opendir和readdir的函数原型,以及dirent结构的定义

int
main(int argc, char *argv[])
{
DIR *dp;
struct dirent *dirp;

// 给出的参数不符
if (argc != 2)
err_quit("usage: ls directory_name");
// 获取命令行的第一个参数, 打开目录,返回一个DIR结构指针
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);
}

输入和输出

文件描述符

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

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

按惯例,每当运行一个新程序时,所有的 shell i都为其打开3个文件描述符,即标准输入 standard input.)、标准输出( Istandard output)以及标准錯误( standard error)

不带缓冲I/O

函数open、read、write、lseek以及close提供了不带缓冲的I/O

复制任意Unix普通文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
#include "apue.h"

#define BUFFSIZE 4096

int
main(void)
{
int n;
char buf[BUFFSIZE];

// STDIN_FILENO 和 STDOUT_FILENO指定标准输入输出的文件描述符 在POSIX标准,他们分别是0和1
// read函数返回读取的字节数,此值作为写入的字节数传入write函数中
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);
}

标准I/O

标准U/O函数为那些不带缓冲的I/O函数提供了一个带缓冲的接口。使用标准I/O函数无需 担心如何选取最佳的缓冲区大小

程序和进程

程序

程序(program)是存储在磁盘上某个目录中的一个可执行文件

进程和进程ID

程序执行实例被称为进程(process),Unix系统确保每一个进程都有唯一一个数字标识符,称为进程ID(process ID)

进程控制

有3个用于控制进程的主要函数:fork、exec和waitpid

线程和线程ID

shell 基本实施程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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 %) */

// 用标准I/O函数fgets从标准输入中读取一行
while (fgets(buf, MAXLINE, stdin) != NULL) {
// 因为fgets返回的每一行中都以换行符终止,但是execlp函数要求参数以null结束,所以用null替代每一行后面的换行符
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replace newline with null */

// fork创建子进程失败
if ((pid = fork()) < 0) {
err_sys("fork error");
// 子进程处理:调用execlp从命令行输入读入命令,用新的程序文件替换子进程原来执行的程序文件
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
exit(127);
}

/* parent */
// 父进程通过waitpid等待子进程终止,指定等待的进程为参数
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}

出错处理

当UINX系统函数出错时,通常会返回一个负值,而且整型变量 errno通常被设置为具有特定 信息的值。

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

出错恢复

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

用户标识

用户ID

向系统标识各个不同的用户,用户ID为0的用户为根用户(root)或者超级用户(superuser)

组ID

口令文件登录项包括用户的组ID(group ID),为一个数值。

信号

信号( signal)用于通知进程发生了某种情况。进程有以下3种处理信号的方式。
(1)忽略信号。有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的存储单元 等,因为这些异常产生的后果不确定,所所以不推荐使用这种处理方式。
(2)按系统狀认方式处理。对于除数为0,系统默认方式是终止该进程。
(3)提供一个函数,信号发生时调用该函数,这被称为捕捉该信号。通过提供自编的函数, 我们就能知道什么时候产生了信号,并按期望的方式处理它。

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

能够处理终端的shell实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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;

// 调用sinal函数,指定当产生SIGINT信号时,调用sig_int函数
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这个特定时间以来所经过的秒数累计值(早期的手册称UTC为格林尼治标准时间)。这 些时间值可用于记录文件最近一次的修改时间等。 系统基本数据类型 time_t用于保存这种时间值
(2)进程时间。也被称为CPU时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算。每秒钟曾经取为50、60或100个时钟滴答。 系统基本数据类型clock_t保存这种时间值。

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

  • 时钟时间 ,时钟时间又称为墙上时钟时间( wall clock time),它是进程运行的时间总量,其值与系统 中同时运行的进程数有关。
  • 用户CPU时间,用户CPU时间是执行用户指令所用的时间量。
  • 系统CPU时间 ,系统CPU时间是为该进程执行内核程序所经 历的时间。

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

系统调用和函数库

所有的操作系统都有提供多种服务的入口点,这些入口点被称为系统调用(system call)

enter description here