APUE笔记-第一章 UNIX基础知识
最近在找工作, 额, 从七月份一直休息到九月初, 开始找, 结果发现快到十月了, 节点不是很好, 要过两个节, 所以估计入职什么的要到节后了>_<#
这几个月也在思考一些东西, 顺手也解决掉了Python源码剖析
, 目前在逐步梳理笔记
发现自己首次做笔记还是太杂太乱, 堆在wiki里面一大坨的感觉, 还是要梳理画图, 思路更清晰些, 静候吧, 还是十篇左右的样子
读APUE, 做法比较残忍
, 把书切开, 拆成一章一章地装订, 方便携带和阅读, 最后发现画满了一堆东西
发现还是不方便自己查阅, 所以还是决定重读, 转成笔记, 放到博客上方便搜索/查阅
对了, 代码之前只是下了看到的时候run下, 这次重读写写注释, 放到github了, 链接
在此感谢作者W.Richard Stevens :) 这本书五星好评, 建议如果搞linux相关后端, 可以读下
第一章 UNIX基础知识
Unix体系结构: 内核与系统调用

内核(kernel): 严格意义上, 将操作系统定义为一种软件, 它控制计算机硬件资源, 提供程序运行环境(相对较小, 位于环境的中心)
系统调用(system call): 内核的接口
关系: 公用函数库构建在系统调用接口之上, 应用软件既可以使用公用函数库, 也可以使用系统调用
登陆
口令文件/etc/passwd
, 七个字段,冒号分隔
登录名:加密口令:数值用户 ID: 数值组 ID: 注释字段:起始目录:shell
加密口令已经转移到另一个文件
shell: 一个命令行解释器, 它读取用户输入, 然后执行命令
Linux 默认shell是Bourne-again shell
文件和目录
文件系统: 目录和文件组成的一种层次接口, 目录的起点称为根(root), 其名字是\\
目录(directory)是一个包含许多目录项的文件
文件名(filename): 不能出现斜线/
和空操作符null
(好的习惯只使用印刷字符的一个子集作为文件名字符)
创建目录时, 会自动创建两个文件名, 当前目录.
以及父目录..
路径名(pathname): 一个或多个以斜线分割的文件名序列. 以斜线开头的是绝对路径(absolute pathname), 否则是相对路径(relative pathname)
#include "apue.h"
#include <dirent.h>
// 可编译执行
// apue.h, 包含某些标准头文件, 定义了很多常量及库函数
int
main(int argc, char *argv[])
{
DIR *dp;
// 结构体
struct dirent *dirp;
// 需要至少一个参数
if (argc != 2)
// apue.h自定义函数
err_quit("usage: ls directory_name");
// 赋值后判断, opendir返回指向 DIR 结构体的指针
if ((dp = opendir(argv[1])) == NULL)
// apue.h自定义函数err_sys
err_sys("can't open %s", argv[1]);
// 赋值后判断, 读每一项, 返回指向readdir结构的指针或null(没有目录项可读时)
while ((dirp = readdir(dp)) != NULL)
// 取出每个目录的名字
printf("%s\n", dirp->d_name);
closedir(dp);
// 终止程序, 0正常结束, 1-255出错
exit(0);
}
工作目录(working directory), 每个进程都有一个, 优势成为当前工作目录, 进程可以通过chdir函数更改其工作目录.
输入和输出
文件描述符(file descriptor), 一个小的负整数, 内核使用它标识一个特定进程正在访问的文件. 当内核打开或创建一个新文件时, 返回一个文件描述符, 在读写的时候使用
每当运行一个新程序时, 所有的shell都为其打开三个文件描述符: 标准输入(standard input), 标准输出(standard output)以及标准错误(standard error)
不用缓冲的I/O 函数open/read/write/lseek/close
#include "apue.h"
/* 缓冲区大小, 常量 */
#define BUFFSIZE 4096
int
main(void)
{
int n;
char buf[BUFFSIZE];
/* STDIN_FILENO/STDOUT_FILENO -> apue.h -> unisted.h, 标准输入文件描述符0/标准输出文件描述符1 */
/* 从标准输入读, read返回读得的字节数, 读到末端返回0, 发生错误返回-1 */
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函数: 提供了一种对不用缓冲 I/O 函数的带缓冲接口, 可以无需担心如何选取最佳的缓冲区大小
#include "apue.h"
/* stdin/stdout -> apue.h -> stdio.h 标准输入文件/标准输出文件 */
/* EOF为stdio.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);
}
程序和进程
程序(program): 存放在磁盘上, 处于某个目录中的一个可执行文件.(使用6个exec函数中的一个有内核将程序读入存储器, 并使其执行)
进程(process): 程序的执行实例
进程ID(process ID), 每个进程都有一个唯一的数字标识符, 总是一非负整数
#include "apue.h"
int
main(void)
{
/* getpid得到进程pid */
printf("hello world from process ID %ld\n", (long)getpid());
exit(0);
}
进程控制: 三个主要函数, fork/exec(六种变体)/waitpid
#include "apue.h"
#include <sys/wait.h>
/* fork创建一个新进程, 它被调用一次(由父进程调用), 返回两次(在父进程中返回子进程的进程ID, 在子进程中返回0) */
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 */
/* 执行读入的命令 */
/* fork创建一个子进程, 返回<0则表示fork发生了错误 */
if ((pid = fork()) < 0) {
err_sys("fork error");
/* 对于子进程, fork返回的pid=0(父进程fork返回的pid>0) */
} else if (pid == 0) { /* child */
/* 调用execlp以执行从标准输入读入的命令 */
/* fork+exec组合, 是某些操作系统所称的产生(spawn)一个新的进程 */
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
/* 退出 */
exit(127);
}
/* 父进程, 等待子进程终止 */
/* pid为子进程id, status为子进程终止状态(用于判断其实如何终止的) */
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
通常, 一个进程只有一个控制线程(thread), 同一时刻只执行一组机器指令.(对于某些问题, 如果不同部分各使用一个控制线程, 那么整个问题解决相对容易, 多个控制线程也能充分利用多处理器系统的并行性)
在一个进程内的所有线程共享同一地址空间/文件描述符/栈以及与进程相关的属性(所以各线程在访问共享数据时需要采取同步措施以避免不一致性)
线程也用 ID 标识, 但只在其所属进程内起作用
出错处理
UNIX 函数出错的时候, 常常返回一个负值, 而整型变量errno通常被设置为含有附加信息的一个值
errno.h
中, 定义了符号errno以及可以赋予它的各种常量.(errno(3)手册中)
对于errno两条规则
1. 如果没有出错, 其值则不会被一个例程清楚, 因此, 仅当函数的返回值指明出错时, 才检验其值
2. 任一函数都不会将errno值设置为0, 在errnoh中定义的所有常量都不为0
#include "apue.h"
#include <errno.h>
/* strerror, 将errnum映射为一个出错信息字符串, 并且返回此字符串的指针 */
/* perror, 基于errno的当前值, 在标准出错上产生一条出错信息, 然后返回 */
int
main(int argc, char *argv[])
{
/* 常量 EACCES / ENOENT */
fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
errno = ENOENT;
perror(argv[0]);
exit(0);
}
出错恢复: errno.h中定义的各种出错分为致命性和非致命性两类.
致命性出错: 无法执行恢复动作, 最多只能在屏幕上打印一条出错信息, 或写入日志, 然后终止
非致命性出错: 可以较为妥善地处理
用户标识
用户ID(user ID), 数值, 系统中标识各个不同的用户, 每个用户唯一(用户不能更改其用户 ID)
用户 ID 为0的用户为根( root) 或超级用户(superuser)
组ID(group ID), 一个数值, 指定用户登陆名时同时指定的. 允许同组各个成员之间共享资源
组文件将组名映射为数字 ID, /etc/group
口令文件包含: 登录名 = 用户 ID 的映射
组文件包含: 组名 = 组ID 的映射
打印用户ID和组ID
#include "apue.h"
int
main(void)
{
/* getuid / getgid */
printf("uid = %d, gid = %d\n", getuid(), getgid());
exit(0);
}
附加组ID
允许一个用户属于多个组, 最多16个.
信号
信号(signal): 通知进程已发生某种情况的一种技术.
e.g. 一个进程执行除法操作, 其除数为0, 则将名为SIGFPE的信号发给该进程
进程如何处理信号?
1. 忽略该信号
2. 按系统默认方式处理.
3. 提供一个函数, 信号发生时则调用该函数(捕捉信号)
信号捕捉
#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;
/* signal函数, 指定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系统两种不同的时间值
日历时间, time_t, 从1970年1月1日00:00:00以来的国际标准时间UTC锁经过的秒数
进程时间, clock_t, CPU时间, 度量进程使用的中央处理器资源, 以始终滴答计算
Unix系统使用三个进程时间
1. 时钟时间, 总时间, real
2. 用户cpu时间, 执行用户指令耗时, user
3. 系统cpu时间, 执行内核程序耗时, sys
系统调用和库函数
所有操作系统都提供多种服务的入口点(系统调用), 程序由此想内核请求服务
UNIX所使用的技术是为每个系统调用在标准 C 库中设置一个具有同样名字的函数. 用户进程用标准 C 调用序列来调用这些函数, 然后函数又用系统所要求的技术调用相应的内核服务
系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。

系统调用: 最小接口, 单一职责, 不可替换
C库函数: 复杂功能, 可替换, 可自行定义