Unix文件和目录

目录是一个包含目录项的文件,创建新目录时,会自动创建两个文件名 . (当前目录)..(上一级目录)

文件描述符(fd),通常是一个非负整数,内核用以标识一个进程正在访问的文件,读写文件都通过这个fd。

出错处理

当系统函数出错时,通常会返回一个负数,或者返回空指针;整型变量errno通常设置成特定信息的值,每个线程有自己的局部errno,避免与其他线程混淆。

任何系统函数都不会将errno设置为0.

出错恢复:出错分为致命性和非致命性,致命性出错无法恢复,程序终止;非致命性出错,开发者可以进行妥善的异常处理进行恢复。

进程就是一个程序的实例,exec函数用来执行程序

时间值

1.日历时间

用UTC计算,1970.1.1的零时零分开始算起到目前的总秒数。

2.进程时间

也成为CPU时间,进程时间以时钟滴答计算,例如每秒钟多少滴答。

Unix系统为每个进程维护了三个时间值:时钟时间(墙上时钟时间)、用户CPU时间、系统CPU时间

用户CPU时间指执行用户命令所用的时间,系统CPU时间值执行内核程序的时间,两者之和称为CPU时间,也即进程时间。

时钟时间(墙上时钟时间)与当前系统的所有进程数有关,是某个进程运行的时间总量。

时钟时间=阻塞时间+用户cpu时间+系统cpu时间,进程数目会影响阻塞时间。

系统调用和库函数

直接进入内核的入口点叫做系统调用;库函数可能调用一个或多个系统调用;系统调用和库函数对于用户来说都是C函数,都给用户提供相应的服务。但系统调用是内核的直接入口点,也是最小入口,而库函数则不是。

Linux是一种提供类似于Unix丰富编程环境的OS——类Unix

Mac OS,Solaris是Unix系统。符合POSIX标准的系统称为Unix系统,Unix系统的一大特点就是丰富的编程环境。

限制

各个标准,ISO C,POSIX都定义了一些常量,表明某些类型或某些量的大小范围限制。

这些常量都是硬编码的, 将数据直接嵌入到程序或其他可执行对象的源代码中的软件开发实践,与从外部获取数据或在运行时生成数据不同。 硬编码的数据通常表示不变的信息,例如物理常量,版本号和静态文本元素。 另一方面,软编码数据对用户输入,HTTP服务器响应或配置文件等任意信息进行编码,并在运行时确定。

举例:int整型实际的位数,不同的系统是不一样的,而ISO C定义了int的最小长度,具体系统的实现不能小于该大小,这是限制的一个作用。

sysconf,pathconf,fpathconf用于运行时确定限制大小

编译时选项定义在unistd.h头文件中,也即编译时能确定的常量。

功能测试宏

用于不同的实现都有一些自己定义的限制,而若只想用POSIX或XOPEN的定义,则可以通过功能测试宏来限制,排除实现的专有定义,功能测试宏一般是下划线开头的:

_POSIX_C_SOURCE _XOPEN_SOURCE

或者在源文件中#defin _POSIX_C_SOURCE 来提供功能测试宏

文件I/O

一个文件的文件偏移量只有一个,不分读操作和写操作两个偏移量,读写两者共用一个文件偏移量。

#include<fcntl.h>

int open(const char* path,int oflag,.../* */)

int openat(int fd,const charpath,int oflag,.../ * */)

oflag是标志位,表明对该fd要进行的一些操作的限制

可以有多个标志位通过按位或给出,类似于文件流的标志位

openat是新的打开函数,目的是可以使用相对路径打开目录中的文件,而不再是只能打开当前工作目录。

同一进程中的线程共享当前工作目录,很难让同一进程的多个不同线程同一时间工作在不同的目录中

open openat的区别

由open和openat返回的文件描述符一定是当前可用的最小值,例如,如果在调用前有个较小的fd释放了,那么这次调用返回的就是这个较小的fd。

close

int close(int fd)

关闭文件的同时,会释放其上的锁标记。当一个进程结束时,内核自动会关闭其打开的文件,所以很多程序不显示的给出close函数。

lseek (location seek)

类似文件流中的设置光标位置,表示当前读或者写的位置,通过偏移量(字节)来设置。

打开文件时,若没有指定O_APPEND标志,偏移量默认为0,表示从文件开头开始。

可以显式的使用lseek函数进行偏移量的设置,而且如果重新定位进行写操作时原本的位置有数据,不会将数据往后推,而是直接覆盖。

如果指定了O_APPEND标志,则无论怎么设置写都是追加写,读写的文件当前位置是不同的,其他打开方式则读写都是同一个文件位置。

off_t lseek(int fd,off_t offset,int whence) 可用来判断fd能否设置偏移量,不能则返回-1

根据whence的值和offset的值设置当前偏移量,offset可正可负,返回设置后的偏移量

可以通过lseek(fd,0,SEEK_CUR)得到当前偏移量

whence=SEEK_SET表明文件头开始+offset 对应0

whence=SEEK_CUR表明当前位置+offset 对应1

whence=SEEK_END表明文件末+offset 对应2 可以直接用数字0,1,2代替常量

空洞部分并不要求在磁盘上占用存储区,具体实现与文件系统有关,一般是不会存储的。

off_t是有符号类型,其类型的大小会影响文件的最大字节上限,也即文件大小上限,有32位文件偏移量,也有64位文件偏移量

若为有符号32位的数,则文件最大为2G(2^31-1字节)。尽管可以有64位的偏移量,但能否创建大于2G的文件取决于文件系统

read

int read(int fd,void* buf,size_t nbytes)

读取nbytes字节的数据,返回实际读取的字节数。

将数据读取到buf中

write

ssize_t write(int fd,const void* buf,size_t nbytes)

将buf中的数据写入文件中,若对该文件没有写权限,虽然无法写进东西,但是write函数不会报错,也即不会返回-1.

在指定的fd上写入数据,返回写入的数据字节数,一般返回值与nbytes一样,否则都代表出错。出错有可能是磁盘已经写满或超过了给定fd的最大文件长度限制。

read和write都要经过内核缓冲区和用户缓冲区的复制。

文件共享

多个进程打开同一个文件时,系统会为每个进程维护一张文件表,这样每个进程都有自己的当前文件偏移量,如果多个文件同时写文件可能产生预想不到的结果。

原子操作

所谓原子性或原子操作,就是多个步骤组成一个操作,而且这个操作要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤中的一个子集。

O_APPEND标志

如果两个进程对同一个文件进行追加写操作,若操作逻辑是两者都先调用了lseek定位到文件尾,再分别执行写操作,则会发生问题,第二次写的内容会覆盖第一次写的内容。

打开文件时若选择了O_APPEND标志,则每次写操作会自动获得当前文件的尾进行追加,不用单独执行lseek,内核在写之前会将当前文件偏移量设置为该文件的尾端处。

pread pwrite

相当于先调用lseek,再调用read/write

创建一个文件

int creat(char* pathname,mode_t mode);创建具有mode文件访问权限的文件,返回以写方式打开的文件描述符,其等同于open(pathname,O_CREAT|O_TRUNC|O_WRONLY);只有使用了O_TRUNC标志,在文件名已存在时才会覆盖原文件,否则创建文件失败。

若检查文件是否存在和创建文件是两个函数分别执行的,那么可能存在另一个进程先一步创建了文件并写了内容,这些该进程再创建就会抹去原有的内容。

在一个目录下创建文件,必须拥有该目录的执行权限和写权限,可以没有读权限,没有读权限只是你不能看到该目录下的东西,实际上文件是已经创建成功了。

dup dup2

复制一个现有的文件描述符

int dup(int fd) 复制文件描述符fd,返回当前可用的文件描述符中最小值

int dup2(int fd, int fd2)复制文件描述fd,返回指定文件描述fd2,若fd2已存在,则先将其关闭后返回

dup(fd) == fcntl(fd,F_DUPFD,0);

dup2(fd,fd2) == close(fd2) ; fcntl(fd,F_DUPFD,fd2); 两者有些许不同,dup2是原子操作,而后者是两步操作,dup2和fcntl的errno也有不同

注意这与一般的文件共享不同,这里的文件表项只有一个,而不是各自有一个

sync fsync fdatasync

内核维护缓冲区高速缓存或页高速缓存,磁盘I/O通过缓冲区进行,写的时候,内核先将数据写入缓冲区,然后排入队列,然后晚些时候延迟写入磁盘。这种叫延迟写。

sync将所有修改过的块缓冲区排入写队列,然后就返回,并不等待实际的磁盘I/O写操作后返回

fsync指定文件描述符fd进行磁盘I/O,等待磁盘I/O后才返回,这种用于应用程序需要立即写入的情境下,例如数据库

fdatasync,与fsync作用相同,但只影响文件的数据部分,而fsync会同步更新文件属性

函数fcntl

fcntl函数可以改变已打开文件的属性

#include <fcntl.h>

int fcntl(int fd,int cmd,...) 若成功返回与cmd相关的值,否则返回-1

fcntl(fd,F_DUPFD,0)复制文件描述符,返回目前可用的大于等于第三个参数的最小值

在设置修改文件描述符标志和文件状态标志时要谨慎,要先获得现在的标志然后按照期望修改它,要注意是否会关闭所需要的标志

同步写标志O_SYNC,表明写操作必须等待磁盘I/O完成后再返回,而默认情况下,write函数只是把数据排入队列后就返回,不等待磁盘I/O的完成

ioctl函数

/dev/fd

在该目录下的文件是当前系统中用到的文件描述符,打开这些文件描述符相当于进行了文件描述符的复制

fd=open('/dev/fd/0',mode) == fd=dup(0);

大多数系统忽略mode参数,而有些系统要求mode参数

需要注意的是,Linux将这些文件与底层物理文件连接,所以打开这些文件需要注意,避免误操作导致文件内容被意外修改。

core dump

通常情况下,core文件会包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成的一个文件,许多的程序出错的时候都会产生一个core文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。

core文件的生成不一定打开了,需要用命令进行打开,以及进行相应的设置。

core dump就是将程序异常终止时保存存储的信息,用于分析错误原因。

不带缓冲的I/O

第三章所用的IO函数都是不带缓冲的,他们都是直接的系统调用,虽然说在磁盘I/O的时候,会经过内核的块缓存区,但这不是缓冲,缓冲是指进程内部对读写的数据进行缓冲处理,这是软件定义的,不是硬件的缓存。

硬件的叫缓存,软件的叫缓冲。

重定向指让两个fd进行共享

用O_APPEND模式打开的文件,可以用lseek在任意位置开始读,但是对于write则不能使用,会自动在末尾进行写操作

文件和目录

stat fstat lstat fstatat函数

文件类型

1.普通文件(regular file)

包含某种形式的数据,该文件是二进制文件还是文本文件对于内核并无区别,但若是二进制可执行文件,那么需要遵循统一的标准,内核才可以确定程序文本和数据加载位置。

2.目录文件(directory file)

可以理解成文件夹,目录文件包含了其他文件的名字和指向这些文件有关信息的指针,具有该目录文件读权限的进程可以读取该目录的内容,而写操作只能通过内核进行,需要使用给定的函数才能执行。

3.块特殊文件(block special file)

这种类型的文件提供对设备(如磁盘)的带缓冲访问,每次访问以固定长度进行。

4.字符特殊文件(character special file)

这种类型的文件提供对设备的不带缓冲访问,每次访问长度可以改变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。

5.FIFO

用于进程间通信,也成为命名管道

6.套接字(socket)

用于进程间的网络通信,也可以用于同一主机的不同进程的非网络通信

7.符号链接(symbolic link)

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

设置用户ID和组ID

可以通过设置用户ID位和设置组ID为来使得程序在执行时的有效用户ID和组ID为相应的可执行文件的所有者ID和组ID。实际用户就是我们当前登录系统的用户是谁。

如果不使用设置用户ID和设置组ID标志,则我们打开程序时,有效用户ID和组ID就是我们当前用户(实际用户)的ID,而设置用户ID和组ID则是当程序执行时,将进程的有效ID设为该文件的所有者ID。

用户访问权限

所有文件都有访问权限,st_mode值包含了对文件的访问权限位。

1.当我们给定一个路径,要打开该路径下的某个文件时,必须对该路径上的所有目录都具有执行权限,包含对隐含的当前目录的执行权限,然后该文件必须有相应的权限才能打开。若中间某个目录没有执行权限,那么打开是失败的。目录的读权限和执行权限是两回事,读权限是指可以读取该目录下所有文件列表,而执行权限是可以让我们通过该目录

2.在一个目录中创建新文件,必须要对该目录有执行权限和写权限。若删除某个文件,必须对目录有写权限和执行权限,没有对文件的读写权限要求

3.用exec函数执行文件时,该文件必须有执行权限,并且文件必须为普通文件

上述的权限测试是按顺序执行的,若已经确定了权限则不会执行下一步的判断,并且会根据所确定的权限来进行文件的权限访问。

所以,每个进程对于系统中文件的访问权限,与进程的有效用户ID和有效组ID有关,会根据权限测试进行判断。设置用户ID位和设置组ID位就是用来设置进程打开时的有效ID位的,与访问文件的权限相关

新文件和新目录的ID

新文件和新目录的ID设置相同,用户ID设置为进程的有效用户ID,而组ID可以有两种情况:

1.设置为进程的有效组ID 2.设置为所在目录的组ID

Linux通过所有目录的设置组ID位是否被置位,是则新文件的组ID与其一样,否则与进程的有效组ID一样

access faccessat函数

用于测试实际用户ID和组ID是否对指定文件有相应的权限

mode是指测试什么权限,如果测试文件是否存在,则mode为F_OK;

函数umask

设置创建文件屏蔽字,

cmask是由9个常量按位或的值,这9个常量是访问权限位,控制用户、组、其他的访问权限的位

只要cmask中置位的对应的权限,则创建文件时,该文件不会拥有该权限,被ban了。

系统登录后,会自动设置umask,若我们的进程想要在创建文件时使用自己的创建文件屏蔽字,则需要调用umask函数进行设置。

注意八进制值与屏蔽位的对应关系

chmod fchmod fchmodat

这三个函数更改文件的访问权限,要想更改文件的访问权限,必须进程的有效用户ID等于该文件的所有者用户ID,或者是超级用户。

多了6个位,所以在ls -l命令中,可能的权限位不止9个,下面这个新增了一个设置组ID的位

ls -l显示的是文件内容修改的最后时间,而chmod修改的是i节点的最后更新时间,所以ls -l的时间没有变化

chmod可能自动清除的mode位

若没有超级用户权限,而又设置普通文件粘着位,则会被自动清除,但Linux和Mac OS并不进行这样的处理,因为粘着位对于这些系统没有意义。

Linux中若没有超级用户权限的进程写一个文件,则该文件的设置用户ID和设置组ID位都被自动清除。

粘着位 S_ISVTX

粘着位也叫正文保持位(saved_text),设置了该位的程序,在程序终止时,其正文部分(机器指令)会存在交换区中,下次再执行时能够更快,交换区是一个连续的区域,对于交换区中可以存放的设置了粘着位的文件数量是有限制的,避免过多占用交换区,但由于现在的文件系统都提供虚拟存储系统以及快速文件系统,所以这个技术逐渐被淘汰了。

函数chown fchown fchownat lchown

上述四个函数可以更改文件的用户ID和组ID,不同于chmod,chmod是更改文件访问权限,而上述四个是改变文件的所有者用户ID和组ID

若_POSIX_CHOWN_RESTRICTED限制生效,超级用户可以改文件的所有者用户ID和组ID,非超级用户只能更改自己所拥有的文件的组ID,且只能改成自己所属的组ID。

文件长度

stat结构中的st_size代表文件按字节算的长度,只对普通文件,目录和符号链接有意义

对于目录和符号链接,文件长度不会为0,因为目录至少包含. ..两项,而符号链接的长度指路径名中包含的字节数。

对于普通文件,其长度可以为0,读该文件得到eof提示

目录文件通常是一个数(16或512)的整数倍;对于符号链接,长度为其文件名的实际字节数,不包含空字符

文件中的空洞

文件的大小分为逻辑大小和实际内存大小,用ls -l 文件名,获得文件的逻辑大小,du -s 文件名获得文件实际占的磁盘内存大小。

对于空洞文件,逻辑大小为包括空洞在内的字节数,而其实际所占的存储块大小为除去空洞后的大小,这个大小不一定就是文件所有字节的大小,因为文件系统存储的结构,存在着对存储块的指针,所以实际存储会大些。

如果把文件中的空洞填充了,那么文件系统才会实质上为其分配存储块。

空洞文件的用途:1.我们下载文件时,文件还没下好,一个临时文件已经有所下载文件的大小了,这就是空洞文件

2.创建虚拟机时,我们分配的磁盘空间也是利用了空洞文件,否则一下就划走了真实的磁盘存储大小。

文件截断

O_TRUNC标志位在打开文件的时候将文件长度截断为0

#include <unistd.h>

int truncate(const char* pathname,off_t length);

int ftruncate(int fd,off_t length); 函数成功执行返回0,否则返回-1

截断指定的文件为length长度,若length比以前的长度小,则截断为length长,若以前的长度小于length则阶段后文件长度将增加,中间会补空洞。

文件系统

磁盘上有不同的分区,每个分区都可以有各自的文件系统

i节点包含了文件的所有信息:文件类型,文件访问权限位,文件长度,指向文件数据块的指针等。

一个文件想要被删除,首先必须去链接计数要为0,对于一个文件,其i节点可能会有多个目录项指向它,每有一个指向则链接计数+1。

所有的文件都是通过i节点管理所有信息,然后i节点再指向实际数据块,体现为整个文件。

在文件系统移动文件时,并不需要复制文件,只需要创新的目录项指向i节点,删除旧的指向,就可以了,相当于只是指针的创建、删除,不涉及数据的复制。

函数link linkat unlink unlinkat remove

也即创建一个新的目录去指向一个现有的目录或者文件,增加起链接计数,大多数操作系统不支持对目录的硬链接,也即不支持对目录的link操作,即使支持也是只有超级用户可以,一般不允许这种硬链接。

unlink函数用来删除一个目录项或文件,同时对其所指向的所有文件,这些文件的链接计数都减一,若此时为0则该文件会被删除,否则该文件还可以通过其他链接进行访问。想要删除一个目录,必须对包含该目录项的路径都有执行和写权限。

当链接计数为0时,会删除文件,但若该文件已经在进程中打开了,在执行了unlink之后,该文件已经被删除了,是找不到该文件的,但是实质上数据块还没有删除,要等到进程结束了,才自动删除数据。du命令是找不到该文件的话就不会统计其大小,而ds则会统计实质当前磁盘的存储情况。

remove函数对于文件相当于unlink,对于目录相当于rmdir

函数rename renameat

文件或目录通过上述函数进行重命名,前提是要改名的文件必须先存在,否则没有任何效果。

对于文件,若不存在重名的就直接重命名。若newname对应的文件已经存在了,则该文件不能是目录,此时会删除现有的newname文件,然后将oldname重命名为newname,这种情况下需要对oldname和newname都具有写权限。

对于目录,若不存在重名就直接重命名;若已存在newname的文件,则该文件必须是目录,且该目录必须是空目录,只有.和..。这是会删除该目录,然后将oldname重命名为newname。newname不能包含oldname作为其前缀,例如不允许oldname=/usr/foo newname=/usr/foo/testdir 因为这样涉及到要删除旧目录,把原来的foo改成foo/testdir后,原来的foo是不存在的,但是最终结果又要有这个目录,· 逻辑上有冲突。

符号链接

硬链接是直接指向文件的i节点,而符号链接是对一个文件的间接指针,可以简单的理解成类似文件的指针一样 。硬链接增加文件的链接计数,而符号链接不增加(因为符号链接可以指向不存在的文件),硬链接类似于shared_ptr,符号链接类似于weak_ptr。使用符号链接可以避免硬链接的限制:

1.硬链接要求链接和文件必须在同一文件系统中

2.只有超级用户才能创建硬链接

对于符号链接以及他所指向的对象没有文件系统限制,符号链接一般用于将一个文件或目录结构移到系统中的另一个位置。

所谓的符号跟随是指,访问符号链接实质上是访问其所指向的文件,而不跟随符号则是访问符号链接本身。

符号链接可能成环,若这种情况下,函数或者操作跟随符号链接的话,那么会出现死循环:

只要不进行符号跟随就能正常运作,也有些函数直接不显示这些循环的结果。

符号链接的循环是很好清除的,通过unlink函数即可,例如unlink(foo/testdir)

创建符号链接和读取符号链接

创建一个sympath符号链接指向actualpath,actualpath的文件可以不存在,例子如上一节。

因为open函数跟随符号链接,所以想要打开符号链接本身,需要用到专门的函数。readlink 和readlinkat

文件的时间

stat结构体中有三个量用来存放文件的相关时间;

1.st_atim 文件的最后一次访问时间

2.st_mtim 文件数据的最后一次修改时间

3.st_ctim i节点的状态最后一次更改时间

注意i节点的最后一次修改时间和文件内容的最后一次修改时间是有区别的,因为在文件系统中,两者是分开存放的。

目录是包含目录项的文件(文件名,相关的i节点编号),所以增加删除修改目录项会影响它所在目录的3个时间。不同的函数对文件的操作不同,所以可能除了修改了文件自身的时间外,还可能修改了其父目录的时间。

函数futimens utimensat utimes

指定修改文件的修改时间和访问时间时,状态更改时间st_ctim会自动更新为实时时间,且不能指定。

函数mkdir mkdirat rmdir

用于创建目录(若存在则不创建,不会覆盖)和删除目录

文件的访问权限由mode和进程的文件创建屏蔽字决定。对于目录一般需要指定执行权限,这样才能通过目录路径,访问该目录下的文件名。

rmdir可以删除一个空目录,该目录只包含. ..两项

读目录

对目录具有访问权限的用户可以读目录,但只有内核才可以写目录,一个目录的写权限和执行权限只是决定了是否可以在该目录中创建文件和删除文件,但不代表能写目录本身。

如果目录没有读权限,我们不能获得该目录下有什么内容。

函数chdir fchdir getcwd

进程使用chdir fchdir可以更改当前工作目录

当前工作目录是进程的一个属性,chdir只会更改进程自身的cwd而不会改变其他的进程。

chdir跟随符号链接,所以如果某个目录是符号链接,那么会更换到其所指向的目录。

fchdir提供了更便捷的方式,如果我们想要回到文件打开的目录位置,只需要保存open函数返回的fd值,然后调用fchdir就能返回到原来的目录下。

getcwd是返回当前目录的绝对路径。可用于工作路径之间的切换,保存之前的,切换到新的后,可以chdir回到之前的路径。

设备特殊文件

每个文件系统所在的存储设备由主设备号和次设备号表示。若各个文件系统都在同一个磁盘下,则主设备号相同,但次设备号不同。st_dev和st_rdev来表示这两个设备号,是stat结构体的成员。只有字符特殊文件和块特殊文件才有rdev。

主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号表示特定的子设备。

实现预定义了宏函数major和minor,分别返回主设备号和次设备号:major(st.st_dev) minor(st.st_rdev);Linux将这两个宏定义在<sys/macros.h>,而该文件由包含在<sys/types.h>

一个文件系统中的所有文件,其st_dev都是保存的该系统的设备号。

通常只有包含随机访问文件系统的设备是块特殊文件设备。

头文件

关于文件I/O的open read write creat等系统调用函数,都在unistd.h

O_RDONLY,O_WRONLY,O_RDW等标志常量在fcntl.h头文件中

涉及文件类型相关的函数一般定义在sys/stat.h中