第一章 UNIX基础
- 编译命令
1
cc myls.c
在配置了GUN C的编译系统的系统中,C编译器是gcc(1),cc被链接到gcc
- 文件描述符是一个小的非负整数
运行程序是,shell打开三个文件描述符,标准输入/标准输出/标准错误,不做特殊处理,这三个描述符都链接向终端,这三个描述符都能重新定向到某个文件
1
ls>file.list//标准输出重新定向到某个文件
将程序编译成标准名称的a.out文件
1
2./a.out >data //标准输入是终端,标准输出重新定向至文件data,标准错误也是终端
./a.out <inflie> outfile //将名为infile的文件复制到outfile中fgets函数读取完整的行,read函数读取指定的字节数
- 标准I/O函数是printf,<stdio.h>包含所有标准I/O函数的原型
- getc函数一次读取一个字符,然后putc将此字符写到标准输出,读到输入的最后一个字节时,getc返回常量EOF
- 标准IO常量stdin和stdout也在头文件<stdio.h>中定义
- UNIX保证程序进程有唯一的数字标识符,称为进程ID(非负整数)
- 一个进程通常只有一个控制线程,如果有多线程解决不同的部分,可以充分利用多处理器系统的并行能力,一个进程的多个线程共享地址空间/文件描述符/栈等等
- 线程ID仅在进程内起作用
- UNIX函数出错,通常返回一个负值,整型变量通常被设置为具有特定信息的值,如:整数或者常量字符(0/1/2/EOF/EACCES)
- 关于errno:
- 如果没有出错,值不会被例程清除
- 任何函数都不会将errno的值设为0
第三章 文件I/O
- I/O文件用到五个函数:open/read/write/lseek/close
- 不带缓冲的I/O指的是每个read和write都调用内核中的一个系统调用
- 文件描述符:
- 所有打开的文件通过文件描述符引用
- open/creat函数将文件描述符作为参数传递给read/write
- 文件描述符幻数通常用符号常量替换,符号常量在头文件<unist.d>定义
- 文件描述符0与标准输入关联-STDIN_FILENO
- 文件描述符1与标准输出关联-STDOUT_FILENO
- 文件描述符2与标准错误关联-STDRR_FILENO
第四章 文件和目录
stat结构中的成员代表文件的属性
1 | #include <sys/stat.h> |
- stat函数返回pathname有关的信息结构
- fstat在打开文件有关信息
- lstat返回符号链接有关信息
文件类型
- 普通文件
- 文件目录
- 块特殊文件
- 字符特殊文件
- FIFO:用于进程间通信
- 套接字:用于进程间网络通信
- 符号链接:指向另一个文件
文件访问权限
进程打开/创建/删除一个文件时,内核进行文件访问权限测试,根据用户ID和进程组ID进行测试
函数umask为进程设置文件模式创建屏蔽字
1 | #include <sys/stat.h> |
- 进程创建一个新的文件或者目录的时候,一定会使用文件模式创建屏蔽字
- 文件模式创建屏蔽字中为1的位,在文件mode中的相应位一定会被关闭
函数chmod/fchmod/fchmodat更改现有的访问权限
- chomd在制定的文件上操作
- fchomd对已经打开的文件进行操作
- 改变文件权限位,进程的有效用户ID必须是文件所有者ID或者有超级用户权限
- 在我自己的例子中不能设置,因为不是超级用户
粘着位 S_ISVTX
- 设置了粘着位之后,文件的正文被保存在交换区,交换区作为连续文件处理,在下次执行该程序时,可以较快的装载入内存
- 后来被称为保存正文位
- 现在的Unix系统都配置了虚拟内存系统以及快速文件系统
- 允许目录使用粘着位,允许对该目录具有写权限且是一下三种之一的用户来能删除或重命名该目录下的文件,拥有此文件、拥有此目录、超级用户
- 在solaris 10中,如果对普通文件设置了粘着位,如果执行位没有任何设置,操作系统不会缓存文件内容
函数chown,fchown、fchownat、lchown
1 | #include <unistd.h> |
- 这几个chown函数可以修改文件的用户ID和组ID,如果两个参数owner或group中的任意一个为-1,对应ID不变
- 引用的文件是符号链接的情况下,函数lchown和fchwnat设置的AT_SYMLINK_NOFOLLOW更改符号链接本身的所有者而不是符号链接指向的所有者
- _POSIX_CHOWN_RESTRICTED常量定义在头文件
<unistd.h>
中,设置更改文件的所有者是超级用户还是所有用户,可以通过pathconf或者fpathconf函数进行查询 - 若_POSIX_CHOWN_RESTRICTED对指定文件生效,则只有超级用户进程能更改该文件的用户ID;进程拥有该文件,参数owner==-1或文件的用户ID,并且参数group等于进程的有效组ID或进程的附属组ID之一,此时,非超级用户也可更改文件的用户ID
- 当_POSIX_CHOWN_RESTRICTED生效时不能更改其他文件的用户ID
文件长度
- stat结构成员st_size表示以字节为单位的文件长度,只对普通文件,文件目录和符号链接有意义,文件长度不包含结尾的null字节
- 对于文件目录,文件长度通常是16或者512的整倍数
- 现代UNIX系统提供字段s_blksize文件I/O适合的块长度和st_blocks分配的实际512字节块块数
- 不同版本的s_blksize并不都是可移植的,因此使用512是不可移植的
- 普通文件可以包含空洞
du -s core
Linux中,如果设置了POSIXLY_CORRECT
du报告的块的大下是1024,否则就是512wc -c core
看出正常的I/O操作读整个文件长度,如果使用实用程序复制整个文件,cat core > core.copy
所有实际数据字节被填写为0,所有空洞都被填满
文件截断
- 打开文件时使用O_TRUNC可以将文件截断为0
1
2
3
4
5/*截断文件可以调用一下两个函数*/
/*将现有文件长度截断为length,如果以前长度没有length则增加到length多的为0也可以是在文件中创建一个空洞*/
#include <unsitd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
文件系统
- 大部分unix文件系统支持大小写敏感的文件名,HFS是大小写保留的且是大小写不敏感的
- 将磁盘多个区分为不同的文件系统,i节点是固定长度的记录项包含文件的大部分信息
- 多个目录项指向i节点,每个i节点都有一个链接计数,当链接计数至少为0时才能删除文件,解除文件链接并不代表删除文件
- 删除目录项的函数unlink,在stat结构中链接计数记录在st_nlink成员中,基本数据类型时nlink_t,这种链接称为硬链接
- LINK_MAX指明一个文件链接数的最大值
- 另一种类型是符号链接,符号链接文件的实际内容在数据块中,包含了符号链接所指向的文件的名字
- i节点的文件类型是S_IFLINK系统知道这是一个文件链接
- i节点包含文件有关的所有信息:文件类型、文件访问权限位、文件长度和指向文件的数据块的指针
- stat结构中的大多数信息都取自i节点
- 目录项中的两个重要数据:文件名和i节点编号,i节点编号的数据类型是ino_t
- 目录项中的i节点编号指向同一个文件系统的i节点,
ln
构造一个指向现有文件的新目录,不能跨越文件系统 - 在不更换文件系统的情况下给文件重命名,则文件内容不会移动,这是
mv
的操作方式 - 在工作目录中构造一个新的目录
mkdir testdir
- 任何一个叶目录(不包含其他目录的目录),父目录中每一个目录都使得父目录的链接计数+1
函数link、linkat、unlink、unlinkat、remove
创建一个指向现有文件的链接的方法,使用link函数或linkat函数
1
2
3
4
5
6
7
8
9
10
11#include <unistd.h>
/*创建新目录项newpath,引用现有文件existingpath,如果newpath已经存在,则返回出错*/
int link(const char *existingpath, const char newpath);
/*现有文件通过efd和existingpath参数指定的,新的路径名是nfd和newpath*/
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
/*1)如果两个路径名中有任意一个是相对路径,通过对应的文件描述符进行计算
2)如果两个文件描述符中的任一设置为AT_FDCWD,路径名就相对于当前目录进行计算
3)如果任一路径是绝对路径,则相对的文件描述符就会被忽略
现有文件是符号链接时,flag参数控制linkat函数,
如果flag被设置了AT_SYMLINK_FOLLOW标志位,则创建符号链接的目标的链接,
如果这个标志被清除了,则创建符号链接本身的连接*/创建新目录项和增加链接计数应该是原子操作
很多文件系统实现不允许对于目录的硬链接
1
2
3
4
5
6
7
8
9
10
11#include <unistd.h>
/*删除现有的目录项可以调用unlink函数*/
/*删除目录项并将pathname引用的文件链接计数-1,如果出错则不对文件进行修改*/
/*要解除对文件的链接必须要包含该目录项的目录具有写和执行的权限*/
/*进程打开或者链接计数大于0则文件不能删除*/
int unlink(const char *path);
/*pathname是相对路径名,函数计算fd文件描述符参数表示的目录的路径名
如果fd被设置为AT_FDCWD,调用进程当前工作目录来计算路径名
如果pathname是绝对路径,fd参数被忽略*/
/*flag参数设置标志位AT_REMOVEDIR,函数unlinkat像rmdir一样删除目录,如果标志被移除,unlinkat和unlink操作一致*/
int unlinkat(int fd, const char *pathname, int flag);使用remove来解除对一个文件或者目录的链接,对于目录,remove和rmdir功能一致
函数rename和renameat
1 | #include <stdio.h> |
符号链接
- 使用符号链接可以避开硬链接要求链接和文件处于同一个文件系统下,只有超级用户才能创建一个指向目录的硬链接,
- 函数是否处理符号链接,第四章中跟随符号链接的函数:access/chdir/chmod/creat/exec/link/open/opendir/pathconf/stat/truncate
- link函数不允许构造指向目录的硬链接
创建和读取符号链接
1 | /*可以用symlink或symlinkat函数创建一个符号链接*/ |
文件的时间
- 每个文件维护三个时间字段
- 影响i节点的操作:更改文件的访问权限、更改用户ID、更改链接
- access和stat函数并不更改这三个时间中的另一个
- find命令用来对一定范围内按访问时间归档的的文件进行操作
ls -l或ls -t
系统默认对文件修改时间进行排序,ls -u
按照访问时间进行排序,ls -c
按状态修改时间进行排序- 读写一个文件只影响该文件的i节点,对目录无影响
函数futimens、utimesat和utimes
- 指定纳秒精度的时间戳
1
2
3
4
5
6
7
8
9
10
11
12
13
14#include <sys/stat.h>
/*time[0]包含访问时间,times[1]包含修改时间*/
/*执行这些函数的优先权取决于times参数的值*/
/*futimes需要打开文件来修改时间*/
int futimens(int fd, const struct timespec times[2]);
/*utimesat使用文件名更改文件时间,flag参数可以修改默认行为,可以设置文件跟随符号链接的时间*/
int utimensat(int fd, const char *path, const struct timespec times[2]);
/*对路径名进行操作,times*/
int utimes(const char *pathname, const struct timeval times[2]);
/*不能对st_ctim指定一个值,调用utimes函数时,字段会自动更新*/
struct timeval{
time_t tv_sec;
long tv_usec;
};
函数mkdir、mkdirat和rmdir
- 用mkdir和mkdirat创建目录,用rmdir函数删除目录
1
2
3
4
5
6
7
8
9#include <sys/stat.h>
/*函数创建一个新的空目录,访问权限mode由进程的文件模式创建屏蔽修改*/
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
#include <unistd.h>
int rmdir(const char *pathname);
/*如果调用此函数使目录的链接计数成为0,并且没有其他进程打开此目录,释放此目录占用的空间
如果链接计数为0,在函数返回前必须删除最后一个链接及.和..*/
读目录
- 对目录有访问权限的用户都可以读目录,但是只有内核才能写目录
函数chdir、fchdir和getcwd
- 每个进程都有一个当前工作目录,是所有搜索相对目录的起点
- 当前工作目录是进程的一个属性,起始目录是登录名的一个属性
- 进程调用chdir和fchdir更改当前工作目录
1 | #include <unistd.h> |
设备特殊文件
- 文件系统的存储设备由主次设备号表示,主设备号标识设备驱动程序,次设备号标识特定的子设备
- 使用两个宏:major和minor来访问主次设备号
- 系统与每个文件名相关的st_dev值是文件系统的设备号,文件系统包含文件名以及对应的i节点
- 只有字符特殊文件和块特殊文件才有st_rdev值,包含实际设备的设备号
- 文件访问权限位
第五章 标准I/O库
流和FILE对象
- 前面所有I/O函数都是围绕文件描述符的,打开一个文件时返回一个文件描述符,此后所有操作都和文件描述符有关
- 标准I/O库,围绕流进行,流的定向决定了所读、写的字符是单字节还是多字节的
- 流在创建时没有定向,两个函数改变流的定向,freopen和fwide
1
2
3
4
5
6
7
8#include <stdio.h>
#include <wchar.h>
/*根据mode的不同值,fwide执行不同的工作
mode参数值为负,函数指定流是字节定向的
mode参数值为证,函数指定流是宽定向的
mode参数值为0,函数不设置流的定向,返回标识该流定向的值
函数不改变已定向流的定向*/
int fwide(FILE *fp, int mode);
标准输入、标准输出和标准错误
三个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引用,这三个指针定义在头文件<stdio.h>中
缓冲
- 全缓冲填满标准I/O缓冲区后才进行实际I/O操作,驻留在磁盘上的
- 行缓冲在输入和输出中遇到换行符,标准I/O库执行I/O操作
- 不带缓冲不对字符进行缓冲存储,字符能够立即输出,通常标准错误流stderr不带缓冲
- 通常标准输出是不带缓冲的,指向终端设备的流是行缓冲的,否则就是全缓冲的
- 可以调用函数setbuf/setvbuf来更改缓冲类型,一定要在流打开后调用
打开流
- 函数如page118所示
- 字符b是参数type的一部分,标准I/O系统可以区分文本文件和二进制文件
- 用读和写类型打开一个文件,如果中间没有fflush/fseek/fsetpos/rewind,在输出的后面不能直接跟随输入;如果中间没有fseek/fsetpos/rewind或者输入操作没有到达文件尾,则输出之后不能直接跟随输出
- 使用函数fclose关闭打开的流,在文件关闭之前,冲洗缓冲中的输出数据,缓冲区中的任何输入数据被丢弃
- 进程正常终止,exit或者在main中返回,所有带未写缓冲数据的标准I/O流被冲洗,所有打开的标准I/O被关闭
读和写流
- 三个输入函数getc/fgetc/getchar,三个函数在返回下一个字符前,将unsigned char类型转换为int类型,返回结果要和EOF等值比较不能是字符类型的
- 调用ferror或者feof函数来区分函数出错还是到达文件尾端
- 每个流在FILE对象中维护两个标志:出错标志和文件结束标志,可以调用clearerr函数来清除这两个标志
- 从流中读出数据后还可以通过ungetc将字符再压送回流中,压送回流后再读字符的顺序和压送回的顺序相反,因为每次只实现一个字符的压送
- 不能回送EOF,一次成功的ugetc调用会清除该流的文件结束标志
每次一行I/O
- gets从标准输入读,不指定行的长度,有可能会造成缓冲区溢出,不将换行符存入缓冲区
- fgets从指定的流读,指定缓冲长度为n一直读到下一个换行符位置,且长度不超过n-1,缓冲区以null为结尾,下一次调用会继续该行
- 函数fputs将一个以null字节终止的字符串写到指定的流,尾端终止符null不写出
- puts将换行符写到标准输出
二进制I/O
- fread和fwrite函数执行二进制I/O操作,读或者写一个二进制数组,读或写一个结构
- 返回读或写的对象数,读的返回可以少于nobj是到达文件尾端或是出错,写的返回少于nobj是出错
- 二进制I/O只能用于同一个文件系统,在同一个结构中,同一个成员的偏移量可能随编译程序和系统的不同而不同,用来存储多字节整数和浮点值的二进制格式在不同的系统结构之间可能不同
格式化输出
- 由5个printf函数来处理
- 一个转换说明有4个可选的部分:
%[flags][fldwidth][precision][lenmodifier]convtype
- flags标志
- fldwidth最小字符宽度
- precision整型转换后最少输出数字位数、浮点数转换后小数点后的最小位数、字符串转换后最大字节数,
.6
- 宽度和精度皆可为*,整型参数指定宽度或者精度的值
- lenmodifier说明参数长度
- convtype不是可选的,控制如何解释参数
格式化输入
- 由三个scanf函数处理
%[*][fidwidth][m][lenmodifier]convtype
- *用于抑制转换,按照转换说明的其余部分对输入进行转换
- lenmodifier转换结果赋值的参数大小
- m是赋值分配符,可以用%c/%s等破事内存缓冲区分配空间接纳转换字符串
实现细节
- 每个I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno函数获得描述符
临时文件
1 | #include<stdio.h> |
- tmpnam产生一个与现有文件名不同的有效路径字符串,每次调用他都产生不同的路径名,最多调用次数TMP_MAX,定义在头文件<stdio.h>
- 如果ptr是NULL每次调用tmpnam都会重写静态区,因此如果要调用这个函数多次,应该保存路径名的副本而不是指针的副本,如果ptr不是NULL,则指针指向的长度至少是L_tmpnam个字符的数量,产生的路径名存放在这个数组中,ptr作为返回值返回
- tmpfile创建一个临时的二进制文件,关闭文件或者进程结束时,自动删除这种文件
内存流
- 所有的IO都是通过缓冲区和主存之间来回传送字节来完成的
1
2
3
4#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
//buf指定了缓冲区开始的位置,size指定了缓冲区大小的字节数,type参数控制如何使用流
//如果bud参数为空,fmenopen函数分配size字节数的缓冲区,流关闭时缓冲区释放
- 其他两个函数
1
2
3
4
5
6
7#include <stdio.h>
FILE *open_menstream(char **bufp, size_t *sizep);
//面向字节的函数
#include <wchar.h>
FILE *open_wmemstream(wchat_t **bufp, size_t sizep);
//面向宽字节的函数
- 这两个函数创建的流只能写打开
- 不能指定缓冲区,分别通过bufp和sizep来访问缓冲区地址和大小
- 关闭流之后需要自行释放缓冲区
- 对流添加字节会增加缓冲区大小
- 任何时候需要增加缓冲区中的数据量以及调用fclose、fflush、fseek、fseeko以及fsetpos时都会在当前位置写入一个null字节
- 缓冲区地址和大小的使用必须遵循的规则
- 缓冲区地址和长度只有在调用fclose或fflush后才有效
- 这些值只有在下一次流写入或调用fclose前有效
- 缓冲区可能会需要重新分配,如果出现这种状况,缓冲区的内存地址值在下一次调用fclose或fflush时会改变
- 内存流能够避免缓冲区溢出,非常适合用来创建字符串。内存流只访问主存,不访问磁盘上的文件,所以对于使用标准IO流作为参数用于临时文件的函数来说性能会有很大的提升