APUE读书笔记(一)
1 UNIX基础
登录
登录UNIX时,依次键入登录名与口令。
口令文件(通常是/etc/passwd
文件)的登录项的7个字段(冒号分隔):登录名、加密口令、数字用户ID、数字组ID、注释字段、起始目录(/home/sar
)以及shell程序。
文件和目录
目录是包含目录项的文件。
POSIX.1限制文件名在此些字符集中:字母(a~z、A~Z)、数字(0~9)、句点(.)、短横线(-)和下划线(_)。
绝对路径名的开头为斜线(/
),反之为相对路径名。
每个进程有一个(当前)工作目录,相对路径名从此处开始解释。
登录时,工作目录设置为起始目录(从口令文件中相应登录项中取得)。
输入和输出
内核用文件描述符(一个小的非负整数)标识一个特定进程正在访问的文件。
每当运行一个新程序,shell会为其打开标准输入、标准输出、标准错误三个文件描述符。
程序和进程
程序为存储在磁盘中的某个可执行文件,而进程是程序的执行实例。
UNIX确保每个进程都有进程ID(唯一的数字标识符、非负整数)。
同一进程中的线程也有线程ID,它们共享同一地址空间、文件描述符、栈以及与进程相关属性。
出错处理
整型变量errno
可以用来存储各种出错信息。
<errno.h>定义两类出错:致命性的(无法执行恢复动作)与非致命性的。
用户标识
用户ID(在口令文件登录项中)是一个向系统标识各个不同用户的数值,用户ID为0的用户是根用户(超级用户)。
组ID(口令文件登录项中)是一个向系统标识各个不同用户组的数值(同组的各个成员共享资源),组文件(通常是/etc/group
)将组名映射为组ID。
信号
信号用于通知进程发生了什么情况(进程可以忽略它或按系统默认方式处理或提供处理函数)。
时间值
- 日历时间:为1970年1月1日00:00:00以来累计的秒数,保存在
time_t
类型中; - 进程时间(CPU时间):用以度量进程使用CPU资源,保存在
clock_t
类型中。
UNIX为一个进程维护3个进程时间值:时钟时间(墙上时钟时间,进程运行的时间总量,包括阻塞与就绪时间)、用户CPU时间(执行用户指令所用的时间量)、系统CPU时间(在内核执行的时间量)。
运行时间 = 用户CPU时间 + 系统CPU时间。
时钟时间 = 运行时间 + 阻塞时间 + 就绪时间。
2 UNIX标准及实现
UNIX标准化
- ISO C
- IEEE POSIX
- Single UNIX Specification(SUS)
- FIPS
UNIX系统实现
- SVR4
- 4.4BSD
- FreeBSD
- Linux
- Mac OS X
- Solaris
限制
UNIX提供的3种限制:
- 编译时限制
- 与文件或目录无关的运行时限制
- 与文件或目录有关的运行时限制
ISO C限制在<limits.h>中。
POSIX限制包括:数值限制、最小值、最大值、运行时可以增加的值、运行时不变值、其他不变值、路径名可变值。
XSI限制包括:最小值、运行时不变值。
确定限制值以及特征选项:
#include <unistd.h> long sysconf(int name); long pathconf(const char *pathname, int name); long fpathconf(int fd, int name); //返回值:成功则返回相应值,出错则返回-1 //name为限制名对应参数
选项
运行一些程序需要先判断当前环境是否支持其依赖的可选选项。
POSIX定义了3种处理选项的方法:
- 编译时选项定义在<unistd.h>中;
- 与文件或目录无关的运行时选项用
sysconf
来判断; - 与文件或目录无关的选项用
pathconf
或fpathconf
来判断。
功能测试宏
常量_POSIX_C_SOURCE
及_XOPEN_SOURCE
被称为功能测试宏。
基本系统数据类型
<sys/types.h>中定义了基本系统数据类型。
3 文件I/O
文件描述符
打开文件并读写:将open
或creat
返回的文件描述符作为参数传递给read
或write
。
标准输入、标准输出、标准错误的文件描述符分别为STDIN_FILENO
、STDOUT_FILENO
和STDERR_FILENO
。
函数open和openat
打开或创建一个文件:
#include <fcntl.h> int open(const char *path, int oflag, ... /* mode_t mode */); int openat(int fd, const char *path, int oflag, ... /* mode_t mode */); //返回值:成功则返回文件描述符,出错则返回-1 //fd为文件描述符 //若path指定的是绝对路径名则fd被忽略,若path指定的是相对路径名则fd指出了相对路径名在文件系统中的开始地址,若path指定了相对路径名且fd为AT_FDCWD则路径名在当前工作目录获取(同之后类似情况) //oflag可以用来说明此函数的多个选项(如读写方式、追加方式、是否存在、检测文件类型、同步读写、是否更新文件属性等,常量定义在<fcntl.h>)
函数creat
创建一个新文件:
#include <fcntl.h> int creat(const char *path, mode_t mode); //返回值:成功则返回为只写打开的文件描述符,出错则返回-1 //等效于:open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
用creat
以只写方式打开所创建的文件,若打开后既要写也要读,便需要先调用close
后再调用open
。
函数close
关闭一个打开文件(会释放该进程在该文件上的所有锁记录):
#include <unistd.h> int close(int fd); //返回值:成功则返回0,出错则返回-1
当一个进程终止时,内核自动关闭它所有的打开文件。
函数lseek
为一个打开文件设置偏移量:
#include <unistd.h> off_t lseek(int fd, off_t offset, int whence); //返回值:成功则返回新的文件偏移量,出错则返回-1 //偏移量offset为字节 /* whence值: SEEK_SET为距文件开始处; SEEK_CUR为其当前值处; SEEK_END为文件长度处 */
如果文件描述符指向的是一个管道、FIFO或网络套接字,则返回-1。lseek
仅将当前的文件偏移量记录在内核中,不引起任何I/O操作。
文件偏移量可以大于文件的当前长度,对该文件的下一次写将加长该文件,并在文件中构成一个空洞(文件中没有写过的字节都被读为0),文件的空洞并不要求在磁盘上占用存储区。
函数read
从打开文件中读数据:
#include <unistd.h> ssize_t read(int fd, void *buf, size_t nbytes); //返回值:读到的字节数,已到文件尾则返回0,出错则返回-1
函数write
向打开文件写数据:
#include <unistd.h> ssize_t write(int fd, const void *buf, size_t nbytes); //返回值:成功则返回已写的字节数,出错则返回-1
write
的返回值通常与nbytes
的值相同,否则表示出错。
I/O的效率
对于UNIX系统内核,文本文件与二进制代码文件没区别。
为改善系统,文件系统使用预读技术,所以要选取合适大小的缓冲区。
文件共享
UNIX系统支持在不同进程间共享打开文件。
内核使用3种数据结构表示打开文件:
- 一个进程在进程表中有一个存放文件描述符(即文件描述符标志与文件指针的映射)的表;
- 文件指针指向文件表项(包含文件状态标志、当前文件偏移量、V节点指针);
- v节点指针指向v节点表项(包含文件相关信息及i节点)。
打开一个文件就会拥有一个独立的文件描述符、文件表项,但是v节点以及i节点对于同一文件来说只有一份。
原子操作
原子操作指的是由多步组成的一个操作,若执行则要么执行完所有步骤要么一步也不执行。
“先定位到文件尾端然后写”的原子操作是用open
时设置O_APPEND
。
原子性地定位并执行I/O:
#include <unistd.h> ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset); //返回值:读到的字节数,已到文件尾则返回0,出错则返回-1 ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset); //返回值:成功则返回已写的字节数,出错则返回-1
调用pread
/pwrite
相当于调用lseek
后调用read
/write
,但是有区别:1)调用pread
/pwrite
时无法中断其定位和读/写操作,2)不更新当前文件偏移量。
“检查文件是否存在”的原子操作是用open
时设置O_CRAET | O_EXCL
。
函数dup和dup2
复制一个现有文件的文件描述符(使得文件指针指向同一文件表项):
#include <unistd.h> int dup(int fd);//新文件描述符一定是当前可用文件描述符中的最小数值 int dup2(int fd, int fd2);//若fd2已经打开则先将其关闭,若fd与fd2不相等则不关闭fd2且其FD_CLOEXEC被清除(fd2在exec时为打开状态) //返回值:成功则返回新的文件描述符,出错则返回-1
复制一个描述符的另一种方法是使用fcntl
:
dup(fd); //即 fcntl(fd, F_DUPFD, 0); dup2(fd, fd2); //即 close(fd2); fcmtl(fd, F_DUPFD, fd2); //dup2是原子操作,且其有些不同的errno
函数sync、fsync和fdatasync
延迟写:向文件写入数据时,内核通常先将数据复制到缓冲区中然后排入队列,晚些时候再写入磁盘。
刷新缓冲区(保证磁盘上实际文件系统与缓冲区中内容的一致性):
#include <unistd.h> int fsync(int fd);//刷新磁盘中特定文件,等待实际写磁盘结束才返回 int fdatasync(int fd);//与fsync类似但不更新文件属性 //返回值:成功则返回0,出错则返回-1 void sync(void);//将修改过的块缓冲区排入写队列,不等待实际写磁盘结束就返回,通常被update守护进程周期性调用
函数fcntl
关于文件的瑞士军刀(复制一个已有的描述符、获取/设置文件描述符标志或文件状态标志或异步I/O所有权或记录锁):
#include <fcntl.h> int fcntl(int fd, int cmd, ... /* int arg */); //返回值:若成功,则依赖于cmd;若出错,返回-1
oldFlag |= addFlag
可以开启addFlag
,oldFlag &= ~deleteFlag
可以关闭deleteFlag
。
函数ioctl
I/O操作的杂物箱(设备驱动程序中对设备的I/O通道进行管理):
#include <unistd.h>/* System V */ #include <sys/ioctl.h>/* BSD and Linux */ int ioctl(int fd, int request, ...); //返回值:出错则返回-1,成功则返回其他值
/dev/fd
打开文件/dev/fd/n
等效于复制描述符n
。
fd = open("/dev/fd/0", mode); //即 fd = dup(0);
/dev/fd
文件主要由shell使用。