前言
张老师推荐阅读的两本书之一,打算用一个月到两个月之间的时间,初步过完《UNIX环境高级编程》与《UNIX网络编程第三版第一卷》,之后继续阅读其他关于UNIX底层的书籍。
一、UNIX基础知识
所有操作系统都提供基础的服务:执行新程序、打开文件、读文件、分配存储区、获取当前时间等。
1.1 UNIX 体系结构
- 操作系统控制计算机硬件资源,提供程序运行环境的“软件”称为操作系统的内核。内核的接口称为系统调用。
- 公共函数库建立在系统调用的接口之上;应用程序可以使用公共函数库,也可以使用系统调用。
- shell 是一个特殊的应用程序,为运行其他应用程序提供接口。
1.2 文件和目录
- 文件系统
UNIX文件系统是目录和文件的一种层次结构,所有的起点为根目录,名称为字符“/”。
目录:包含目录项的文件,逻辑上认为目录项包含文件名和文件属性信息。文件属性:文件类型(普通文件 or 目录)、文件大小、文件所有者、文件权限、文件最后修改时间等。
stat和fstat函数返回包含所有文件属性的一个信息结构。
- 文件名
斜线(/)和空字符不允许出现在文件名中,创建目录会自动创建两个文件名:(.)指向当前目录和(..),在最高层次的根目录中,二者相同。
- 路径名
斜线开头路径为绝对路径,否则为相对路径。根名字是特殊的绝对路径名,不包含文件。
- 工作目录
所有相对路径从当前目录开始解释。进程可以使用chdir函数更改其工作目录。
1.3 输入输出
- 文件描述符
文件描述符为一个小的非负整数,内核用于标识一个特定进程正在访问的文件。当内核打开现有文件时或创建新文件时,会返回文件描述符。
- 标准输入、标准输出、标准错误
每当运行新的程序时,所有的shell都会为其打开3个文件描述符,即标准输入、标准输出、标准错误。一般情况下,三个描述符的链接都指向终端,可以使其中一个或三个重定向到某个文件。例如
# 执行 ls 命令,将输出重定向到名为 file.txt 的文件
ls > file.txt
- 缓冲的IO
函数 open、read、write、lseek 以及 close提供了不带缓冲区的IO,这些函数都使用文件描述符。
- 标准IO
标准IO为不带有缓冲的IO函数提供了一个带缓冲的接口。使用标准IO函数无需担心选择最佳缓冲区的大小。
1.4 程序和进程
程序:程序是存储在磁盘的某个目录中的可执行文件。内核使用exec函数,将程序读入内存,并执行程序。
进程和进程ID
程序的执行实例为进程(process),UNIX系统确保每个进程有唯一的数字标识符,称为进程ID,为非负整数。
- 进程控制
3个主要函数:fork,exec,waitpid。
fork:父进程通过调用 fork 函数创建一个新的运行的子进程。
- 子进程得到与父进程用户级虚拟地址空间相同但独立的副本,包括代码和数据段、堆、共享库和用户栈,但是私有地址空间不同。
- 子进程获得与父进程任何打开文件描述符相同的副本,即子进程可以读写父进程中打开的任何文件。
- 父子进程最大的区别在于有着不同的PID。
- fork函数调用一次,返回两次,父进程中,fork函数返回子进程的PID;在子进程中,返回0。因为子进程的PID永远非0,可以用于区分是父进程还是子进程。
- 线程和线程ID
通常,一个进程只有一个控制线程,某一时刻执行的一组机器指令。一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。各线程在访问共享数据时需要采取同步措施避免不一致性。
进程ID只在其所属的进程内起作用,一个进程中的线程ID在另一个进程中没有意义。可以使用线程ID对进程内的特定线程进行操作。
1.5 出错处理
UNIX系统出错后一般会返回一个负值,整形变量 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);
#include <stdio.h>
void perror(const char *msg);
strerror 将 errnum 映射为出错消息字符串,并返回字符串指针。 perror 函数基于 errno 当前值,在标准错误上产生一条出错信息,然后返回。
二、UNIX标准
三、文件IO
3.1 文件描述符
对内核而言,所有打开的文件都通过文件描述符引用。文件描述符为非负整数。
打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。读、写一个文件时,使用 open 或 creat 返回的文件描述符标识该文件,将其作为参数传递给 read 或 write。
文件描述符的变化范围为$0 \sim OPEN\_MAX-1$,早期UNIX的上限为19,即打开19个文件。对于FreeBSD 8.0、Linux 3.2.0,MacOS X和Solaris 10,文件描述符范围只受制于存储器总量、整型的字长以及系统管理员所配置的软权限和硬权限。
3.2 open opanat
调用函数open或 openat可以打开或创建文件,两函数成功调用,则返回文件描述符,出错则返回-1。
#include <fcntl.h>
int open(const char *path, int oflag, ... /* mode_t mode*/);
int openat(int fd, const char *path, ... /* mode_t mode*/);
最后一个参数可变,open函数仅当创建新文件时才使用最后一个参数。path为打开或创建的文件名字,oflag用于指定函数的多个选项。
3.3 creat
调用 creat 函数创建新文件。
#include <fcntl.h>
int creat(const char *path, mode_t mode);
等效于
open(path, O_WRONLT | O_CREAT | O_TRUNC, mode);
creat的不足是以只写道德形式打开文件。在提供open的新版本之前,如果需要临时创建文件,并且要先写再读,则必须先调用 creat、close,再调用 open。现在可以使用新版的 open实现:
open(path, O_RDWR | O_CREAT | O_TRUNC, mode);
3.4 close
调用close函数关闭一个文件:
#include <unistd.h>
int close(int fd);
关闭一个文件也将释放该进程加在该文件上的所有记录锁。当进程终止时,内核会自动关闭其打开的所有文件。可以使用这种方法,不需要显式地调用 close 关闭文件。
3.5 lseek
每个打开文件都有与之关联的“当前文件偏移量”,通常为非负整数,度量从文件开始处计算的字节数。通常,读写操作都是从当前文件偏移量开始,并使得偏移量增加读写字节数。当打开文件时,除非指定 O_APPEND
选项,否则该偏移量设置为0。
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
对参数 offset 的解释和参数 whence 有关,
- whence 为
SEEK_SET
,则将该文件的偏移量设置为距文件开始处 offset 字节 - whence 为
SEEK_CUR
,则将该文件的偏移量设置为其当前值加 offset 字节,offset 可正可负 - whence 为
SEEK_END
,则将该文件的偏移量设置为文件长度加 offset 字节,offset 可正可负
若 lseek 执行成功,返回新的文件偏移量,可用下列方式确定打开文件的当前偏移量:
off_t currpos;
currops = lseek(fd, 0, SEEK_CUR);
也可以用来检验所涉及的文件是否设置偏移量。如果该文件描述符指向的是管道、FIFO或socket,则lseek返回-1,并将 errno 设置为 ESPIPE。
- 通常,文件的当前偏移量为非负整数,但是某些设备也允许负的偏移量。对于普通文件,偏移量必须为非负值。
- 文件偏移量可以大于当前文件长度,此时,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,位于文件中但是没有写过的字节都被读为0.
- 文件的空洞并不要求在磁盘上占用存储区,具体处理方式与文件系统有关,当定位到
四、文件和目录
4.1 函数 stat、fstat、fstatat、lstat
#include <sys/stat.h>
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);
// 所有四个函数的返回值,成功返回0,出错返回-1
给出pathname后,stat函数会返回与此命名文件有关的信息结构。fstat函数获得已在描述符fd上打开文件的有关信息。lstat函数类似于stat,但是当命名的文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是该符号链接引用的文件信息。
fstatat 函数为一个相对于当前打开目录的路径名返回文件统计信息。flag参数控制着是否紧跟一个符号链接,
4.2 文件类型
- 普通文件
除了二进制可执行文件外,其他文件的形式对于UNIX系统来说并无区别。二进制可执行文件为了执行,内核必须理解其格式,所有二进制可执行文件都遵循一种标准格式,使得内核能确定程序文本和数据的加载位置。
- 目录文件
包含其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件有读权限的任一进程都可以读该目录的内容,但是只有内核可以直接写目录文件。
- 块特殊文件
提供对设备带缓冲的访问,每次访问以固定长度为单位进行。
- 字符特殊文件
提供对设备不带缓冲的访问,每次访问长度可变。
- FIFO
用于进程间通信,有时也称为命名管道(named piped)
- socket 套接字
进程间网络通信,也可用于一台主机上的非网络通信。
- 符号链接
这种类型的文件指向另一个文件。
4.3 设置用户ID和设置组ID
与一个进程相关联的ID一般有6个
- 实际用户ID
- 实际组ID
- 有效用户ID
- 有效组ID
- 附属组ID
- 保存的设置用户ID
- 保存的设置组ID
1和2在登录时取自口令文件中的登录项。