在linux系统中——一切都是文件。
1. 磁盘物理结构
磁盘的物理结构如下:
磁盘由很多盘面组成,而盘面上则是由很多同心圆环组成的磁道,每个磁道又被切割成许多扇区。所有磁盘面的同一个磁道构成一个柱面,同一柱面的所有磁道写完后,才会移入下一柱面。
磁盘的最小组成单位可以看成扇区,每个扇区的大小逻辑上看起来是512字节,但实际上底层的物理扇区是4096字节,可以通过如下方式去得到:
gqx@gqx-Lenovo-Product:~$ sudo fdisk -l Disk /dev/sda: 931.5 GiB, 1000204886016 bytes, 1953525168 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 4096 bytes I/O size (minimum/optimal): 4096 bytes / 4096 bytes
由于扇区太小了,如果实际操作的时候按扇区为单位去读写数据的效率比较低,就比如10M的文件,磁头就需要读取(1024*10/512)20480次移动操作,所以一般都是用块(多个扇区)这个逻辑概念来实现具体的读写操作,一个块的大小通常是4096个字节:
gqx@gqx-Lenovo-Product:~$ sudo tune2fs -l /dev/sda4 | grep "Block size" Block size: 4096
2. 文件系统的结构
在UNIX系统里当我们将上述的磁盘结构上升一层到具体的文件层面时,就会将磁盘划分成很多个分区,每个分区对应一个文件系统,磁盘、分区和文件系统的结构如下:
-
自举块:也称为引导块,分区中文件系统自身引导程序存放的地方。
-
超级块:超级块在每个文件系统的根上,超级块描述和维护文件系统的状态。
-
......
这里只关注柱面组里的i节点和数据块部分结构,其中数据块存放着文件的具体内容,如下图:
每个i节点都有一个链接计数标记,表示指向该i节点的文件/目录的数量,只有当链接计数标记减少到0时,才会释放该文件所占用的数据块,可以从图中看到有两个目录项指向同一个i节点。
i节点包含了文件的所有信息,包含文件的权限位信息,文件类型文件长度等(文件名保存在目录项中)。
在不更换文件系统的情况下为一个文件重命名,该文件的实际位置并没有更改,只需在目录块中构造一个新的目录项指向原先的i节点,并将原来的目录项删除即可。
可以举个栗子,假设我现在在一个空的工作目录中创建了一个目录(testdir),则现在这个目录中有三个子目录——("." , ".." 和 "testdir"),这三个目录以及testdir目录在柱面组里目录块和数据块的结构如下:
图中,最右侧部分是工作目录的目录项,中间部分是testdir目录的目录项,通过这种组织结构可以从工作目录跳到testdir目录(i节点2549),也可以从testdir目录回到原先的工作目录(i节点1267)。
i节点一般存放的是一些状态信息,所以影响到i节点的操作有很多,比如更改文件的访问权限,更改用户ID,更改链接数等,一些涉及到文件实际内容相关的操作都不会影响到节点。
3.文件I/O
首先可以来看看Unix系统的体系结构如下图,所谓的内核,通常也是指操作系统(当然一般意义上的操作系统还包含了一些更加易于操作的交互功能等,如果忽略这些次要的东西,操作系统其实就是内核)。从图中可以看出应用程序既可以通过直接系统调用(内核对外提供的接口)来访问内核,也可以通过shell,公用函数库来达到一些目的。
在I/O函数这里,根据程序所使用的方式不同——系统调用还是公用函数库调用,可以将I/O函数分成两类,带缓冲的I/O函数和不带缓冲的I/O函数。(严格意义上讲,传统的UNIX系统在内核中都会设有缓冲高速缓存或页高速缓存,当向文件写数据时,通常都会先把数据放入高速缓存中,然后在将其落盘,而此处的流缓存区是在公用函数库上做了又加了一层缓存区)
-
无缓存IO操作数据流向路径:数据——内核缓存区——磁盘:不带缓冲的I/O函数——open,read,write,lseek和close。
-
标准IO操作数据流向路径:数据——流缓存区——内核缓存区——磁盘:带缓冲的I/O函数——通常是以f为开头的操作函数等,比如fopen,fputs等。
输入输出
文件描述符:内核用一个非负整数来标志一个特定进程正在访问的文件。
每当运行一个程序的时候,所有shell都会为这个程序打开三个文件描述符——标准输入,标准输出,以及标准错误。
/* Standard file descriptors. POSIX 标准中 (unistd.h文件中定义) */ #define STDIN_FILENO 0 /* Standard input.STDIN_FILENO的值是0,表示标准输入文件描述符 */ #define STDOUT_FILENO 1 /* Standard output.STDOUT_FILENO的值是1,表示标准输出文件描述符*/ #define STDERR_FILENO 2 /* Standard error output. */
stdin和STD××_FILENO的区别
-
STDIN_FILENO是系统级API定义的变量,而stdin则是属于标准库处理的输入流
-
数据类型不一样 stdin是FILE*类型的,STDIN_FILENO是int类型。
-
使用stdin的函数主要有:fread、fwrite、fclose等,基本上都以f开头。而使用STDIN_FILENO的函数有:read、write、close等。
文件共享
unix内核使用三种数据结构——进程表项,文件表项和v节点表项来表示打开的文件。
上图表示一个进程打开了标准文件输入(0)、一个标准输出打开(1)等。每个进程都会维护一个进程表项记录了①文件描述符的标志;②指向文件表项的指针。而文件表项则包含①文件状态标志(读、写、追加、阻塞等);②当前文件的偏移量;③v节点指针(v节点指针指向v节点表项)。v节点表项包含了对文件的操作函数指针,同时v节点还包含了指向i节点的指针,i节点的相关介绍由在前面已经说过。
现在如果两个独立的线程打开了同一个文件,这个时候,该文件的v节点表项会被共享,他们所共享的也就是v节点表项,如图,这样就可以维护多个不同的文件打开状态,同时又保证底层数据的空间开销最小,但也会带来一些新的问题,如何避免高并发下的发生文件操作的冲突问题。
4.文件的权限位
这里不再简单的论述九个简单(r,w,x)权限位,而是对其余三个(和三个角色的最后一位执行权限共享位表示)做解释:
-
setuid:设置使文件在执行阶段具有文件所有者的权限。比如一般用户想要更改自己的登入密码,而这个文件是存放在/etc/shadow下的,但是只有root才有权限去读写,这个时候就需要/usr/bin/passwd文件来帮我们完成。
gqx@gqx-Lenovo-Product:~/桌面$ ll /usr/bin/passwd -rwsr-xr-x 1 root root 54256 5月 17 2017 /usr/bin/passwd*
通过执行该命令,系统看到passwd命令文件的属性有小写s后,表示这个命令的属主权限被gqx用户获得,也就是gqx用户获得文件/etc/shadow的读写权限。
-
setgid:该权限只对目录有效。目录被设置该位后,任何用户在此目录下创建的文件都具有和该目录所属的组相同的组。
-
sticky bit(粘着位):该位可以理解为防删除位。 一个文件是否可以被某用户删除,主要取决于该文件所属的组是否对该用户具有写权限。如果没有写权限,则这个目录下的所有文件都不能被删除,同时也不能添加新的文件。 如果希望用户能够添加文件但同时不能删除文件,则可以对文件使用sticky bit位。设置该位后,就算用户对目录具有写权限也不能删除该文件。(比如ubuntu里的目录/tmp和/var/tmp, 除文件所有者外,超级用户也有权限删除,其他用户没有权限)
当前用户可以通过umask(既是C函数,也是shell的一个命令)来控制创建文件的权限:
gqx@gqx-Lenovo-Product:~/shellTest$ umask 0002 gqx@gqx-Lenovo-Product:~/shellTest$ touch a -rw-rw-r-- 1 gqx gqx 0 7月 3 14:14 a gqx@gqx-Lenovo-Product:~/shellTest$ umask 0022 gqx@gqx-Lenovo-Product:~/shellTest$ umask 0022 gqx@gqx-Lenovo-Product:~/shellTest$ touch b gqx@gqx-Lenovo-Product:~/shellTest$ ls -l -rw-rw-r-- 1 gqx gqx 0 7月 3 14:14 a -rw-r--r-- 1 gqx gqx 0 7月 3 14:26 b #创建新文件:读写权限为666-022=644
参考资料:
《UNIX环境高级编程》