UNIX环境高级编程阅读笔记


前言

张老师推荐阅读的两本书之一,打算用一个月到两个月之间的时间,初步过完《UNIX环境高级编程》与《UNIX网络编程第三版第一卷》,之后继续阅读其他关于UNIX底层的书籍。

一、UNIX基础知识

所有操作系统都提供基础的服务:执行新程序、打开文件、读文件、分配存储区、获取当前时间等。

1.1 UNIX 体系结构

  1. 操作系统控制计算机硬件资源,提供程序运行环境的“软件”称为操作系统的内核。内核的接口称为系统调用
  2. 公共函数库建立在系统调用的接口之上;应用程序可以使用公共函数库,也可以使用系统调用。
  3. shell 是一个特殊的应用程序,为运行其他应用程序提供接口。

1.2 文件和目录

  1. 文件系统

UNIX文件系统是目录和文件的一种层次结构,所有的起点为根目录,名称为字符“/”。

目录:包含目录项的文件,逻辑上认为目录项包含文件名和文件属性信息。文件属性:文件类型(普通文件 or 目录)、文件大小、文件所有者、文件权限、文件最后修改时间等。

stat和fstat函数返回包含所有文件属性的一个信息结构。

  1. 文件名

斜线(/)和空字符不允许出现在文件名中,创建目录会自动创建两个文件名:(.)指向当前目录和(..),在最高层次的根目录中,二者相同。

  1. 路径名

斜线开头路径为绝对路径,否则为相对路径。根名字是特殊的绝对路径名,不包含文件。

  1. 工作目录

所有相对路径从当前目录开始解释。进程可以使用chdir函数更改其工作目录。

1.3 输入输出

  1. 文件描述符

文件描述符为一个小的非负整数,内核用于标识一个特定进程正在访问的文件。当内核打开现有文件时或创建新文件时,会返回文件描述符。

  1. 标准输入、标准输出、标准错误

每当运行新的程序时,所有的shell都会为其打开3个文件描述符,即标准输入、标准输出、标准错误。一般情况下,三个描述符的链接都指向终端,可以使其中一个或三个重定向到某个文件。例如

# 执行 ls 命令,将输出重定向到名为 file.txt 的文件
ls > file.txt
  1. 缓冲的IO

函数 open、read、write、lseek 以及 close提供了不带缓冲区的IO,这些函数都使用文件描述符。

  1. 标准IO

标准IO为不带有缓冲的IO函数提供了一个带缓冲的接口。使用标准IO函数无需担心选择最佳缓冲区的大小。

1.4 程序和进程

  1. 程序:程序是存储在磁盘的某个目录中的可执行文件。内核使用exec函数,将程序读入内存,并执行程序。

  2. 进程和进程ID

程序的执行实例为进程(process),UNIX系统确保每个进程有唯一的数字标识符,称为进程ID,为非负整数。

  1. 进程控制

3个主要函数:fork,exec,waitpid。

fork:父进程通过调用 fork 函数创建一个新的运行的子进程。

  • 子进程得到与父进程用户级虚拟地址空间相同但独立的副本,包括代码和数据段、堆、共享库和用户栈,但是私有地址空间不同。
  • 子进程获得与父进程任何打开文件描述符相同的副本,即子进程可以读写父进程中打开的任何文件。
  • 父子进程最大的区别在于有着不同的PID。
  • fork函数调用一次,返回两次,父进程中,fork函数返回子进程的PID;在子进程中,返回0。因为子进程的PID永远非0,可以用于区分是父进程还是子进程。
  1. 线程和线程ID

通常,一个进程只有一个控制线程,某一时刻执行的一组机器指令。一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。各线程在访问共享数据时需要采取同步措施避免不一致性。

进程ID只在其所属的进程内起作用,一个进程中的线程ID在另一个进程中没有意义。可以使用线程ID对进程内的特定线程进行操作。

1.5 出错处理

UNIX系统出错后一般会返回一个负值,整形变量 errno 具有特定信息的值。在支持线程的环境中,多个线程共享地址空间,每个线程有属于其自己的局部errno,避免一个线程干扰另一个线程。

Linux支持多线程存取 errno,定义为:

extern int *__errno_location(void);
#define errno (*__errno_location())

注意规则有二:

  1. 若没有出错,则 errno 的值不会被例程清除。所以,只有当函数的返回值明确出错时,才会校验其值。
  2. 任何函数都不会将 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。

  1. 通常,文件的当前偏移量为非负整数,但是某些设备也允许负的偏移量。对于普通文件,偏移量必须为非负值
  2. 文件偏移量可以大于当前文件长度,此时,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,位于文件中但是没有写过的字节都被读为0.
  3. 文件的空洞并不要求在磁盘上占用存储区,具体处理方式与文件系统有关,当定位到

四、文件和目录

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 文件类型

  1. 普通文件

除了二进制可执行文件外,其他文件的形式对于UNIX系统来说并无区别。二进制可执行文件为了执行,内核必须理解其格式,所有二进制可执行文件都遵循一种标准格式,使得内核能确定程序文本和数据的加载位置。

  1. 目录文件

包含其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件有读权限的任一进程都可以读该目录的内容,但是只有内核可以直接写目录文件

  1. 块特殊文件

提供对设备带缓冲的访问,每次访问以固定长度为单位进行。

  1. 字符特殊文件

提供对设备不带缓冲的访问,每次访问长度可变。

  1. FIFO

用于进程间通信,有时也称为命名管道(named piped)

  1. socket 套接字

进程间网络通信,也可用于一台主机上的非网络通信。

  1. 符号链接

这种类型的文件指向另一个文件。

4.3 设置用户ID和设置组ID

与一个进程相关联的ID一般有6个

  1. 实际用户ID
  2. 实际组ID
  3. 有效用户ID
  4. 有效组ID
  5. 附属组ID
  6. 保存的设置用户ID
  7. 保存的设置组ID

1和2在登录时取自口令文件中的登录项。


文章作者: Demerzel
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Demerzel !
评论
  目录