一、基本概念
1. shell
- shell:命令解释器,根据输入的命令执行相应命令
- bash(Bourne-Again-SHell)是一个为 GNU 计划编写的 Unix shell
- Linux 默认的 shell:/bin/bash
2. 类 Unix 系统目录结构
- Ubuntu 没有盘符这个概念,只有一个根目录 /,所有文件都在它下面
/:根目录 /bin: bin 是 Binaries (二进制文件) 的缩写, 这个目录存放着最经常使用的命令 /dev : dev 是 Device(设备) 的缩写, 该目录下存放的是 Linux 的外部设备,在 Linux 中访问设备的方式和访问文件的方式是相同的 /home: 用户的主目录,在 Linux 中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的,如上图中的 alice、bob 和 eve /lib: lib 是 Library(库) 的缩写这个目录里存放着系统最基本的动态连接共享库,其作用类似于 Windows 里的 DLL 文件。几乎所有的应用程序都需要用到这些共享库 /media: linux 系统会自动识别一些设备,例如U盘、光驱等等,当识别后,Linux 会把识别的设备挂载到这个目录下 /mnt: 系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了 /opt: opt 是 optional(可选) 的缩写,这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的 /proc: proc 是 Processes(进程) 的缩写,/proc 是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,这个目录是一个虚拟的目录,它是系统内存的映射,可以通过直接访问这个目录来获取系统信息 /root: 该目录为系统管理员,也称作超级权限者的用户主目录 /sbin: s 就是 Super User 的意思,是 Superuser Binaries (超级用户的二进制文件) 的缩写,这里存放的是系统管理员使用的系统管理程序 /tmp: tmp 是 temporary(临时) 的缩写这个目录是用来存放一些临时文件的 /usr: usr 是 unix shared resources(共享资源) 的缩写,这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似于 windows 下的 program files 目录 /usr/bin: 系统用户使用的应用程序 /usr/sbin: 超级用户使用的比较高级的管理程序和系统守护程序
- 目录(directory)
- 是一个包含目录项的文件。在逻辑上,可认为每个目录项都包含一个文件名以及说明该文件属性的信息
- 用户目录:位于 /home/user,称之为用户工作目录或家目录,两种表示方式如下
/home/yue(改为自己的 user name) ~
yxd@yxd-VirtualBox:~$ cd /home/ yxd@yxd-VirtualBox:/home$ cd ~ # cd(同样的效果) yxd@yxd-VirtualBox:~$
3. 文件权限与设定
- 用 ls -l 命令显示的信息中,开头是由 10 个字符构成的字符串
$ ls -l total 236 -rw-r--r-- 1 yxd yxd 0 11月 14 19:05 apitest drwxrwxr-x 2 yxd yxd 4096 7月 31 13:41 bag ... ... -rw-r--r-- 1 yxd yxd 0 11月 13 19:46 my,db -rw-r--r-- 1 yxd yxd 24576 11月 14 15:40 my.db lrwxrwxrwx 1 yxd yxd 13 9月 14 15:37 myfile -> /no/such/file
- 第 1 个字符表示文件类型,可以是下述类型之一
- 普通文件 d 目录 l 符号链接 b 块设备文件 c 字符设备文件 s socket文件,网络套接字 p 管道
- 后 9 个字符表示文件访问权限:第一组表示文件属主的权限、第二组表示同组用户的权限、第三组表示其他用户的权限
r 读 w 写 x 可执行。对于目录,表示进入权限 s 当文件被执行时,把该文件的 UID 或 GID 赋予执行进程的 UID(用户 ID)或 GID(组 ID) t 设置标志位(sticky bit)。如果有 sticky bit 的目录,在该目录下任何用户只要有适当的权限即可创建文 件,但文件只能被超级用户、目录拥有者或文件属主删除。如果是有sticky bit的可执行文件,在该文件执行后, 指向其正文段的指针仍留在内存。这样再次执行它时,系统就能更快地装入该文件。 - 没有相应位置的权限
- chmod:改变文件或目录的访问权限
- 1、文字设定法
# 模板 chmod [who] [+|-|=] [mode] 文件名 # 操作对象 who u 表示 “用户(user)”,即文件或目录的所有者 g 表示 “同组(group)用户”,即与文件属主有相同组 ID 的所有用户 o 表示 “其他(others)用户” a 表示 “所有(all)用户”,它是系统默认值 # 操作符号可以是 + 添加某个权限 - 取消某个权限 = 赋予给定权限并取消其他所有权限(如果有的话) # 设置 mode 所表示的权限可用下述字母的任意组合 r 可读 w 可写 x 可执行
- 2、数字设定法
# 模板 chmod [mode] 文件名 # 0 表示没有权限 # 1 表示可执行权限 # 2 表示可写权限 # 4 表示可读权限 # 设置一个文件允许所有用户可读、可写、可执行 $ chmod 777 file1 user group other r w x r w x r w x 4 2 1 4 2 1 4 2 1 7 7 7
4. Unix 和 Linux 对比
- Unix 操作系统诞生于 1969 年,贝尔实验室发布了一个用 C 语言编写的名为 Unix 的操作系统,该系统可以更快地修改、调整与移植适配
- Unix 操作系统主要在 CLI(命令行界面)上工作
- Unix 的个人用户不多,主要使用集中在商业领域
- 1991 年,芬兰大学生 Linus Torvalds 开发了一个自由的内核(Linux 这个名字来自于其使用的内核名称)
- 1992 年,把 Linux 和 GNU 系统结合起来,就构成一个完整的操作系统:基于 Linux 的 GNU 系统(GNU/linux)
- Linux 并没有使用 Unix 的源码,而是按照公开的 POSIX 标准重新编写
- Unix 和 Linux 对比
- Linux 思想源于 Unix,以 Unix 为原型开发的
- Linux 完全开源,Unix 是版权专有
- Linux 狭义上是指 GNU/Linux 操作系统使用的内核,广义上讲,是一种「类Unix」操作系统;Unix 狭义上是指贝尔实验室最初开发的一整套操作系统,广义上讲是属于符合「Unix」标准的一类操作系统
- Linux 的默认 shell 是 BASH;而 Unix 用的是 Bourne shell
- Linux 是一个开源的操作系统内核(一个 640KB文件,在 /Boot 目录下),而 Ubuntu 是基于 Linux 内核开发的一个完整的操作系统,具体而言,Ubuntu 是基于 Debian 发行版的 Linux 操作系统
5. POSIX 标准
- POSIX(Portable Operating System Interface,可移植操作系统接口)
- POSIX 是 IEEE 为了在各种 UNIX 操作系统上运行的软件而定义的一系列 API 标准的总称
- POSIX.1 已被国际标准化组织(International Standards Organization,ISO)接受,命名:ISO/IEC 9945-1:1990
6. 系统调用和库函数
-
Linux 下对文件操作有两种方式:系统调用(system call)和库函数(Library functions)
- 系统调用是 Linux 内核提供给应用层的应用编程接口(API),是 Linux 应用层进入内核的入口,内核提供了一系列服务、资源、支持等,应用程序通过系统调用 API 函数来使用内核提供的服务、资源以及各种各样的功能,通过系统调用 API 函数,应用层可以实现与内核的交互
- 库函数(Library function)是把函数放到库里,供别人使用的一种方式,库函数调用面向应用开发,可分为两类
- 一类是 C 语言标准规定的库函数
- 一类是编译器特定的库函数
为什么需要库函数?
- 有些系统调用使用起来并不是很方便,于是就出现了 C 语言库,这些 C 语言库函数的设计是为了提供比底层系统调用更为方便、更为好用、且更具有可移植性的调用接口
系统调用流程
- 当应用程序调用 printf() 函数时,printf() 函数会调用 C 库中的 printf(),继而调用 C 库中的 write(),C 库最后调用内核的 write()
- 用户态 –> 系统调用 –> 内核态 –> 返回用户态
-
系统调用和库函数对比
- 库函数属于应用层,而系统调用是内核提供给应用层的编程接口,属于系统内核的一部分
- 库函数运行在用户空间,调用系统调用会由用户空间(用户态)陷入到内核空间(内核态)
- 库函数通常是有缓存的,而系统调用是无缓存的,所以在性能、效率上,库函数通常要优于系统调用
- 库函数调用属于过程调用,运行时间为用户时间,开销较小
- 系统调用需要在用户态和内核态之间切换,运行时间为系统时间,开销较大
- 可移植性:库函数相比于系统调用具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同的,而对于 C 语言库函数来说,由于很多操作系统都实现了 C 语言库,C 语言库在不同的操作系统之间其接口定义几乎是一样的,所以库函数在不同操作系统之间相比于系统调用具有更好的可移植性
7. Linux 应用编程(系统编程)与裸机编程、驱动编程有什么区别?
- 裸机编程:一般把没有操作系统支持的编程环境称为裸机编程环境,如:单片机上的编程开发,编写直接在硬件上运行的程序,没有操作系统支持
- 驱动编程:狭义上 Linux 驱动编程指的是基于内核驱动框架开发驱动程序,驱动开发工程师通过调用 Linux 内核提供的接口完成设备驱动的注册,驱动程序负责底层硬件操作相关逻辑
- 应用编程:基于 Linux 操作系统的应用编程,在应用程序中通过调用系统调用 API 完成应用程序的功能和逻辑,应用程序运行于操作系统之上。通常在操作系统下有两种不同的状态:内核态和用户态,应用程序运行在用户态,而内核则运行在内核态
二、文件 I/O
UNIX 系统中的大多数文件 I/O 只需用到 5 个函数:open、read、write、lseek 以及close
8. 文件描述符
- 文件描述符 (file descriptor) 通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符
- 对一个进程来说,文件描述符是一种有限资源,从 0 开始分配,文件描述符数字最大值为 1023(0~1023),其中 0、1、2 这三个文件描述符已经默认被系统占用:标准输入(0)、标准输出(1)以及标准错误(2)
- 在符合 POSIX.1 的应用程序中,幻数 0、1、2 虽然已被标准化,但应当把它们替换成符号常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 以提高可读性。这些常量都在头文件 <unistd.h> 中定义
- 每一个被打开的文件在同一个进程中都有一个唯一的文件描述符,不会重复,文件被关闭后,它对应的文件描述符将会被释放,然后可以再次分配给其它打开的文件
9. 不带缓冲 I/O 和标准 I/O
- 不带缓冲指的是每个 read 和 write 都调用内核中的一个系统调用
- 函数 open、read、write、lseek 以及 close 提供了不带缓冲的 I/O,这些函数都使用文件描述符,属于 POSIX1 的组成部分
- 标准 I/O 函数为那些不带缓冲的 I/O 函数提供了一个带缓冲的接口,最熟悉的标准 I/O 函数是 printf
10. open、close、read、write 和 lseek
10.1 open
-
功能:打开或创建一个文件
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> // 定义 flags 参数 int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); // 仅当创建新文件时才使用第三个参数,表明文件权限
-
flags:用来说明此函数的多个选项,用以下一个或多个常量进行 “或” 运算构成 flags 参数
- O_RDONLY(只读打开)、O_WRONLY(只写打开)、O_RDWR(读、写打开)、O_EXEC(只执行打开)、O_SEARCH(只搜索打开,用于目录)
- O_APPEND(每次写时都追加到文件末尾,意味着每次写入数据都是从文件末尾开始)
- O_CREAT(若此文件不存在则创建它,与第三个参数 mode 同时使用)
- O_EXCL(如果同时指定了 O_CREAT,而文件已经存在,则出错)
- O_NONBLOCK(为文件的本次打开操作和后续的 I/O 操作设置非阻塞方式)
- O_TRUNC(如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为 0)
-
函数返回值
- 若成功,返回文件描述符
- 若出错,返回 -1
-
创建文件时,指定文件访问权限 mode,权限同时受 umask 影响。结论为
- 文件权限 = mode & ~umask
$ umask 0002 # 表明默认创建文件权限为 ~umask = 775(第一个 0 表示八进制)
多次打开同一个文件的情况
- 一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符
- 一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件(见 13. 静态文件和动态文件)
- 一个进程内多次 open 打开同一个文件,不同文件描述符所对应的读写位置偏移量是相互独立的
10.2 close
- 功能:关闭一个打开文件
#include <unistd.h> int close(int fd);
- 函数返回值
- 若成功,返回 0
- 若出错,返回 -1
- 关闭一个文件时还会释放该进程加在该文件上的所有记录锁
- 当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用 close 关闭打开文件
10.3 read
- 功能:从打开文件中读数据
#include <unistd.h> // ssize_t 表示带符号整型;void* 表示通用指针 // 参数1:文件描述符;参数2:存数据的缓冲区;参数3:缓冲区大小 ssize_t read(int fd, void *buf, size_t count);
- 函数返回值
- 若 read 成功,则返回读到的字节数,若已到文件尾,返回 0
- 若出错,返回 -1
- 若返回 -1,并且 errno = EAGIN 或 EWOULDBLOCK,说明不是 read 失败,而是 read 在以非阻塞方式读一个设备文件/网络文件,并且文件无数据
10.4 write
- 功能:向打开文件写数据
#include <unistd.h> // 参数1:文件描述符;参数2:待写出数据的缓冲区;参数3:数据大小 ssize_t write(int fd, const void *buf, size_t count);
- 函数返回值
- 若 write 成功,则返回已写的字节数(返回值通常与参数 count 值相同,否则表示出错)
- 若出错,返回 -1
- write 出错的一个常见原因是:磁盘已写满,或者超过了一个给定进程的文件长度限制
10.5 lseek
-
功能:显式的为一个打开文件设置偏移量
#include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence);
-
对参数 offset 的解释与参数 whence 的值有关
- 若 whence 是 SEEK_SET,则将该文件的偏移量设置为距文件开始处 offset 个字节
- SEEK_SET(0) 绝对偏移量
- 若 whence 是 SEEK_CUR,则将该文件的偏移量设置为其当前值加 offset,offset 可正可负
- SEEK_CUR(1) 相对于当前位置的偏移量
- 若 whence 是 SEEK_END,则将该文件的偏移量设置为文件长度加 offset,offset 可正可负
- SEEK_END(2) 相对文件尾端的偏移量
- 若 whence 是 SEEK_SET,则将该文件的偏移量设置为距文件开始处 offset 个字节
-
lseek 仅将当前的文件偏移量记录在内核中,它并不引起任何 I/O 操作。然后,该偏移量用于下一个读或写操作
10.6 空洞文件
- lseek() 系统调用允许文件偏移量超出文件长度,如:有一个 test_file,文件大小 4K(4096 个字节),若通过 lseek() 系统调用将该文件的读写偏移量移动到偏移文件头部 6000 个字节处,接下来使用 write() 函数对文件进行写入操作
- 此时将是从偏移文件头部 6000 个字节处开始写入数据,意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件
- 文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分大小的
- 空洞文件作用:对多线程共同操作文件是有用的,有时候创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长时间,有一种思路就是:将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入,最后将他们连接起来
11. 什么是预读入缓输出机制
- 大多数文件系统为改善性能都采用某种预读入 (read ahead) 缓输出技术。当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很快就会读这些数据
- read/write:每次写一个字节,会不断的进行内核态和用户态的切换,所以非常耗时
- fgetc/fputc:有个 4096 缓冲区,所以不是一个字节一个字节地写,内核和用户切换就比较少(预读入缓输出机制)
12. 什么是 inode
- 磁盘在进行分区、格式化的时候会将其分为两个区域
- 一个是数据区,用于存储文件中的数据
- 另一个是 inode 区,用于存放 inode table(inode 表)
- inode table 中存放的是一个一个的 inode(也称 inode 节点),不同的 inode 表示不同的文件,每一个文件都必须对应一个 inode,inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件不同信息,如:文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳(创建时间、更新时间等)、文件类型、文件数据存储的 block(块)位置等信息,如下图所示(文件名并不记录在 inode 中)
- inode table 表本身也需要占用磁盘的存储空间。每一个文件都有唯一的一个 inode,每一个 inode 都有一个与之相对应的数字编号,通过这个数字编号就可以找到 inode table 中所对应的 inode
13. 静态文件和动态文件
- 文件在没有被打开的情况下一般都是存放在磁盘中的,如:电脑硬盘、移动硬盘、U 盘等外部存储设备,文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,称为静态文件
- 当调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并将静态文件的数据内容从存储设备(磁盘)中读取到内存中进行管理、缓存,把内存中的这份文件数据叫做动态文件(内核缓冲区)
为什么需要内存中的动态文件?
- 因为磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,而块设备硬件有读写限制等特征:块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以对块设备的读写操作非常不灵活
- 而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常灵活,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,内存的读写速率远比磁盘读写快得多
14. 什么是进程控制块
- Linux 系统中,内核会为每个进程设置一个专门的数据结构用于管理该进程,如:记录进程的状态信息、运行特征等,把这个称为进程控制块(Process control block,缩写 PCB)
- PCB 数据结构体中有一个指针指向了文件描述符表,文件描述符表中的每一个元素索引到对应的文件表
- 文件表也是一个数据结构体,其中记录了很多文件相关的信息,如:文件状态标志、引用计数、当前文件的读写偏移量以及 i-node 指针(指向该文件对应的 inode)等
- 进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表
15. exit 、_exit 、_Exit
- 在 Linux 系统下,进程(程序)退出可以分为正常退出和异常退出
- 在 Linux 系统下,进程正常退出除了可以使用 return 之外,还可以使用 exit()、_exit() 以及 _Exit()
- main 函数中使用 return 后返回,return 执行后把控制权交给调用函数,结束该进程
- 调用 _exit()(_Exit() 与其等价)函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程,将控制权交给操作系统
- exit() 是一个标准 C 库函数,_exit() 和 _Exit() 是系统调用
- 在 Linux 系统下,进程正常退出除了可以使用 return 之外,还可以使用 exit()、_exit() 以及 _Exit()
16. 复制文件描述符
- Linux 系统中,open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件描述符
- 使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作
- 复制得到的文件描述符和旧的文件描述符拥有相同的权限
- 复制得到的文件描述符与旧的文件描述符都指向了同一个文件表
- Linux 系统下,可以使用 dup 或 dup2 这两个系统调用对文件描述符进行复制
dup 分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件描述符,这是 dup 系统调用的缺陷;而 dup2 系统调用修复了这个缺陷,可以手动指定文件描述符,而不需要遵循文件描述符分配原则
17. 文件共享
- 文件共享指的是同一个文件(如:磁盘上的同一个文件,对应同一个 inode)被多个独立的读写体同时进行 IO 操作
- 多个独立的读写体:可以将其简单地理解为对应于同一个文件的多个不同的文件描述符。如:多次打开同一个文件所得到的多个不同的 fd,或使用 dup()(或 dup2)函数复制得到的多个不同的 fd 等
- 同时进行 IO 操作:一个读写体操作文件尚未调用 close 关闭的情况下,另一个读写体去操作文件
例:同一个文件对应两个不同的文件描述符 fd1 和 fd2,当使用 fd1 对文件进行写操作之后,并没有关闭 fd1,而此时使用 fd2 对文件再进行写操作,这其实就是一种文件共享
- 文件共享的意义
- 用于多进程或多线程编程环境中,如:可以通过文件共享的方式来实现多个线程同时操作同一个大文件,以减少文件读写时间、提升效率
隐患:文件共享存在着竞争冒险
- 文件共享三种实现方式
- 同一个进程中多次调用 open 函数打开同一个文件
- 不同进程中分别使用 open 函数打开同一个文件
- 同一个进程中通过 dup(dup2)函数对文件描述符进行复制
18. 竞争冒险与原子操作
- 竞争冒险
- 操作共享资源的两个进程(或线程),其操作之后所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争状态(也称为竞争冒险)
- 如何规避或消除竞争状态?
- 采用原子操作。所谓原子操作,是由多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤,不可能只执行所有步骤中的一个子集(即 lseek 和 write 要么同时执行完,要么一个都不执行)
- 实现原子操作的 2 种方法
- O_APPEND 实现原子操作
- 当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,“移动当前写位置偏移量到文件末尾、写入数据” 这两个操作步骤就组成了一个原子操作,加入 O_APPEND 标志后,不管怎么写入数据都会是从文件末尾写
- pread() 和 pwrite()
- pread() 和 pwrite() 都是系统调用,与 read()、write() 函数的作用一样。区别在于,pread() 和 pwrite() 可用于实现原子操作,调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数,用于指定文件当前读或写的位置偏移量,所以调用 pread 相当于调用 lseek 后再调用 read;同理,调用 pwrite 相当于调用 lseek 后再调用 write
- O_APPEND 实现原子操作
19. 标准 I/O 和文件 I/O 的区别
- 标准 I/O 是标准 C 库函数,而文件 I/O 则是系统调用
- 标准 I/O 是由文件 I/O 封装而来,标准 I/O 内部实际上是调用文件 I/O 来完成实际操作的
- 可移植性:标准 I/O 相比于文件 I/O 具有更好的可移植性
- 通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同,如:系统调用的定义、功能、参数列表、返回值等往往都不同
- 而对于标准 I/O 来说,由于很多操作系统都实现了标准 I/O 库,标准 I/O 库在不同的操作系统之间其接口定义几乎是一样的,所以标准 I/O 在不同操作系统之间相比于文件 I/O 具有更好的可移植性
- 性能、效率:标准 I/O 库在用户空间维护了自己的 stdio 缓冲区,所以标准 I/O 是带有缓存的,而文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O
20. FILE 指针
- FILE 指针的作用相当于文件描述符,FILE 指针用于标准 I/O 库函数中,而文件描述符则用于文件 I/O 系统调用中
- 所有文件 I/O 函数(open()、read()、write()、lseek()等)都是围绕文件描述符进行的,当调用 open()函数打开一个文件时,即返回一个文件描述符 fd,然后该文件描述符就用于后续的 I/O 操作
- 而对于标准 I/O 库函数来说,它们的操作是围绕 FILE 指针进行的,当使用标准 I/O 库函数打开或创建一个文件时,会返回一个指向 FILE 类型对象的指针(FILE *),使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作
21. I/O 缓冲
背景
- 出于速度和效率的考虑,系统 I/O 调用(即文件 I/O,open、read、write 等)和标准 C 语言库 I/O 函数
(即标准 I/O 函数)在操作磁盘文件时会对数据进行缓冲- 文件 I/O 内核缓冲区和 stdio 缓冲区之间的联系与区别
- 首先,应用程序调用标准 I/O 库函数将用户数据写入到 stdio 缓冲区中,stdio 缓冲区是由 stdio 库所维护的用户空间缓冲区
- 然后,针对不同的缓冲模式,当满足条件时,stdio 库会调用文件 I/O(系统调用 I/O)将 stdio 缓冲区中缓存的数据写入到内核缓冲区中,内核缓冲区位于内核空间
- 最终,由内核向磁盘设备发起读写操作,将内核缓冲区中的数据写入到磁盘(或者从磁盘设备读取数据到内核缓冲区)
21.1 文件 I/O 内核缓冲
- read() 和 write() 系统调用在进行文件读写操作时并不会直接访问磁盘设备,而是在用户空间缓冲区和内核缓冲区之间复制数据,如:调用 write() 后只是将这 5 个字节数据拷贝到了内核空间的缓冲区中,拷贝完成之后函数就返回了,在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中
write(fd, "Hello", 5); // 写入 5 个字节数据
- 对于读文件而言亦是如此,内核会从磁盘设备中读取文件数据并存储到内核缓冲区中,当调用 read() 读取数据时,read() 调用将从内核缓冲区中读取数据,直至把缓冲区中的数据读完,然后内核会将文件的下一段内容读入到内核缓冲区中进行缓存,把这个内核缓冲区就称为文件 I/O 的内核缓冲
- 文件 I/O 的内核缓冲区的设计目的
- 提高文件 I/O 的速度和效率
- 减少内核操作磁盘的次数
21.2 直接 I/O:绕过内核缓冲
- Linux 允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O
- 例如,某应用程序的作用是测试磁盘设备的读写速率,那么在这种应用需求下,就需要保证 read/write 操作是直接访问磁盘设备,而不经过内核缓冲
- 对于大多数应用程序而言,使用直接 I/O 可能会大大降低性能
- 因为直接 I/O 涉及到对磁盘设备的直接访问,所以在执行直接 I/O 时,必须要遵守三个对齐限制要求
- 应用程序中用于存放数据的缓冲区,其内存起始地址必须以块大小的整数倍进行对齐
- 写文件时,文件的位置偏移量必须是块大小的整数倍
- 写入到文件的数据大小必须是块大小的整数倍
21.3 stdio 缓冲
- 标准 I/O 是 C 语言标准库函数,而文件 I/O 是系统调用,虽然标准 I/O 是在文件 I/O 基础上进行封装而实现,但在效率、性能上,标准 I/O 要优于文件 I/O,原因在于标准 I/O 维护了自己的缓冲区,称为 stdio 缓冲区
- 文件 I/O 内核缓冲,这是由内核维护的缓冲区,而标准 I/O 所维护的 stdio 缓冲是用户空间缓冲区
- 当应用程序中通过标准 I/O 操作磁盘文件时,标准 I/O 函数会将用户写入或读取文件的数据缓存在 stdio 缓冲区,然后再一次性将 stdio 缓冲区中缓存的数据通过调用系统调用 I/O(文件 I/O)写入到文件 I/O 内核缓冲区或拷贝到应用程序的 buf 中
- stdio 缓冲的问题:标准输出默认采用的是行缓冲模式,printf() 输出的字符串写入到了标准输出的 stdio 缓冲区中,只有输出换行符时(不考虑缓冲区填满的情况)才会将这一行数据刷入到内核缓冲区,也就是写入标准输出文件(终端设备)
- 第一个 printf 包含了换行符,所以已经刷入了内核缓冲区
- 第二个 printf 没有包含换行符,所以输出的 “Hello World!” 还缓存在 stdio 缓冲区中,需要等待一个换行符才可输出到终端
三、文件和目录
22. Linux 系统中的文件类型
- 1、普通文件:普通文件(regular file)也就是一般意义上的文件
- 文本文件
- 文件中的内容是由文本构成的(本质上都是数字),所谓文本指的是 ASCII 码字符
- 如常见的 .c、.h、.sh、.txt 等这些都是文本文件,文本文件的好处就是方便人阅读、浏览以及编写
- 二进制文件:
- 二进制文件中存储的本质上也是数字,只不过对于二进制文件来说,这些数字并不是文本字符编码,而是真正的数字
- 如 Linux 系统下的可执行文件、C 代码编译之后得到的 .o 文件、.bin 文件等都是二进制文件
- 文本文件
- 2、目录文件:目录(directory)就是文件夹,文件夹在 Linux 系统中是一种特殊文件
- 目录(文件夹)是由 inode 节点和目录块所构成(普通文件由 inode 节点和数据块构成)
- 目录(文件夹)作为一种特殊文件,并不适合使用文件 I/O 的方式来读写
- 3、字符设备文件和块设备文件
- 设备文件(字符设备文件、块设备文件)对应的是硬件设备,在 Linux 系统中,硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控、使用硬件设备,如:LCD 显示屏、串口、音频、按键等
- 设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失
- 字符设备文件一般存放在 Linux 系统 /dev/ 目录下,所以 /dev 也称为虚拟文件系统 devfs
- 4、符号链接文件
- 符号链接文件(link)类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作
- 如:读取一个符号链接文件内容时,实际上读到的是它指向的文件的内容
- 符号链接文件的后面箭头所指向的文件路径便是符号链接文件所指向的文件
- 符号链接文件(link)类似于 Windows 系统中的快捷方式文件,是一种特殊文件,它的内容指向的是另一个文件路径,当对符号链接文件进行操作时,系统根据情况会对这个操作转移到它指向的文件上去,而不是对它本身进行操作
- 5、管道 FIFO 文件
- 管道文件(pipe)主要用于进程间通信
- 6、套接字文件
- 套接字文件(socket)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进程间通信,实际上就是网络通信
23. stat、fstat 和 lstat
- stat 函数将返回与此命名文件有关的信息结构,struct stat 是内核定义的一个结构体,在 <sys/stat.h> 头文件中申明,可以在应用层使用,这个结构体中的所有元素加起来构成了文件的属性信息
- fstat 与 stat 区别在于,stat 是从文件名出发得到文件属性信息,不需要先打开文件,而 fstat 函数则是从文件描述符出发得到文件属性信息,所以使用 fstat 函数之前需要先打开文件得到文件描述符
- lstat 与 stat、fstat 的区别在于,对于符号链接文件,stat、fstat 查阅的是符号链接文件所指向的文件对应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息
- stat 会拿到符号链接指向的那个文件或目录的属性,不想穿透符号就用 lstat
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> int stat(const char *pathname, struct stat *buf); int fstat(int fd, struct stat *buf); int lstat(const char *pathname, struct stat *buf);
24. 符号链接(软链接)与硬链接
- 在 Linux 系统中有两种链接文件,分为软链接(也叫符号链接)文件和硬链接文件
- 软链接文件是 Linux 系统下的七种文件类型之一,其作用类似于 Windows 下的快捷方式
- 使用 ln 命令可以为一个文件创建软链接文件或硬链接文件
硬链接:ln 源文件 链接文件
软链接:ln -s 源文件 链接文件
24.1 硬链接
- 硬链接文件与源文件拥有相同的 inode 号,既然 inode 相同,也就意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已,创建出来的硬链接文件与源文件对文件系统来说是完全平等的关系
- inode 数据结结构中会记录文件的链接数,这个链接数指的就是硬链接数
- 当为文件每创建一个硬链接,inode 节点上的链接数就会加一,每删除一个硬链接,inode 节点上的链接数就会减一,直到为 0,inode 节点和对应的数据块才会被文件系统所回收,也就意味着文件已经从文件系统中删除
- 源文件本身就是一个硬链接文件
#include <unistd.h> // oldpath:用于指定被链接的源文件路径,应避免 oldpath 参数指定为软链接文件,为软链接文件创建硬链接没有意义,虽然并不会报错 // newpath:用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误 int link(const char *oldpath, const char *newpath);
24.2 软链接
- 软链接文件与源文件有不同 inode 号,所以也就意味着它们之间有不同的数据块,但软链接文件的数据块中存储的是源文件的路径名,链接文件可以通过这个路径找到被链接的源文件
- 它们之间类似于一种 “主从” 关系,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径,这种链接文件被称为悬空链接
- inode 节点中记录的链接数并未将软链接计算在内
#include <unistd.h> // target:用于指定被链接的源文件路径,target 参数指定的也可以是一个软链接文件 // linkpath:用于指定硬链接文件路径,如果 newpath 指定的文件路径已存在,则会产生错误 int symlink(const char *target, const char *linkpath); // pathname:需要读取的软链接文件路径。只能是软链接文件路径,不能是其它类型文件,否则调用函数将报错 // buf:用于存放路径信息的缓冲区 // bufsiz:读取大小,一般读取的大小需要大于链接文件数据块中存储的文件路径信息字节大小 // 返回值:失败将返回-1,并会设置 errno;成功将返回读取到的字节数 ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
24.3 对比
- 硬链接存在一些限制情况
- 不能对目录创建硬链接(超级用户可以创建,但必须在底层文件系统支持的情况下)
- 硬链接通常要求链接文件和源文件位于同一文件系统中
- 软链接并没有上述限制条件
- 可以对目录创建软链接
- 可以跨越不同文件系统
- 可以对不存在的文件创建软链接
25. 删除文件 unlink 和 remove
- unlink() 用于删除一个文件(不包括目录)
- link 函数,用于创建一个硬链接文件,创建硬链接时,inode 节点上的链接数就会增加;unlink() 的作用与 link() 相反,unlink() 系统调用用于移除/删除一个硬链接(从其父级目录下删除该目录条目)
- unlink() 系统调用实质上是移除 pathname 参数指定的文件路径对应的目录项(从其父级目录中移除该目录项),并将文件的 inode 链接计数减 1,如果该文件还有其它硬链接,则仍可通过其它链接访问该文件的数据;只有当链接计数变为 0 时,该文件的内容才可被删除,只要有进程打开了该文件,其内容也不能被删除
- unlink() 系统调用并不会对软链接进行解引用操作,若 pathname 指定的文件为软链接文件,则删除软链接文件本身,而非软链接所指定的文件
- remove() 是一个 C 库函数,用于移除一个文件或空目录
- 如果 pathname 参数指定的是一个非目录文件,则 remove() 调用 unlink()
- 如果 pathname 参数指定的是一个目录,那么 remove() 调用 rmdir()
- remove() 同样不对软链接进行解引用操作,若 pathname 参数指定的是一个软链接文件,则 remove()会删除链接文件本身、而非所指向的文件