Chapter 1. UNIX System
Overview UNIX
结构: 操作系统是控制硬件资源和提供程序运行环境的一种软件。 系统调用是访问内核的接口。 Shell是特殊的应用程序,提供运行其他程序的接口。 登入: 用户名,/etc/passwd, shell 文件和目录: 文件系统,文件名,路径名,工作目录,Home目录 输入和输出: 文件描述符,标准输入、输出、错误,unbuffered I/O,标准I/O 程序和进程: 程序是磁盘上的可执行文件,读入到内存中并由内核执行。 进程,进程ID,进程控制(fork, exec, waitpid),线程和线程id(id只在本进程内有效) 错误处理: 当函数执行错误发生时,将返回一个负值,并设置errno为一个整数以提供额外的信息。strerror和perror打印错误信息。 错误定义可以分为两类:致命和非致命的错误。非致命错误一般是暂时的,可以恢复,如缺少资源,就可以延迟一段时间再重试。 用户身份: 用户id,组id,补充组id(一个用户可以属于多个组中) 信号: 用来通知进程有情况发生的一种技术。进程收到信号后有三种选择1,忽略
2,默认处理方法 3,自己提供一个函数当信号发生时调用。 时间值: 有两种:1,日历时间time_t 2,进程时间clock_t( clock time, user
cpu time, system cpu time) 日历时间可用来记录文件最后修改的时间等。 Clock
time是此进程开始执行到终止的时间,包括了进程的切换,以及其他进程运行的时间片。 系统调用和库: 系统调用是在内核空间中运行。库封装了系统调用,运行在用户空间。 Chapter 2. UNIX
Standardization and Implementation UNIX标准化: ISO
C, IEEE POSIX,Single UNIX
Specification(POSIX的超集) UNIX系统的实现: System
V (AT&T) BSD (Berkeley) Linux Mac
OS(core is Darwin,
combination of Mach kernel and FreeBSD) Solaris
(Sun, based on System V) 限制: 由于不同系统的实现定义了很多magic numbers和常量。为了增强可移植性,需要两类的限制1,编译时的限制2,运行时的限制。处理方法分为三种:1,编译时的限制(headers) 2,运行时和文件、目录无关的限制(sysconf函数)3,运行时和文件、目录相关的限制(pathconf和fpathconf函数) 选项: 同样为了不同系统的代码可移植性. 1. Compile-time options are
defined in <unistd.h>. 2. Runtime options that are
not associated with a file or a directory are identified with the sysconf function. 3. Runtime options that are
associated with a file or a directory are discovered by calling either the pathconf or the fpathconf function. 特性测试宏: 测试系统是否支持某种特性的宏定义,如__STDC__ 原语系统数据类型: <sys/types.h>中定义的数据类型。如time_t, off_t, pid_t等。 标准间的冲突: 为了兼容性,很多POSIX的系统实现了ISO C的函数。 Chapter 3. File I/O 文件描述符: 打开文件、创建文件时返回的值,用来读、写、定位文件的标识。 open,creat,close函数: 好像没什么好说的。要注意open时一些特殊的文件状态标记,如O_SYNC,O_NOCTTY等。 lseek函数: 定位文件,可以产生空洞文件。定位可以从头,从尾,从当前位置开始。 Read,write函数: 没啥好说的。I/O效率根据自己设定的buffer大小而不同。因此,调用read,write的次数不同,耗去的系统cpu时间不同。测试时,第一次和之后的测试效率是不同的,因为cache的存在。 文件共享: 文件共享是指不同进程间打开文件的共享。内核通过三种数据结构来表示打开的文件,1,每个进程有个进程表项,表中是打开文件的描述符向量。每个向量包含了文件描述符标记和一个指向文件表项的指针;2,内核为所有打开的文件维护一个文件表,每个文件表项包括了a,文件状态标记,如读、写、添加、非阻塞等b,当前的文件偏移量c,指向文件v-node表项的指针;3,每个打开文件或设备都有一个v-node结构 ,包含文件类型和指向操作文件的函数的指针。 当两个以上独立进程打开同一个文件实现文件共享时,内核维护不同的文件表项,就是两个以上进程表项中的文件表项指针指向不同的文件表项,而不同的文件表项中的v-node指针指向同一个v-node而实现文件共享。由于每个进程有自己的打开文件表项,所以有自己的文件打开状态以及文件偏移量。 原子操作: 为了解决多个进程间共享文件时读写不一致的问题。如多个进程对同一个文件添加内容,如多个进程创建同一个文件,如lseek后的write,当多个进程同时执行时就会使文件写的内容不是预期的。 函数pread,pwrite是把lseek和read/write作为原子操作来实现,避免上述问题。 Dup和dup2函数: 复制描述符,dup是将文件描述符复制到最小可用的描述符,让最小可用的描述符指向原描述符所指向的文件表项。 dup2是将指定的文件描述符指向原描述符所指向的文件表项。如果指定的描述符已打开则先关闭。 Sync,fsync,fdatasync函数: 延迟写是将排队在buffer或page
cache中的数据过一些时间再写到磁盘中。当再次用到这些cache时再写到磁盘中。 提供这些函数是将数据立即写到磁盘中,保持文件系统的一致性。Sync和fsync的区别是,前者不等待数据写到磁盘完成即返回,而后者等待完成后才返回。前者是写入cache中的所有数据,系统中的update是daemon进程,它调用sync,并30s中刷新一次。而后者是指定单个文件刷新数据,多用于数据库的更新。Fdatasync和fsync类似,但是它只刷新数据部分,而不更新文件的属性。 fcntl函数: 作用是能够改变打开文件的属性。根据参数不同主要有五种用途: 1, 复制已存在的描述符 2, 获得和设置文件描述符标记 3, 获得和设置文件状态标记 4, 获得和设置异步I/O的所属者 5, 获得和设置记录锁 注意的是:我们在设置文件描述标记或文件状态标记时,不能只是F_SETFD 或 F_SETFL的fcntl的调用,而是要先获得值再设置值。 文件状态标记O_SYNC,表示同步写,每个write要等待数据写到磁盘上才返回。而在通常情况下write只是将要写的数据排队,某个时刻后才写到磁盘上。 还提到fcntl这样的用处,fcntl可以设置shell打开的标准输出输入的属性,因为它只要知道文件描述符就能做到文件属性的改变。 Iocntl函数: 主要用于终端的I/O和提供本章中提到的函数无法实现的功能。如对磁带的读写和定位的操作。 /dev/fd: 打开/dev/fd/n文件就等于是对文件描述符dup(n),假定n是已打开的描述符。 即open(”/dev/fd/n”,
mode) ==
dup(n) Chapter 4. Files and
Directories stat,
fstat, lstat函数: 获得磁盘上文件的结构信息,由结构stat描述。这些信息应该是从文件系统中的inode结构读出的。包括:文件类型、访问权限、大小、所属用户id等等信息。 文件类型: 普通文件,目录文件,符号链接,字符设备、块设备、socket、FIFO。 Set-User-ID和Set-Group-ID: 每个进程有六个和他相关联的id,真实用户id、有效用户id、保存设置用户id以及三个对应的组id。注意的是这是与进程相关的,以前总是弄混。 真是用户id是用户登录时使用的id。 一般,有效用户id=真是用户id。例外是在文件模式字st_mode中置位一个特殊标记set-user-id,那么有效用户id就是此进程所执行的文件所属者的用户id。有用的是,判断一个文件是否可以被进程访问,是根据进程的有效用户id是否等于文件所属的用户id来的。举例:普通用户用程序passwd来修改密码时,会访问到root用户的文件/etc/passwd和/etc/shadow。为什么能成功,是因为passwd是set-user-id程序并且所属是root,当普通用户执行时,它的有效用户id就变成root,就可以访问上面两个文件。 新建文件和目录的所属者: 新文件的用户id是进程的有效用户id。 而新文件的组id有两种说法 1.进程的有效组id;2.
文件所在的目录的组id。具体实现,每个系统有所不同。 access函数: 它的用处是检测真实用户id的访问文件的权限,而不管它是否是set-user-id程序。 umask函数: 创建文件时的屏蔽位,设置创建的文件的访问权限。shell中也有umask命令。 chmod和fchmod函数: 改变已有文件的访问权限。 Sticky
bit: 作用是在可执行程序在第一次执行时,程序的代码段text将复制到swap空间中。在下次执行时,此程序load到内存中就很快。如今的unix系统,虚拟内存以及快速文件系统的出现,这项技术并不需要了。 chown,
fchown, and lchown
Functions: 改变文件的所属者。大多数系统中,只有root能改变文件的所属者。Whether the owner can be changed depends
on the different systems. 文件大小: stat结构中的st_size包含文件的字节大小。但是只对普通、目录、符号链接文件有意义。 对于普通文件,大小为0是允许的。第一次读就得到end-of-file。 对于目录文件,大小通常是16或512的倍数。 对于符号链接文件,大小是被链接的文件路径名长度。此外,它不包含C语言中字符串的终止空字符。 stat结构中的st_blksize为文件I/O时所使用的块大小。 stat结构中的st_blocks为文件所占的实际磁盘块数。 文件中的洞,当文件中有洞时,通过ls –l core显示时,会包含洞所占的字节数。而用du
–s core则显示文件实际所占的磁盘块数。当用read读文件中洞的内容时,得到的是0(不是‘ 文件截断: truncation,ftruncation函数。例外:当截断的长度大于原有文件的长度时,效果依赖于具体的系统。符和XSI的系统文件变长,而变长的部分read出内容是0. 文件系统: 磁盘可以有多个分区,每个分区可以是不同的文件系统。文件系统主要包含部分:boot block,super
block,cylinder
group0~n。 每个cylinder group中包含:super block copy,i-node
map,block
bitmap,
i-nodes(一个文件/目录一个), data/directory blocks(存放文件/目录的数据)。 对于普通文件,i-node指向了所属文件的数据块。而目录文件,i-node则指向了所属目录的directory
blocks。 每个directory block中的主要内容是目录项所包含的文件的i-node号以及文件名。当目录项包含这样的信息时,被包含的文件的i-node中stat的成员st_nlink即链接数就增1。这种链接就是硬链接,通过ln命令实现,即创建一个新目录项指向已有的文件。而只有当链接数为0的时候,文件才被删除。 修改文件名如mv命令,是添加一个新目录项指向已有文件,再unlink掉原目录项,而不需要移动文件的实际内容。 叶目录(目录中不包含任何其他目录)的链接数是2,分别是目录中dot,以及父目录对它的指向。在叶目录增加一个子目录,则叶目录的链接数就增1,因为新增子目录的dotdot指向它。 link,unlink,remove,rename函数: 通过link创建一个新的目录项指向一个文件,并使链接数增1。这样多个目录项就可以指向同一个文件。大多数的系统不允许对目录硬链接,因为会产生文件系统中的loop。 unlink删除目录项,链接数减1.参数是符号链接的话,将移除本身而不是所链接的文件。超级用户能用unlink去删除目录,同rmdir。 remove对文件来说相当于unlink,对目录来说是rmdir。 rename的处理情况较多复杂些。暂时不去关注,用到再说。 符号链接: 硬链接是直接指向文件的i-node,通过对i-node的链接增加计数来实现的。使用限制有两点1,链接和文件必须在同一个文件系统中,因为硬链接是与i-node号相关的,不同的文件系统中不同文件可能有相同的i-node号。2,只有超级用户才能创建目录的硬链接。 符号链接则是间接指向一个文件,通过创建新的i-node,i-node指向data block,data
block内容是文件的路径名。符号链接通常是用来将一个文件或整个目录层次移动到系统的另一个位置。 注意的是,当用到文件名作参数的函数时,函数是否follow符号链接。 syslink和readlink函数: 创建符号链接syslink。 open函数会follow符号链接,如果要打开符号链接并读其中的内容,则要用readlink。 文件的时间: 有三种,1,最后访问时间 2,最后修改时间 3,最后i-node状态改变时间。 utime函数可以修改1,2的时间,而3的时间调用utime时自动更新。 mkdir和rmdir函数 mkdir创建空的目录,并自动创建dot和dotdot目录项。注意创建时mode的权限除了读写外,还要有执行,这不同于文件。新目录的用户id和组id需要讨论。 rmdir则是删除空目录。 读目录: 有访问权限就可读目录,而只有内核才可以写目录。 Chdir,
fchdir,getcwd函数 改变当前的工作目录chdir,fchdir。当前工作目录是进程的一个属性。它并不影响调用进程的当前工作目录。 Getcwd得到当前工作目录。 Chapter 5. Standard I/O
Library Streams
and FILE Objects: 就是通过标准库如fopen打开的返回值指针FILE,使Stream关联到打开的文件。 fwide函数的作用是改变Stream的模式,是wide-oriented还是byte-oriented。Wide是为了支持国际化字符集,宽字符。 标准输出、输入、错误: 对应文件描述符的有相应的流, stdin, stdout, stderr。在stdio.h中有定义。 Buffering: 目的是为了最小化系统调用read、write的次数。 分为完全缓冲、行缓冲、无缓冲三种。 完全缓冲:行缓冲的流如果没有连接到终端设备上,则是完全缓冲。Stdin和stdout如果被重定位到文件上,则是完全缓冲。 行缓冲:stdin,stdout 无缓冲:stderr 打开流: Fopen,freopen,fdopen。 读写流: 一字节一次I/O: getc, fgetc, getchar.
Putc,fputc, putchar.其中,getc,putc一般由宏来实现。 一行一次I/O:gets,fgets,puts,fputs,最好不要使用gets,puts,不安全。造成缓冲区溢出。 直接I/O(二进制I/O):fread,fwrite,结构体的读写。以上的几个读写函数均不适用,如遇到空字节在结构体中时,上面函数的读写就无法完成。存在问题,当编译器或系统的不同,结构体字节对齐会造成读写不正确;机器体系结构的不同,二进制格式对多字节的整数或浮点数的存储也会不同。 标准I/O的效率: 验证的结果是,用户不用去设定在系统调用read、write时所用的缓冲区大小,在标准I/O中内部已经自动设定了这样最佳的buffer,使系统CPU的时间使用最少。而fgets和fputs中用户设定的line buffer的大小只会影响用户CPU的时间。因此,数据的copy有两次:一次在内核和标准I/O buffer之间(系统调用read,write);另一次在标准I/O buffer和我们的行buffer之间。
流的定位: ftell,fseek,rewind fgetpos,fsetpos(可移植的) 格式化I/O: 用得最多的是printf,scanf, sprintf,
snprintf, 具体的参数标志细节,用到时再查。 临时文件: 由两个函数生成,tmpnam, tmpfile. 目前作用貌似不大。 Chapter 6. System Data
Files and Information Password
File: /etc/passwd文件中包含了各个用户的如用户名、密码、用户id、组id等信息。并通过passwd结构来描述。 通过如getpwuid、getpwnam、getpwent函数来获得passwd结构。 Shadow
Passwords: 出于安全考虑,用户登录密码在/etc/shadow文件中加密。由spwd结构描述。 加密是one-way加密算法,意思是你不能通过加密后的密码得出原密码,而只能通过原密码去验证是否正确。 提供了相似的访问函数。但是shadow中的用户加密密码是不可读出的。 Group
File,Supplementary Group ID: /etc/group文件描述组的信息,并由group结构描述,包括组名、加密密码、组id、所属组 的用户名数组。 Other
Data Files: /etc/services; /etc/protoclos; /etc/networks; Login
Accounting: 有两个相关的文件:utmp,记录当前所有登录的用户。wtmp,记录所有的登录和退出。 System
Identification: 主机号、操作系统名、版本号等信息。uname,gethostname来获取。 Time
and Date Routines: 提供一系列的时间的相关函数,具有不同的表示方式。有个不同时间函数之间的关系图可作 参考。 Chapter 7. Process
Environment main函数: 在执行之前,内核通过start-up routine(通常是由汇编编写的)来执行main; 进程终止: 多种exit的终止:exit(会关闭打开的流), _exit, Exit,还有线程的pthread_exit。 atexit注册终止时调用的函数,按照注册的顺序,反序依次调用。 shell命令:echo
$? 查看程序的返回状态。 命令行参数: 没什么好说的 环境列表: 很少用到,主要是程序运行时的环境变量,如:HOME,SHELL,PATH,USER,LOGNAME等。 C程序的内存布局: Text,
Data, BSS, Stack, Heap. Text是程序的代码段。 Data是程序中初始化了的全局、静态数据变量。 BSS是程序中未初始化的全局、静态数据变量。即使全局、静态数据变量初始化为0仍然是属于BSS段。 Stack是程序中的局部变量,由高地址到低地址向下增长。 Heap是malloc调用动态分配的内存,由低地址到高地址向上增长。 注意:定义的字符串常量,其中变量算Data,而字符串的大小是属于Text的。 共享库: 好处是1.共享,减少可执行文件的大小 2.库更新时,使用库的每个程序都不用重新链接。 编译时用$ cc -static hello1.c 就取消了使用共享库,编译出的程序很大。 内存分配: malloc,
calloc, realloc 环境变量: 提供读取和设置环境变量的接口,getenv,setenv。。用处大概就是通过程序来设置和修改环境变量。 setjmp和longjmp: 用处是处理有深度嵌套调用的不同函数间的错误情况。用goto只能是本函数内的局部跳转,这两个函数就可以在函数间进行跳转。 有问题是跳转会影响不同类型变量的状态,对于没有优化编译的程序,即使是register变量也是放在内存中,因此所有类型的变量仍然得到内存中的值。但是优化-O编译的程序,由于自动变量、register变量是在寄存器中,当执行setjmp时,会从寄存器中读取数值,setjmp之后的重新赋值将不起作用。而全局、静态、以及volatile变量仍在内存中,值不会受到影响。因此,当写可移植性非局部跳转代码时,应使用volatile属性。 getrlimit和setrlimit函数: 每个进程都有资源限制的一个集合。如进程可用的总内存的大小,core文件的大小,创建文件的最大字节数等等。 包括软限制和硬限制。可以设置软限制小于等于硬限制。可以降低硬限制大于等于软限制但不可逆。只有超级用户进程才能提升硬限制。 资源限制影响调用的进程,并由子进程继承。一般内建到shell中设置资源限制。 Chapter 8. Process Control 进程标识符: 进程id是唯一的但是可以被重用。 进程id=0是调度进程,即swapper。此进程在磁盘上没有对应的程序,是内核的一部分,系统进程。 进程id=1是init进程,内核在启动过程结束后调用它,对应磁盘程序/etc/init,或/sbin/init.它是普通的用户进程,虽然它以超级用户权限运行。 Fork函数: 子进程和父进程共享代码段,但子进程有父进程的数据段、堆和栈的副本,但具体实现并不是完全的副本。 (sizeof 和
strlen,前者是在编译期间就计算了常量字符串的长度,它包括了字符串终止符,而strlen是在运行时计算字符串长度,并且不包括字符串终止符) Fork函数生成的子进程改变变量时并不影响父进程中的变量值。 对于I/O来说,如write输出字符串到标准输出上,由于是无缓冲的I/O,直接将字符串输出到标准输出上。 对于有缓冲的I/O,如printf的标准输出是连接到终端的,即此时是行缓冲,当输出的字符串没有换行符时,由于行缓冲没有被fflush掉,此时会从父进程中copy一份行缓冲到子进程中,输出的时候就会有两份。当字符串中有换行符,换行符会fflush掉缓冲,同无缓冲的I/O,只有父进程中的一份,直接将字符串输出到标准输出上。 当标准输出被重定向到文件上时,缓冲变为全缓冲,由于缓冲一直存在,子进程就copy了父进程的缓冲,因此也输出两份。 Fork函数使父进程中所有打开的文件描述符在子进程中都有一个副本。他们共享父进程打开的所有文件的文件表项(File Table Entry) Vfork函数: 与fork相似,但是语义不同,主要是用来exec一个新程序。它保证子进程先运行,直到调用exec或exit时,父进程才开始执行。 此外,不同于fork,vfork使父子进程共享虚拟内存空间,子进程中修改的变量会影响父进程中的变量。在子进程中调用exec或exit之前,他在父进程的空间中运行。 Exit函数: 正常退出和非正常退出. 正常退出包括,exit,_exit,_Exit, return,.pthread_exit;一般会通知父进程它是怎么退出的以及退出状态。 非正常退出包括, abort, 接受到某种信号,cancellation request(不懂);内核会产生非正常退出的原因。 僵尸进程是一个进程终止了而它的父进程没有等到它并且没有获得它的退出状态。当一个进程的父进程终止了,它的父进程将由init进程来继承,其pid是1. |
|