第一章 应用编程概念
1.1 系统调用
系统调用(system call)其实是 Linux 内核提供给应用层的应用编程接口(API)。通过系统调用 API,应用层可以实现与内核的交互。
应用编程与裸机编程、驱动编程有什么区别?
就拿嵌入式 Linux 硬件平台下的软件开发来说,可将编程分为裸机编程、Linux 驱动编程、Linux 应用编程。
没有操作系统支持的编程环境称为裸机。
通过内核驱动框架开发驱动程序称为驱动开发,驱动程序负责底层硬件相关逻辑。
应用程序运行于操作系统之上,通过调用系统调用 API 完成应用程序的功能和逻辑。
驱动程序属于内核的一部分,当操作系统启动的时候会加载驱动程序,比如点亮 LED 的驱动程序中,仅仅实现点亮/熄灭 LED 硬件操作相关逻辑代码,应用程序可通过系统调用 API函数 write 控制 LED 亮灭;
LED 应用程序与 LED 驱动程序是分离的,它们单独编译。应用程序运行在操作系统之上,有操作系统支持,应用程序处于用户态,而驱动程序处于内核态,与纯裸机程序存在着质的区别。
Linux一切皆文件,外设也作为文件,给一个设备号,设备类,抽象化到设备树上进行文件管理。
1.2 库函数
系统调用是内核直接向应用层提供的应用编程接口,譬如 open、write、read、close 等。
编写应用程序除了使用系统调用之外,还可以使用库函数。
库函数也就是 C 语言库函数,C 语言库是应用层使用的一套函数库,在 Linux 下,通常以动态库文件(.so)的形式提供,存放在根文件系统/lib 目录下,大多数库函数是由系统调用封装而来。
库函数和系统调用的区别:
1、库函数属于应用层,系统调用属于系统内核。
2、库函数运行在用户空间,系统调用会由用户空间(用户态)进入内核空间(内核态)。
3、库函数通常有缓存,系统调用无缓存。
4、库函数相比于系统调用具有更好的可移植性。
不同操作系统的内核向应用层提供的系统调用往往都不同,但很多操作系统都实现了C语言库。因此库函数比系统调用具有更好的移植性。
1.3 标准 C 语言函数库
在 Linux 系统下 , 使用的 C语言库为 GNU C 语言函数库,也叫作 glibc,作为Linux下的标准C语言函数库。
确定 Linux 系统的 glibc 版本
C 语言库是以动态库文件的形式提供的,通常存放在/lib 目录,它的命名方式通常是libc.so.6,不过这个是一个软链接文件,它会链接到真正的库文件。
可以看到 libc.so.6 链接到了 libc-2.23.so 库文件,2.23 表示的就是这个 glibc 库的版本号为 2.23。除此之外,我们还可以直接运行该共享库来获取到它的信息。
1.4 main 函数
在 Linux 应用程序中,main 函数也是作为应用程序的入口函数存在,main 函数的形参一般有有参和无参两种写法。
int main(int argc, char **argv)
argc代表参数个数。argv代表字符串个数,argv[0]固定为程序自身路径。
./hello 112233
那么此时参数个数为 2,并且这些参数都是作为字符串的形式传递给 main 函数:
argv[0]等于"./hello"。argv[1]等于"112233"。
第二章 文件I/O基础
2.1 一个简单的文件 IO 示例
一个通用的 IO 模型通常包括打开文件、读写文件、关闭文件这些基本操作,主要涉及到 4 个函数:open()、read()、write()、close()。
Linux 下有 3 大帮助方法:help、man、info。
help只能查看命令的用法。man可以查看命令的详细介绍。info是最详细的介绍。
2.2 文件描述符
调用 open 函数返回一个int类型的数据,成功时该值就是一个文件描述符(file descriptor)。对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引。
在 Linux 系统中,一个进程可以打开的文件数有限制。超过最大打开文件数限制,内核将会发送警告信号给对应的进程,然后结束进程。
我们可以通过 ulimit 命令来查看进程可打开的最大文件数,用法如下所示:
ulimit -n
对于进程来讲,文件描述符是一种有限资源,从 0 开始分配的,逐个递增,文件描述符最大值为 1023。
每个被打开的文件在同一个进程中都有唯一的文件描述符,文件关闭后对应的文件描述符被释放。每次分配文件描述符都从最小的未被使用的开始。
调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始。因为 0、1、2 这三个文件描述符已分配给了标准输入(0)、标准输出(1)、标准错误(2)。
2.3 open 打开文件
open(文件路径,操作方式,文件权限);
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/*返回文件描述符*/
int open(const char *pathname,//文件路径
int flags, //打开文件的方式。读写/读/写/追加/覆盖/创建并打开。宏。
mode_t mode);//创建时设置文件的权限。宏。
/*
文件打开方式:
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
O_CREAT 不存在则创建
O_EXCL 存在则报错。需配合O_CREAT使用
*/
文件权限 mode_t是一个u32无符号整型。低12位每3bit一组。
S---这 3 个 bit 位用于表示文件特殊权限。通常为0。
U---这 3 个 bit 位用于表示文件所属用户权限。
G---这 3 个 bit 位用于表示同组用户权限,即与文件所有者有相同组 ID 的用户;
O---这 3 个 bit 位用于表示其他用户权限。
八进制表示,最高权限0777。读4写2执行1。用的时候也是用的宏。允许所有者读、写、执行,允许同组读、写、执行,允许其他用户读、写、执行。
2.4 write 写文件
write(文件描述符,数据缓冲区,要写入的字节数);
#include <unistd.h>
/* 写入,返回成功写入的字节数 出错返回-1 */
ssize_t write(int fd, //文件描述符
const void *buf, //写入数据缓冲区
size_t count); //写入的字节数
cat 输入文件.txt > 输出文件.txt //一个文件的内容输入到另一个文件
my_var="变量内容" //自定义变量
echo "$my_var" > 文件名.txt //写入文件,覆盖
echo "xxx" >> 文件名.txt //写入文件,追加
2.5 read 读文件
read(文件描述符,数据缓冲区,要读出的字节数);
#include <unistd.h>
/* 返回成功读到的字节数 */
ssize_t read(int fd, //文件描述符
void *buf, //数据缓冲区
size_t count); //读出的字节数
2.6 close 关闭文件
close(文件描述符);
#include <unistd.h>
/*关闭文件描述符*/
int close(int fd);
2.7 lseek 读写偏移量
每个打开的文件,系统都会记录它的读写偏移量,用来记录了文件当前的读写位置。
调用 read()或 write()函数对文件进行读写操作时,从当前读写偏移量进行数据读写。
#include <sys/types.h>
#include <unistd.h>
/* 设置文件读写偏移量,成功则返回以头部为基准的偏移值 */
off_t lseek(int fd, //文件描述符
off_t offset, //偏移量。可正可负。
int whence); //偏移量的参考值
/*
偏移量参考值:
SEEK_SET offset以头部为基准
SEEK_CUR offset以当前位置偏移量为基准
SEEK_END offset以文件末尾为基准
*/
第三章 深入探究文件 I/O
3.1 Linux 系统如何管理文件
3.1.1 静态文件与 inode
文件在没有被打开时一般都存放在磁盘中,譬如电脑硬盘、移动硬盘、U 盘等外部存储设备,文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为静态文件。
像这里的硬盘、磁盘一般都指的是机械硬盘(HDD)。
机械硬盘以"块"和"扇区"为单位组织存储空间。一个"块"4KB,最常见的是分为8个扇区,每个扇区512字节(0.5KB)。
对于操作系统而言,不管是SSD还是HDD,都是以[块]为单位进行读写和擦除的。但是在硬盘内部可能使用NOR FALSH \ NAND FALSH或者磁盘,需要细分。
由此可知,静态文件对应的数据都是存储在磁盘设备不同的"块"中。
我们的磁盘在进行分区、格式化的时候会分为两个区域,
一个是数据区,用于存储文件中的数据;
一个是 inode 区,用于存放 inode table(inode 表)。
inode table 中存放的是 inode节点,实质上是一个结构体,用属性元素记录了文件了不同信息,譬如文件字节大小、文件所有者、文件对应的读/写/执行权限、文件时间戳、文件类型、文件数据存储的 block(块) 位置等等信息。
index node,inode,索引节点
inode table 表本身也需要占用磁盘的存储空间。每个文件都有唯一的 inode,每个 inode 都有与之对应的数字编号,通过这个数字编号就可以找到 inode table 中对应的 inode。
在 Linux 系统下,我们可以通过"ls -i"命令查看文件的 inode 编号,如下所示:
还可以使用 stat 命令查看:
像WINDOS操作系统中的快速格式化选项,快速格式化就是只删除了磁盘的inode表,数据区的数据并没有动。
通过以上介绍可知,打开一个文件,系统内部会将这个过程分为三步:
1) 系统找到这个文件名所对应的 inode 编号;
2) 通过 inode 编号从 inode table 中找到对应的 inode 结构体;
3) 根据 inode 结构体的文件属性元素,找到文件数据区所在的 block,读出数据。
3.1.2 文件打开时的状态
调用 open 函数去打开文件时,内核会申请一段缓冲区,将静态文件的数据内容从磁盘读取到内存中进行缓存(内存中的这份文件数据也被叫做动态文件、内核缓冲区)。
对这个文件的读写操作,其实都是针对内存中这份内核缓冲区进行操作。
因为磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,块设备硬件本身有读写限制等特征,是以块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,导致对块设备的读写操作非常不灵活;
而内存SRAM可以按字节为单位来操作,且可以随机操作任意地址数据,非常地很灵活.
所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,因为内存的读写速率快且操作灵活。
内核使用进程控制块PCB管理进程。每个进程都有一个专门的PCB,用于记录进程的状态进行、运行特征等信息。
PCB 结构体中有一个指针指向文件描述符表。文件描述符表记录了进程打开的所有文件的文件描述符,每个文件描述符指向对应的文件表。文件表结构体记录了文件相关的信息,譬如文件状态标志、引用计数、当前文件的读写偏移量以及该文件的 i-node 指针等。
进程,
PCB控制块,
文件描述符表,
文件描述符,
文件表,
inode指针 根据文件/目录又分数据块/文件块
inode表中的 inode(指向数据区的数据块Block)
Process control block,PCB。
3.1.3 返回错误处理与 errno
Linux 系统对常见错误做了编号,每个编号代表不同的错误类型。
函数执行发生错误的时候,操作系统会将错误编号赋值给 errno 变量,每一个进程都维护了自己的 errno 变量,用于存储就近发生的函数的错误编号,errno是一个全局变量,下一次的错误码会覆盖上一次的错误码。
errno 本质上是一个 int 类型的变量,用于存储错误编号。并不是所有的系统调用或 C 库函数出错时操作系统都会设置 errno。可以通过man手册查看函数的帮助信息。
3.1.4 strerror 函数
errno 只是一个错误编号,strerror() 可以将对应的 errno 转换成适合我们查看的字符串信息。
#include <string.h>
/*返回值:对应错误编号的字符串描述信息 errnum:错误编号 errno。*/
char *strerror(int errnum);
3.1.5 perror 函数
perror() 函数会自动获取 errno 变量的值并直接将错误提示字符串打印出来,而不是返回字符串,除此之外还可以在输出的错误提示字符串之前加入自己的打印信息
#include <stdio.h>
/* 让函数自动获取错误码,并打印出来 可在打印前添加自己的信息 */
void perror(const char *s);
3.3 exit、_exit、_Exit
进程退出可以分为正常退出和异常退出,异常并不是执行函数出现了错误,更多的是不可预料的系统异常,可能是执行了某个函数时发生的、也有可能是收到了某种信号等,这里我们只讨论正常退出的情况。
进程正常退出除了可以使用 return 之外,还可以使用 exit()、_exit() 以及 _Exit()。
3.3.1 _exit()和_Exit()函数
main 函数中 return 执行后会把控制权交给系统调用_exit(),结束该进程。
_exit() 函数会清除函数使用的内存空间,关闭进程的所有文件描述符,并结束进程,将控制权交给操作系统。
#include <unistd.h>
//系统调用,释放进程 状态标志,0为正常结束,其他代表进程执行过程有错误
void _exit(int status);
_Exit() 和 _exit() 都是系统调用,用法相同。
#include <stdlib.h>
void _Exit(int status);
3.3.2 exit()函数
exit()函数、_exit()、_Exit() 函数都是用来终止进程的。
exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。
当调用 exit() 时,它会按注册的反向顺序执行所有通过 atexit() 或 on_exit() 注册的清理函数,释放资源并保存状态。然后,它会刷新并关闭 I/O 流的缓冲区,确保所有输出都被写入。
最后,exit() 会调用 _exit() 系统调用来终止进程、关闭文件描述符,并传递一个状态码给操作系统,以供父进程或其他工具捕获。
exit() 是C库函数,执行更多的清理工作,包括刷新缓冲区、调用注册的清理函数。
_exit() 是一个简单的系统调用,它会立即停止进程的执行,关闭所有文件描述符,但不会刷新缓冲区。
#include <stdlib.h>
void exit(int status);
3.4 空洞文件
3.4.1 概念
lseek() 系统调用,可以修改文件的当前读写位置偏移量,且允许文件读写偏移量超出文件长度。假如文件偏移量超过了文件长度,比如偏移量6000,文件长度4000,那么中间400~6000字节之间就被称为文件空洞。
文件空洞部分并不占用任何物理空间,直到在某个时刻对空洞部分写入数据时才会为它分配空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的。
空洞文件对多线程共同操作文件及其有用的,有时候我们创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长的时间,有一种思路就是将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入。
比如迅雷下载,比如给虚拟机分配磁盘空间。
ls 指令查看到的空洞文件大小是逻辑大小。du 命令查看到的是实际大小。
若使用 read 函数读取文件空洞部分,读取出来的将会是用0填充的数据。
3.5 O_APPEND 和 O_TRUNC 标志
open打开文件的操作除了只读,只写,读写,不存在则创建,存在则报错以外。
还有追加、全丢弃。
3.6 多次打开同一个文件
3.6.1 基本概念
同一个文件可以被多次打开,在一个进程中多次打开同一个文件、在多个不同的进程中打开同一个文件,那么这些操作都是被允许的。
一个进程内多次 open 打开同一个文件,那么会得到多个不同的文件描述符 fd,同理在关闭文件的时候也需要调用 close 依次关闭各个文件描述符。
一个进程内多次 open 打开同一个文件,在内存中并不会存在多份动态文件。
一个进程内多次 open 打开同一个文件,不同文件描述符的读写位置偏移量相互独立。
由于读写位置偏移量独立,因此每个文件描述符各写各的。
要追加写可以使用O_APPEND操作。open使用追加写操作得到的文件描述符,在使用write时会自动把读写偏移量移动到文件的末尾。
3.7 复制文件描述符
在 Linux 系统下,可以使用 dup 或 dup2 这两个系统调用对文件描述符进行复制。
复制成功之后可以得到一个新的文件描述符,新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,拥有相同的权限。
复制得到的文件描述符与旧的文件描述符指向同一个文件表。
3.7.1 dup 函数
dup 函数用于复制文件描述符。
#include <unistd.h>
//复制文件描述符,失败返回-1
int dup(int oldfd);
3.7.2 dup2 函数
dup 系统调用分配的文件描述符是由系统分配的,不能自己指定, dup2 系统调用可以手动指定文件描述符,不需要遵循文件描述符分配原则。
#include <unistd.h>
//复制文件描述符,手动指定描述符编号,失败返回-1
int dup2(int oldfd, int newfd);
3.8 文件共享
文件共享指同一个文件(对应同一个 inode)被多个独立的文件描述符同时进行 IO 操作。
常见的三种文件共享的实现方式:
(1) 同一个进程中多次调用 open 函数使用不同的文价描述符操作同一个文件。
(2) 不同进程中分别使用 open 函数打开同一个文件。
(3) 同一个进程中通过 dup、dup2函数对文件描述符进行复制。
3.9 原子操作与竞争冒险
3.9.1 竞争冒险简介
竞争冒险不但存在于 Linux 应用层、也存在于 Linux 内核驱动层。
假设有两个进程 A 和进程 B 都对同一个文件在末尾写入数据,每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志。每个进程都有它自己的进程控制块 PCB,有自己的文件表,也就会有独立的读写偏移量,且共享同一个 inode 节点。
两个线程如果从同样的位置开始写,可能会导致覆盖,出现未知的结果。
3.9.2 原子操作
(1) O_APPEND 实现原子操作
当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据。这里“移动写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作,
(2) pread() 和 pwrite()
pread()和 pwrite()都是系统调用,可以理解成实现了原子操作的 read()、write()函数,用于读取和写入数据。
调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数来指定文件当前读或写的位置偏移量,所以调用 pread/pwrite 相当于调用 lseek 后再调用 read/write。
#include <unistd.h>
/* 原子读 返回成功读到的字节数*/
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
/* 原子写 返回成功写入的字节数*/
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
(3) 创建一个文件
open 函数的 O_EXCL 标志用于文件存在的时候返回错误,使得测试和创建两者成为一个原子操作。可以避免创建文件时的竞争冒险情况(两个线程都先打开,打开失败,然后创建)。
3.10 fcntl 和 ioctl
3.10.1 fcntl 函数
fcntl() 函数可以对文件描述符执行一系列控制操作。比如复制一个文件描述符(与 dup、dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。
#include <unistd.h>
#include <fcntl.h>
/*对文件描述符进行操作 失败返回-1并设置errno 成功返回值与命令相关*/
int fcntl(int fd, //文件描述符
int cmd, //操作命令
... /* arg */ )
/*
复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC)
获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD)
获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL);
获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN)
获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK)
*/
3.10.2 ioctl 函数
ioctl() 可以认为是一个文件 IO 操作的工具箱。一般用于操作特殊文件或硬件外设。比如可以通过 ioctl 获取 LCD 相关信息等。
#include <sys/ioctl.h>
/* io操作的工具箱 成功返回0,失败返回-1*/
int ioctl(int fd,
unsigned long request, //操作命令
...);
3.11 截断文件
使用系统调用 truncate() 或 ftruncate() 可将普通文件截断为指定字节长度。
ftruncate()使用文件描述符 fd 来指定目标文件,而 truncate()则直接使用文件路径 path 来指定目标文件,功能都是截断文件到指定长度。
#include <unistd.h>
#include <sys/types.h>
/*截断文件,文件路径*/
int truncate(const char *path, //文件路径
off_t length); //截断的长度,字节
/*截断文件,文件描述符*/
int ftruncate(int fd, off_t length);
如果文件长度不足,则将扩展,填充'\0'。
第四章 标准 I/O 库
标准IO对文件IO进行了封装,文件IO属于系统调用。
4.1 标准 I/O 库简介
标准 I/O 库是标准 C 库中用于文件 I/O 操作相关的一系列库函数的集合,通常标准 I/O 库函数相关的函数定义都在头文件<stdio.h>中。
标准 I/O 库函数是构建于文件 I/O(open()、read()、write()、lseek()、close()等)这些系统调用之上的,fopen()、fread()、fwrite()、flseek()、fclose()。
标准IO和文件IO区别:
- 标准 I/O 是标准 C 库函数,而文件 I/O 则是 Linux系统调用;
- 标准 I/O 是由文件 I/O 封装而来,内部实际上是调用文件 I/O 来完成操作;
- 标准 I/O 相具有更好的可移植性。不同的操作系统内核提供的系统调用往往不同,而很多操作系统都实现了标准 I/O 库。标准 I/O 库在不同的操作系统之间其接口定义几乎一样。
- 标准 I/O 库在用户空间维护了自己的 stdio 缓冲区。所以标准 I/O 是有缓存的,而文件 I/O 在用户空间是不带有缓存的,所以在性能上标准 I/O 要优于文件 I/O。
4.2 FILE 指针
文件 I/O 函数(open()、read()、write()、lseek()等)都是围绕文件描述符进行的。
标准 I/O 函数的操作是围绕 FILE 指针进行的,当使用标准 I/O 库函数打开或创建一个文件时,会返回一个指向 FILE 类型对象的指针(FILE *)。
FILE 结构体包含了标准 I/O 管理文件所需要的信息,包括用于实际I/O 的文件描述符、文件缓冲区指针、缓冲区长度、当前缓冲区中的字节数以及出错标志等。
4.3 标准输入、标准输出和标准错误
标准输入设备就是键盘,标准输出设备就是屏幕,标准错误设备指显示错误信息的设备。
进程的文件描述符0、1、2是被标准输入、标准输出、标准错误默认占用的。每个进程启动之后都会默认打开标准输入、标准输出以及标准错误。
在应用编程中可以使用宏 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 分别代表 0、1、2,这些宏定义在 unistd.h 头文件中。
/* 文件描述符 */
#define STDIN_FILENO 0 /* 标准输入. */
#define STDOUT_FILENO 1 /* 标准输入. */
#define STDERR_FILENO 2 /* 标准错误. */
在标准 I/O 中,自然是无法使用文件描述符来对文件进行 I/O 操作的,它们需要围绕 FILE 类型指针来进行,在 stdio.h 头文件中有相应的定义。
/* 标准流 */
extern struct _IO_FILE *stdin; /* 标准输入流 */
extern struct _IO_FILE *stdout; /* 标准输出流 */
extern struct _IO_FILE *stderr; /* 标准错误流 */
/* C89/C99 规范定义宏 */
#define stdin stdin
#define stdout stdout
#define stderr stderr
struct _IO_FILE 结构体就是 FILE 结构体,使用了 typedef 进行了重命名。
所以在标准 I/O 中,可以使用 stdin、stdout、stderr 来表示标准输入、标准输出、标准错误。
4.4 打开文件 fopen()
#include <stdio.h>
/*打开文件 文件路径 操作模式*/
/* 成功返回FILE*,失败返回NULL */
FILE *fopen(const char *path, const char *mode);
4.5 读文件和写文件
#include <stdio.h>
/*返回成功写入/读出的数据项个数*/
/* 读文件 读缓冲区指针 读数据项个数 读数据项大小 FILE* */
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
/* 写文件 写缓冲区指针 写数据项个数 写数据项大小 FILE* */
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
4.6 fseek 定位
库函数 fseek()用于设置标准IO的读写偏移量。
#include <stdio.h>
/*设置标准IO的读写偏移量 成功返回0,失败返回-1*/
int fseek(FILE *stream, //FILE*
long offset, //偏移量
int whence); //偏移基准。文件头、文件尾、当前位置
ftell()函数
库函数 ftell()用于获取文件当前的读写位置偏移量。
#include <stdio.h>
/*获取当前读写偏移量 失败返回-1*/
long ftell(FILE *stream);
可以利用fseek()移动读写偏移量到文件末尾,然后ftell()获取文件字节数,得到文件大小。
4.7 检查或复位状态
调用 fread()读取数据时,如果返回值小于读取数据项大小参数,表示发生了错误或者已经到了文件末尾,但 fread()无法具体确定是哪一种情况;此时可以通过判断错误标志或 end-of-file 标志来确定具体的情况。
4.7.1 feof()函数
库函数 feof()用于测试参数 stream 所指文件的 end-of-file 标志,如果 end-of-file 标志被设置了,则调用 feof()函数将返回一个非零值,否则返回 0。
#include <stdio.h>
/* 测试File* 的end-of-file标志 被设置了返回非0,否则0 */
int feof(FILE *stream);
end-of-file 标志用来标识读写偏移量是否到达文件末尾。
4.7.2 ferror()函数
库函数 ferror()用于测试参数 stream 所指文件的错误标志,如果错误标志被设置了,则调用 ferror()函数将返回一个非零值,否则返回 0。
#include <stdio.h>
/*测试FILE*所指文件的错误标志是否被设置,设置了返回非0,否则返回0*/
int ferror(FILE *stream);
4.7.3 clearerr()函数
库函数 clearerr()用于清除 end-of-file 标志和错误标志,当调用 feof()或 ferror()校验这些标志后,通常需要清除这些标志,避免下次校验时使用到的是上一次设置的值。
#include <stdio.h>
/*清除FILE*的错误标志和end-of-file标志*/
void clearerr(FILE *stream);
调用 fseek()函数成功时也会清除文件的 end-of-file 标志。
4.8 格式化 I/O
格式化输出指将格式化数据写入到标准输出。包括:
printf()、fprintf()、dprintf()、sprintf()、snprintf()这 5个库函数。
格式化输入将指格式化数据写入到标准输入。包括:
scanf()、fscanf()、sscanf()这三个库函数。
4.8.1 格式化输出
C 库函数提供了 5 个格式化输出函数,包括:
printf()、fprintf()、dprintf()、sprintf()、snprintf(),其函数定义如下所示:
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *buf, const char *format, ...);
int snprintf(char *buf, size_t size, const char *format, ...);
这 5 个函数都是可变参函数,它们都有一个共同的参数 format,这是一个字符串,称为格式控制字符串,用于指定后续的参数如何进行格式转换。
printf() 函数用于将格式化数据写入到标准输出。
dprintf() 用于将格式化数据写入到指定的文件中,使用文件描述符 fd 指定文件。
fprintf() 用于将格式化数据写入到指定的文件中,使用FILE指针指定文件。
sprintf()、snprintf()函数可将格式化的数据存储在用户指定的缓冲区 buf 中。区别在于后者多了个参数指定写入的字节数,避免缓冲区溢出。
printf用的FILE* 写入到标准输出stdout,属于库函数。底层涉及系统调用write,写入到设备文件。
4.8.2 格式化输入
C 库函数提供了 3 个格式化输入函数,包括:
scanf()、fscanf()、sscanf()。
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
这 3 个格式化输入函数也是可变参函数,它们都有一个共同的参数 format,同样也称为格式控制字符串,用于指定输入数据如何进行格式转换。
scanf()函数可将用户标准输入的数据进行格式化转换。
fscanf()函数从 FILE 指针指定文件中读取数据,并将数据进行格式化转换。
sscanf()函数从参数 str 所指向的字符串中读取数据,并将数据进行格式化转换。
scanf用的FILE* 写入到标准输入stdin,属于库函数。底层涉及系统调用read,从设备文件读出。
4.9 I/O 缓冲
系统 I/O 调用(即文件 I/O,open、read、write 等)和标准 C 语言库 I/O 函数(即标准 I/O 函数)在操作磁盘文件时会对数据进行缓冲。
4.9.1 文件 I/O 的内核缓冲
read()和 write() 作为系统调用,在进行文件读写操作的时候不会直接访问磁盘设备,而是仅在用户空间缓冲区和内核缓冲区之间复制数据。
比如调用 write()函数将 5 个字节数据从用户空间内存拷贝到内核空间的缓冲区中:
write(fd, "Hello", 5); //写入 5 个字节数据
调用 write()后仅仅只是将这 5 个字节数据从用户缓冲区拷贝到了内核缓冲区中,拷贝完成之后函数就返回了。内核会在某个时刻将缓冲区中的数据刷新到磁盘设备中。由此可知,系统调用 write()与磁盘操作并不是同步的,write()函数不会等待数据真正写入到磁盘之后再返回。
具体是什么时间点写入到磁盘是不确定的,由内核根据相应的存储算法自动判断。
如果在内核缓冲区的数据刷新到磁盘之前,有进程调用 read() 来读磁盘,那么系统会直接从内核缓冲区取数据。
同理,读文件也是如此,内核会从磁盘设备中读取文件的数据并存储到内核的缓冲区中,
当调用 read()函数读取数据时,read()调用将从内核缓冲区中读取数据,直至把缓冲区中的数据读完,这时,内核会将文件的下一段内容读入到内核缓冲区中进行缓存。
4.9.2 刷新文件 I/O 的内核缓冲区
强制将文件 I/O 内核缓冲区中缓存的数据写入(刷新)到磁盘设备中,对于某些应用程序来说是很有必要的。比如当我们在 Ubuntu 系统下拷贝文件到 U 盘时,文件拷贝完成之后,通常在拔掉 U 盘之前,需要执行 sync 命令进行同步操作,这个同步操作其实就是刷新内核缓冲区。
控制文件 I/O 内核缓冲的系统调用
系统调用 sync()、syncfs()、fsync()、fdatasync() 可用于控制文件 I/O 内核缓冲。
(1)fsync()函数
将参数 fd 所指文件内核缓冲的内容数据和元数据写入磁盘,写入完成之后才返回。
#include <unistd.h>
/*将参数fd所指文件的内容数据和元数据写入磁盘,写完才返回。成功返回0,失败返回-1*/
int fsync(int fd);
(2)fdatasync()函数
将参数 fd 所指文件内核缓冲的内容数据写入磁盘,不包括元数据,写入完成之后才返回。
#include <unistd.h>
/*将参数fd所指文件的内容数据写入磁盘,不包括文件的元数据*/
int fdatasync(int fd);
(3)sync()函数
将所有文件内核缓冲区的内容数据和元数据都写入磁盘。
#include <unistd.h>
/*将所有文件内核缓冲区的数据内容和元数据都写入磁盘*/
void sync(void);
在 Linux实现中,调用 sync()函数仅在所有数据已经写入到磁盘设备之后才会返回;然后在其它系统中,sync()实现只是简单调度一下 I/O 传递,在动作未完成之后即可返回。
控制文件 I/O 内核缓冲的标志
调用 open()函数时指定一些标志也可以影响到文件 I/O 内核缓冲。
1、O_DSYNC 标志
效果类似于在每个 write()调用之后调用 fdatasync()函数进行数据同步。
会在每个write()之后将缓冲区指定文件的内容数据刷新进磁盘。
fd = open(filepath, O_WRONLY | O_DSYNC);
2、O_SYNC 标志
效果类似于在每个 write()调用之后调用 fsync()函数进行数据同步。
会在每个write()之后将缓冲区指定文案金的内容数据和元数据刷新进磁盘。
由于频繁刷盘对性能影响极大,因此很少用到。
4.9.3 直接 I/O:绕过内核缓冲
Linux 2.4及以上版本允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O(direct I/O)或裸 I/O(raw I/O)。
比如测试磁盘设备的读写速率,我们需要保证 read/write 操作是直接访问磁盘设备,而不经过内核缓冲,否则必然会导致测试结果出现比较大的误差。
使用直接 I/O 可能会大幅降低性能,因为内核针对文件 I/O 内核缓冲区做了不少的优化,包括按顺序预读取、在成簇磁盘块上执行 I/O、允许访问同一文件的多个进程共享高速缓存的缓冲区。直接 I/O 只在一些特定的需求场合,譬如磁盘速率测试工具、数据库系统等。
open文件时指定 O_DIRECT 标志可以在读写文件时使用直接IO。
fd = open(filepath, O_WRONLY | O_DIRECT);
直接 I/O 的对齐限制
执行直接 I/O 时,必须要遵守以下三个对齐限制要求:
1、应用程序中用于存放数据的变量内存起始地址必须是块大小的整数倍;
2、写文件时,文件的位置偏移量必须是块大小的整数倍;
3、写入到文件的数据大小必须是块大小的整数倍。
static 静态数组 buf,将其作为数据存放的缓冲区,在变量定义后加了__attribute((aligned (4096)))修饰,使其起始地址以 4096 字节进行对其。
如果不满足以上任何一个要求,调用 write()均为以错误返回 Invalid argument。
块大小指的是磁盘设备的物理块大小(block size),常见的块大小包括 4096 字节、2048 字节、1024 以及 512 字节。
可以通过 tune2fs 命令查看块所占字节大小。
tune2fs -l /dev/sda1 | grep "Block size"
-l 后面指定了需要查看的磁盘分区,可以使用 df -h 命令查看 Ubuntu 系统的根文件系统所挂载的磁盘分区。
4.9.4 stdio 缓冲
stdio缓冲就是用户空间缓冲。
虽然标准 I/O 是对文件 I/O 的封装,但在效率上标准 I/O 要优于文件 I/O,因为标准 I/O 维护了自己的缓冲区,stdio 缓冲区。每个FILE*(文件描述符)都有自己的stdio缓冲区。
当应用程序通过标准 I/O 操作磁盘文件时,为了减少调用系统调用的次数,标准 IO 函数会将用户写入或读取的文件数据缓存在 stdio 缓冲区,然后一次性缓存的数据通过系统调用(文件IO)写入到内核缓冲区或者拷贝到应用程序的 buf 中。
对 stdio 缓冲进行设置
库函数提供的对stdio缓冲区进行设置的函数包括,
setvbuf(FILE* fd,char* buf,mode,size):
设置文件描述符的stdio缓冲区。
可指定缓冲区地址、大小、模式。模式有无缓冲、行缓冲、全缓冲。
setbuf(FILE* fd,char* buf):
设置文件描述符的stdio缓冲区。
可指定缓冲区地址。指定了就是全缓冲,否则无缓冲。
setbuffer(FILE* fd,char* buf,size)。
设置文件描述符的stdio缓冲区。
可指定缓冲区地址、大小。指定了就是全缓冲,否则无缓冲。
1、setvbuf()函数
对文件的 stdio 缓冲区进行设置,比如缓冲区的缓冲类型、缓冲区大小、起始地址等。
#include <stdio.h>
/*对stdio缓冲区进行设置,包括缓冲区类型,缓冲区大小,起始地址 成功返回0失败返回非0*/
int setvbuf(FILE *stream, //文件描述符
char *buf, //不为NULL。则指定stdio缓冲区起始地址,大小为size
int mode, //缓冲区类型
size_t size);
/*
参数 mode 用于指定缓冲区的缓冲类型:
_IONBF 无缓冲。每个标准IO不经过缓冲直接调用write/read等文件IO
_IOLBF 采用行缓冲 I/O。标准IO输入输出遇到\n,标准IO才执行文件IO。
_IOFBF 采用全缓冲 I/O。填满stdio缓冲区后才进行文件IO。
*/
2、setbuf()函数
没有返回值。可指定缓冲区首地址。指定了就是全缓冲;没指定就是无缓冲。
#include <stdio.h>
/* 设置stdio缓冲区,buf不为NULL就是全缓冲,buf为NULL就是无缓冲 */
void setbuf(FILE *stream, char *buf);
相当于
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
3、setbuffer()函数
可指定缓冲区首地址和缓冲区大小。
#include <stdio.h>
/*设置stdio,允许指定缓冲区大小。指定了缓冲区地址就是全缓冲,没指定就是无缓冲*/
void setbuffer(FILE *stream, char *buf, size_t size);
标准输出 printf()的行缓冲模式测试
printf("Hello World!\n");
printf("Hello World!");
//注意程序退出时也会刷新缓冲区,所以上面测试加个循环不让退出
while(1);
printf()函数是标准 I/O 库函数,向标准输出,输出打印信息,编译测试:
发现只有第一个加了"\n"的成功打印出来,这是因为标准输出默认是行缓冲,遇到"\n"时才刷新到内核缓冲区,否则要等缓冲区满。
格式化输入 scanf()函数,用户通过键盘输入数据,只有在按下回车键(换行符键)时程序才会接着往下执行,因为标准输入默认也是采用了行缓冲模式。
由于stdout是标准输出的文件描述符,因此可以用setvbuf去修改标准输出的模式。
刷新 stdio 缓冲区
无论行缓冲还是全缓冲,都可以使用库函数 fflush(FILE*) 来强制刷新缓冲区。其原理是用了系统调用write()把用户空间缓冲区内容写到内核缓冲区。
#include <stdio.h>
/* 强制调用write刷新 不指定文件描述符,则刷新进程的全部文件*/
int fflush(FILE *stream);
通常文件关闭时、程序退出时、缓冲区满时也会自动刷新缓冲区。
fclose(stdio);
4.10 文件描述符与 FILE 指针互转
在同一个文件上执行 I/O 操作时,还可以将文件 I/O与标准 I/O 混合使用。借助于库函数 fdopen()、fileno() 可以实现文件描述符与FILE 指针互转。
#include <stdio.h>
//将FILE *转文件描述符
int fileno(FILE *stream);
//将文件描述符转File指针,并指定FILE指针的缓冲区类型(无,行,全)
FILE *fdopen(int fd, const char *mode);
当混合使用文件 I/O 和标准 I/O 时,需要特别注意缓冲的问题,文件 I/O 会直接将数据写入到内核缓冲区进行高速缓存,而标准 I/O 会先将数据写入到 stdio 缓冲区,之后再调用 write()将 stdio 缓冲区中的数据写入到内核缓冲区。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("print");
write(STDOUT_FILENO, "write\n", 6);
exit(0);
}
上面这段代码使用混合IO对标准输出FILE* stdout,也就是文件描述符STDOUT_FILENO进行操作, 结果是先打印文件io操作的内容,因为文件io直接写进内核缓冲区,标准io要从用户缓冲区到内核缓冲区。
第五章 文件属性与目录
5.1 Linux 系统中的文件类型
Linux 下一切皆文件。
Linux 系统下一共分为 7 种文件类型。
普通文件、
目录文件、
字符设备文件、
块设备文件、
符号链接文件、
管道文件、
套接字文件。
5.1.1 普通文件
普通文件(regular file)在 Linux 系统下是最常见的,比如文本文件、二进制文件。
普通文件可以分为两大类:文本文件和二进制文件。文本文件对应的是ASCII码字符,二进制文件像.o、.bin等。
在 Linux 系统下,可以通过 stat 命令或者 ls -l 命令来查看文件类型。
' - ':普通文件
' d ':目录文件
' c ':字符设备文件
' b ':块设备文件
' l ':符号链接文件
' s ':套接字文件
' p ':管道文件
5.1.2 目录文件
目录就是文件夹,文件夹也是一种文件。我们也可以使用 vi 编辑器来打开文件夹,文件夹的内容就是文件夹自身的路径和所存放的文件。
5.1.3 字符设备文件和块设备文件
硬件设备会对应到一个设备文件,应用程序通过对设备文件的读写来操控硬件设备,譬如 LCD 显示屏、串口、音频、按键等。
硬件设备分为字符设备和块设备。
设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失。
设备文件一般存放在 /dev/目录下,所以/dev 也称为虚拟文件系统 devfs。
5.1.4 符号链接文件
符号链接文件(link)类似于快捷方式,它的内容指向的是另一个文件路径。对符号链接文件的操作其实是对它指向路径的操作。
5.1.5 管道文件
管道文件(pipe)主要用于进程间通信。
5.1.6 套接字文件
套接字文件(socket)也是一种进程间通信的方式,与管道文件不同的是,它们可以在不同主机上的进程间通信,实际上就是用于网络通信。
5.2 stat 函数
使用 stat 命令查看文件的属性,其实这个命令内部就是通过调用 stat()函数来获取文件属性的,stat 函数是 Linux 中的系统调用,用于获取文件相关的信息。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
/* 查看文件属性 */
int stat(const char *pathname,//文件路径
struct stat *buf); //struct stat类型指针,用来记录结果
5.2.1 struct stat 结构体
struct stat 结构体中的所有元素加起来构成了文件的属性信息。
struct stat
{
dev_t st_dev; /* 文件所在设备的 ID */
ino_t st_ino; /* 文件对应 inode 节点编号 */
mode_t st_mode; /* 文件对应的模式,包括文件权限、文件类型等 */
nlink_t st_nlink; /* 文件的链接数 */
uid_t st_uid; /* 文件所有者的用户 ID */
gid_t st_gid; /* 文件所有者的组 ID */
dev_t st_rdev; /* 设备号(指针对设备文件) */
off_t st_size; /* 文件大小(以字节为单位) */
blksize_t st_blksize; /* 文件内容存储的块大小 */
blkcnt_t st_blocks; /* 文件内容所占块数 */
struct timespec st_atim; /* 文件最后被访问的时间 */
struct timespec st_mtim; /* 文件内容最后被修改的时间 */
struct timespec st_ctim; /* 文件状态最后被改变的时间 */
};
5.2.2 st_mode 变量
st_mode 是 structstat 结构体中的一个成员变量,是一个 32 位无符号整形数据,该变量记录了文件的类型、文件的权限这些信息。
低16位用了4位记录文件类型。八进制,三位表示。
12位对应特殊权限、用户权限、组权限、其他权限。八进制,三位表示。
权限就是读、写、执行。对应4+2+1。
可用 stat.st_mode&权限的宏定义 来判断是否具有某权限。
S_IRWXU 00700 owner has read, write, and execute permission
S_IRUSR 00400 owner has read permission
S_IWUSR 00200 owner has write permission
S_IXUSR 00100 owner has execute permission
S_IRWXG 00070 group has read, write, and execute permission
S_IRGRP 00040 group has read permission
S_IWGRP 00020 group has write permission
S_IXGRP 00010 group has execute permission
S_IRWXO 00007 others (not in group) have read, write, and execute permission
S_IROTH 00004 others have read permission
S_IWOTH 00002 others have write permission
S_IXOTH 00001 others have execute permission
文件类型这4个bit位也有对应的宏定义。
S_IFSOCK 0140000 socket(套接字文件)
S_IFLNK 0120000 symbolic link(链接文件)
S_IFREG 0100000 regular file(普通文件)
S_IFBLK 0060000 block device(块设备文件)
S_IFDIR 0040000 directory(目录)
S_IFCHR 0020000 character device(字符设备文件)
S_IFIFO 0010000 FIFO(管道文件)
Linux也提供了封装好的,用来判断文件st_mode变量是否属于某文件类型,是否具有某权限的宏。
S_ISREG(m) #判断是不是普通文件,如果是返回 true,否则返回 false
S_ISDIR(m) #判断是不是目录,如果是返回 true,否则返回 false
S_ISCHR(m) #判断是不是字符设备文件,如果是返回 true,否则返回 false
S_ISBLK(m) #判断是不是块设备文件,如果是返回 true,否则返回 false
S_ISFIFO(m) #判断是不是管道文件,如果是返回 true,否则返回 false
S_ISLNK(m) #判断是不是链接文件,如果是返回 true,否则返回 false
S_ISSOCK(m) #判断是不是套接字文件,如果是返回 true,否则返回 false
5.2.3 struct timespec 结构体
该结构体定义在<time.h>头文件中,是 Linux 系统中时间相关的结构体。
struct timespec
{
time_t tv_sec; /* 秒 */
syscall_slong_t tv_nsec; /* 纳秒 */
};
time_t 其实指的就是 long int 类型。
该结构体所表示的时间可以精确到纳秒。
time_t 时间在 Linux下被称为日历时间,指的是一个时间段,从某一个时间点到某一个时间点所经过的秒数。
可以通过 localtime()/localtime_r()或者 strftime()把秒数转成24h制。
5.3 fstat 和 lstat 函数
stat 是从文件名出发得到文件属性信息,不需要先打开文件。
fstat 是从文件描述符出发得到文件属性信息,需要先打开文件得到文件描述符。
lstat 查阅符号链接文件的时候,通过路径查阅的是符号链接文件本身的属性信息。
5.3.1 fstat 函数
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, //文件描述符
struct stat *buf); //文件属性 struct stat
5.3.2 lstat 函数
lstat()与 stat、fstat 的区别在于,对于符号链接文件,stat、fstat 查阅的是符号链接文件所指向的文件对应的文件属性信息,而 lstat 查阅的是符号链接文件本身的属性信息。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
/*查文件stat结构体信息,对链接文件查的是链接文件自身属性*/
int lstat(const char *pathname, struct stat *buf);
5.4 文件所属组
5.4.1 用户ID和组ID
文件有用户ID(UID)和组ID(GID)。
使用 touch 命令创建了一个文件,那么这个文件的所有者就是调用命令的用户;同理,在程序中调用 open 函数创建新文件时也是如此,执行该程序的用户是谁,其文件所有者便是谁。
Linux 系统会为每一个用户或用户组分配一个 ID,将用户名或用户组名与对应的 ID 关联起来,所以系统通过用户 ID(UID)或组 ID(GID)就可以识别出不同的用户和用户组。
文件的用户 ID 和组 ID 分别由 struct stat 结构体中的 st_uid 和 st_gid 所指定。
进程也有用户ID和组ID。进程的用户ID和组ID分为实际ID和有效ID。
实际ID标识区分进程属于谁,有效ID用来在对文件进行权限检查时使用。
进程对文件是否具有xx权限,其实指的是执行该进程的用户是否具有文件的xx权限。
5.4.2 chown 函数
chown 是一个系统调用,该系统调用可用于改变文件的 UID 和 GID 。
#include <unistd.h>
/* 更改文件所有者,所属组 */
int chown(const char *pathname,
uid_t owner,
gid_t group);
linux 也有 chown 命令。
// 所有者:所属组
sudo chown root:root testApp.c
只有超级用户能更改文件的用户ID。
普通进程能更改文件的组ID,前提是进程的有效用户ID和文件的用户ID一致。也就是进程和文件属于同一个用户。
5.4.3 fchown 和 lchown 函数
chown,修改文件UID和GID,通过文件路径修改
fchown,修改文件UID和GID,通过文件描述符修改
lchown,修改文件UID和GID,通过文件路径修改
5.5 文件访问权限
struct stat 结构体中的 st_mode 字段记录了文件的访问权限位。见5.2.2 st_mode变量介绍。
5.5.1 普通权限和特殊权限
文件的权限可以分为两个大类,分别是普通权限和特殊权限(附加权限)。
普通权限包括对文件的读、写以及执行,而特殊权限则包括一些对文件的附加权限,比如Set-User-ID、Set-Group-ID以及Sticky。
普通权限
每个文件都有用户、组、其他三类所属对象,每类所属对象有读、写、执行,共 9种权限。
特殊权限
st_mode 字段中除了记录文件的 9 个普通权限之外,还记录了文件的 3 个特殊权限。
当进程对文件进行操作的时候、将进行权限检查。
如果文件的 set-user-ID 位权限被设置,内核会将进程的有效 ID 设置为该文件的用户 ID,意味着该进程直接获取了文件所有者的权限。
如果文件的 set-group-ID 位权限被设置,内核会将进程的有效组 ID 设置为该文件的组 ID,意味着该进程直接获取了文件所属组的权限。
Sticky位别管,写书的也不了解。
5.5.2 目录权限
目录也有读、写、执行权限。
读权限:可列出(譬如:通过 ls 命令)目录之下的内容(即目录下有哪些文件)。
写权限:可以在目录下创建文件、删除文件。
执行权限:可访问目录下的文件,譬如对目录下的文件进行读、写、执行等操作。
5.5.3 检查文件权限 access
文件的权限检查不单检查文件本身的权限,还要检查文件所在目录的权限。
程序当中对文件进行操作之前,检查执行进程的用户是否具有该文件的操作权限,可以使用access()系统调用,检查是否存在/可读/可写/可执行:
#include <unistd.h>
/* 检查文件是否具有某权限 */
int access(const char *pathname,
int mode);//检查类型
/*
F_OK 是否存在
R_OK 是否可读
W_OK 是否可写
X_OK 是否可执行
*/
5.5.4 修改文件权限 chmod
使用 chmod 命令修改文件权限,该命令内部实现方法其实是系统调用 chmod 函数,通过路径找到文件。
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
fchmod 函数
fchmod和chmod的区别在于使用文件描述符操作文件。
#include <sys/stat.h>
int fchmod(int fd, mode_t mode);
5.5.5 umask函数
umask 命令用于查看/设置权限掩码,权限掩码主要用于对新建文件的权限进行屏蔽。
权限掩码的表示方式与文件权限的表示方式相同,但是需要去除特殊权限位。umask 不能对特殊权限位进行屏蔽。
当新建文件时,文件实际的权限并不等于我们所设置的权限,比如:调用 open 函数新建文件时,文件实际的权限并不等于 mode 参数所描述的权限,而是通过将权限掩码取反后相与。
mode & ~umask
调用 open 函数新建文件时,mode 参数指定为 0777,假设 umask 为 0002,实际权限为:
0777 & (~0002) = 0775
Linux 系统提供了 umask 函数用于设置进程的权限掩码,该函数是一个系统调用。进程的 umask 通常继承至其父进程。
#include <sys/types.h>
#include <sys/stat.h>
/*设置进程的权限掩码*/
mode_t umask(mode_t mask);
5.6 文件的时间属性
st_atim 文件最后访问时间。
st_mtim 文件内容最后修改时间。
st_ctim 文件状态最后改变时间。
inode 中包含了很多文件信息,譬如:文件字节大 小、文件所有者、文件对应的读/写/执行权限、文件时间戳(时间属性)、文件数据存储的 block (块)等。
最后改变时间其实指的就是文件的inode节点里面的任一属性被改变的时间。
5.6.1 utime()、utimes()修改时间属性
系统调用显式的修改文件的时间属性。
#include <sys/types.h>
#include <utime.h>
/*修改文件的访问时间、修改时间,精确到秒。给NULL代表当前时间*/
int utime(const char *filename,
const struct utimbuf *times);
#include <sys/time.h>
/*修改文件的访问时间,修改时间,精确到微秒。给NULL代表当前时间*/
int utimes(const char *filename,
const struct timeval times[2]);
来看看 struct utimbuf 结构体:
struct utimbuf {
time_t actime; /* 访问时间 */
time_t modtime; /* 内容修改时间 */
};
ime_t 类型其实就 是 long int 类型,所以这两个时间是以秒为单位的。
utime()函数设置文件的时间属性精度只能到秒。
来看看 struct timeval 结构体:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒 */
};
5.6.2 futimens()、utimensat()修改时间属性
这两个系统调用相对于 utime 和 utimes 函数有以下三个优点:
可按纳秒级精度设置时间戳。相对于提供微秒级精度的 utimes(),这是重大改进!
可单独设置某一时间戳。比如,只设置访问时间、而修改时间保持不变,utime()或 utimes() 需要将访问时间、修改时间一同指定。
5.7 符号链接(软链接)与硬链接
链接文件分为软连接(符号链接)和硬链接。
使用上两者没有任何区别,都与正常的文件访问方式一样,支持读、写以及执行。
创建硬链接:ln 源文件 链接文件
创建软链接:ln -s 源文件 链接文件
使用 ln 命令创建的两个硬链接文件与源文件 test_file 都拥有相同的 inode 号,意味着它们指向了物理硬盘的同一个区块,仅仅只是文件名字不同而已。
inode 数据结构中会记录文件的链接数,这个链接数指的就是硬链接数。
struct stat 结构体中的 st_nlink 成员变量就记录了文件的链接数。
每删除一个硬链接,inode 节点上的链接数就会减一,直到为 0,inode 节点和对应的数据块才会被文件系统所回收,也就意味着文件已经从文件系统中被删除了。
源文件 test_file 本身就是一个硬链接文件。
软链接文件与源文件有着不同的 inode 号,意味着软链接之间有着不同的数据块。
软链接文件的数据块中存储的是源文件的路径名,当源文件被删除之后,软链接文件依然存在,但此时它指向的是一个无效的文件路径,这种链接文件被称为悬空链接。
inode节点和struct stat不会记录软链接数。
5.7.1 创建链接文件
除了命令行 ln 和 ln -s,还可以可以使用系统调用创建硬链接文件或软链接文件。
创建硬链接 link()
#include <unistd.h>
int link(const char *oldpath, //源文件
const char *newpath);//硬链接
创建软链接 symlink()
#include <unistd.h>
int symlink(const char *target,
const char *linkpath);
5.7.2 读取软链接文件
open可以打开文件硬链接获得文件描述符,但是不能打开软链接。
系统调用 readlink() 可以用来读取软链接。
#include <unistd.h>
ssize_t readlink(const char *pathname, //软链接路径
char *buf, //存放读取内容的地址
size_t bufsiz); //读取的字节数
5.8 目录
目录(文件夹)是一种特殊文件,同样可以使用前面给大家介绍 open、 read 等这些系统调用以及 C 库函数对其进行操作,但是目录作为一种特殊文件,并不推荐这样。
5.8.1 目录存储形式
普通文件由 inode 节点和数据块构成。数据块属于inode节点,记录文件信息。
目录由 inode 节点和目录块构成。目录块属于inode节点,记录目录信息。
目录块记录了目录下各个文件的文件名和 对应的 inode编号。
5.8.2 创建和删除目录
用于创建目录 mkdir()以及删除空目录 rmdir() 相关的系统调用。
mkdir 函数
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname,//目录路径
mode_t mode); //目录权限
rmdir 函数
#include <unistd.h>
/*删除空目录*/
int rmdir(const char *pathname);
5.8.3 打开、读取以及关闭目录
库函数 opendir()、 readdir()和 closedir() 用来打开、读取以及关闭目录。
打开目录 opendir
#include <sys/types.h>
#include <dirent.h>
/* 打开目录,返回该目录的句柄。失败返回NULL*/
DIR *opendir(const char *name);
一个 DIR 指针(DIR结构体指针),其作用类似于open函数返回的文件描述符fd,后续对该目录的操作需要使用该DIR指针变量。
读取目录 readdir
readdir()是库函数,但实际上系统还提供了一个readdir系统调用。用于读取目录,获取目录下所有的文件名称和对应的inode号。
#include <dirent.h>
/*读取目录,获取目录下所有文件的名称以及对应 inode 号*/
struct dirent *readdir(DIR *dirp);
当使用 opendir()打开目录时,目录流将指向了目录列表的头部,使用 readdir()读取一条目录条目之后,目录流将会向后移动、指向下一个目录条目。
这其实跟 open()类似,当使用 open()打开文件的时候, 文件位置偏移量默认指向了文件头部,当使用 read()或 write()进行读写时,文件偏移量会自动向后移动。
rewinddir 函数
rewinddir()是 C 库函数,可将目录流重置为目录起点,以便对 readdir()的下一次调用将从目录列表中的第一个文件开始。
#include <sys/types.h>
#include <dirent.h>
/* 将目录流重置为目录的起点 参数:DIR句柄*/
void rewinddir(DIR *dirp);
进程的当前工作目录
每一个进程都有自己的当前工作目录。
当前工作目录是该进程搜索相对路径名的起点。
运行一个进程时、其父进程的当前工作目录将被继承。
可通过 getcwd() 函数来获取进程的当前工作目录。
#include <unistd.h>
char *getcwd(char *buf, //存放当前工作路径字符串的缓冲区
size_t size); //缓冲区大小
改变当前工作目录
系统调用 chdir()和 fchdir() 用于更改进程的当前工作目录。一个用路径,一个用文件描述符。
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
5.9 删除文件
通过系统调用 unlink()或使用 C 库函数 remove() 删除文件。
使用 unlink 函数删除文件
系统调用ulink()用于删除一个文件,不包括目录。
#include <unistd.h>
/*删除一个文件*/
int unlink(const char *pathname);
使用 remove 函数删除文件
C 库函数remove(),用于移除一个文件或空目录。
#include <stdio.h>
int remove(const char *pathname);
5.10 文件重命名
#include <stdio.h>
int rename(const char *oldpath, //文件原路径
const char *newpath);//文件新路径
rename() 仅改变目录条目,不改变 inode 编号。不移动数据块中存储的内容。
第六章 字符串处理
6.1 字符串输入/输出
标准输入stdin
标准输出stdout
标准错误stderr
6.1.1 字符串输出
常用的字符串输出函数有 putchar()、puts()、fputc()、fputs()。
还有格式化输出 printf()、fprintf()、dprintf()、sprintf()、snprintf()。
这些都是标准C库函数。使用stdio缓冲。
puts 函数
把字符串输出到标准输出设备,将末尾' \0 '转换为换行符' \n '。
#include <stdio.h>
/*把字符串输出到显示设备。会在末尾添加\n*/
int puts(const char *s);
putchar 函数
把字符输出到标准输出设备。ASCII码。0~127之间的整数。
#include <stdio.h>
int putchar(int c);
fputc 函数
把字符输出到指定的文件。
#include <stdio.h>
int fputc(int c, //字符
FILE *stream);//文件描述符
fputs 函数
把字符串输出到指定的文件。
#include <stdio.h>
int fputs(const char *s,//字符串
FILE *stream);//文件描述符
6.1.2 字符串输入
常用的字符串输出函数有 getchar()、gets()、fgetc()、fgets()。
还有格式化输出 scanf()、fscanf()、scanf()。
这些都是标准C库函数。使用stdio缓冲。
gets 函数
gets()函数用于从标准输入设备获取用户输入的字符串。
#include <stdio.h>
char *gets(char *s);
gets()与 scanf()的区别
gets() 函数允许带有空格、制表符,仅以回车、换行作为分割符。
scanf() 函数以空格、制表符、回车、换行作为分割符。
#include <stdio.h>
char *gets(char *s);
getchar 函数
从标准输入设备中读取一个字符。
#include <stdio.h>
int getchar(void);
getchar() 函数也是从输入缓冲区读取字符数据,但只读取一个字符,包括空格、TAB 制表符、换行回车符等。
fgets 函数
从文件获取输入的字符串,可指定文件描述符。
#include <stdio.h>
char *fgets(char *s,//存储读到的内容
int size, //读多少字节
FILE *stream);//FILE* 文件描述符
fgets() 会将缓冲区中的换行符读取出来。读取完成后自动在末尾添加\0。
fgetc 函数
从文件读取一个字符,用到文件描述符。
#include <stdio.h>
int fgetc(FILE *stream);
6.2 字符串长度
计算字符串长度的库函数 strlen()。
#include <string.h>
//计算字符长度,\0截止,不包括\0
size_t strlen(const char *s);
sizeof 和 strlen 的区别
sizeof 是关键字,strlen 是库函数。
sizeof 用于计算变量大小,strlen 计算结尾为' \0 '的字符串长度。
编译阶段就计算出了 sizeof 的结果,strlen 在运行阶段才计算出来。
6.3 字符串拼接
库函数 strcat()或 strncat()用于将两个字符串拼接起来。strncat()可以指定追加到目标字符串的字符数量。
#include <string.h>
char *strcat(char *dest, //目标字符串
const char *src);//源字符串
char *strncat(char *dest,
const char *src,
size_t n);//追加到目标字符串的字符数量
6.4 字符串拷贝
strcpy()函数和 strncpy()函数用于实现字符串拷贝。strncpy()可以指定从源复制到目标的字符长度。
#include <string.h>
char *strcpy(char *dest, //目标字符串
const char *src);//源字符串
char *strncpy(char *dest,
const char *src,
size_t n); //复制的字符长度
6.5 内填充
将某一块内存中的数据全部设置为指定的值。
库函数 memset()、bzero()。
#include <string.h>
/* 将指定个数的填充值填充到指定地址 */
void *memset(void *s,//地址指针
int c, //填充值
size_t n);//填充个数
/* 将指定个数的0填充到指定地址 */
void bzero(void *s,
size_t n);
6.6 字符串比较
标准库提供了用于字符串比较的函数 strcmp()和 strncmp()。
#include <string.h>
/* 比较俩字符串ascii码 */
int strcmp(const char *s1, const char *s2);
/* 比较俩字符串前n个字符的ascii码 */
int strncmp(const char *s1, const char *s2, size_t n);
6.7 字符串查找
strchr()、 strrchr()、strstr()、strpbrk()、index()、 rindex()用于字符串查找。
#include <string.h>
/*找到字符串中的给定字符,返回位置,从0开始*/
char *strchr(const char *s, int c);
/* 从后往前找给定字符,返回位置,位置从左0开始 */
char *strrchr(const char *s, int c);
/* 找给定字符串的位置 */
char *strstr(const char *haystack, const char *needle);
6.8 字符串与数字互转
6.8.1 字符串转整形数据
标准库提供函数,包括 atoi()、atol()、atoll()以及 strtol()、strtoll()、strtoul()、strtoull()。
#include <stdlib.h>
/*字符串转整数*/
int atoi(const char *nptr);
/*字符串转长整数 */
long atol(const char *nptr);
/* 字符串转long int */
long int strtol(const char *nptr, //需要转换的目标字符串
char **endptr, //存储出现的无效字符
int base); //字符代表的进制。特殊值0为自行判断。
目标字符串可以以任意数量的空格或者 0 开头,转换时跳过前面的空格字符,直到遇上数字字符或正负号(' + '或' - ')才开始做转换,而再遇到非数字或字符串结束时(' /0 ')才结束转换。
6.8.2 字符串转浮点型数据
标准库提供函数,包括 atof()、strtod()、strtof()、strtold()。
#include <stdlib.h>
/* 字符串转double */
double atof(const char *nptr);
6.8.3 数字转字符串
sprintf可以将数字转化为特定格式然后以字符形式存储。
sprintf(str, "%d", i);
sprintf(str, "%.2f", f);
sprintf(str, "%c", c);
sprintf(str, "%u", ui);
sprintf(str, "%ld", l);
6.9 给应用程序传参
main函数可以有argc和argv两个参数,argc是传入参数的数量,argv存储传入的参数。argv[0]固定为程序的相对地址。
6.10 正则表达式
略。