nullLinux环境高级编程Linux环境高级编程李 林 电子科技大学 软件学院 第二讲 执行体程序库—日志的实现第二讲 执行体程序库—日志的实现李 林 电子科技大学 软件学院 日志的实现日志的实现本课程围绕执行体模型的程序库展开
当库中函数或系统API出错时,总希望把出错信息记录下来。这一任务通常是由日志来实现的
在本库中,假设日志信息被记录在文件中日志的实现日志的实现文件的基本操作(打开、定位、读写、关闭)
I/O效率
出错处理
CLLog的实现日志的实现日志的实现文件的基本操作(打开、定位、读写、关闭)
I/O效率
出错处理
CLLog的实现文件操作基本顺序文件操作基本顺序打开 open
创建 creat
定位 lseek
读 read
写 write
关闭 closeopen函数open函数用于打开或者创建一个文件
函数原型
#include
int open(const char* pathname, int oflag, ...)
参数
第一个参数pathname:要打开或者创建的文件名
第二个参数oflag:用于指定文件打开模式、标志等信息。open函数open函数第二个参数oflag:
Linux头文件已经为文件打开模式、标志等定义了若干的宏
oflag需要指定这些宏
宏定义在/usr/include/bits/fcntl.h中
在该头文件中,只读打开标志被定义为:
#define O_RDONLY 00
open函数open函数oflag:
文件打开模式标志
以下三个标志必须指定一个且只能指定一个
O_RDONLY : 只读打开
O_WRONLY : 只写打开
O_RDWR : 读写打开
其他文件标志
下面的标志是可以选择的,可通过C语言的或运算与文件打开标志进行组合
open函数open函数oflag
其他文件标志:
O_APPEND:每次写的数据都添加到文件尾
O_TRUNC:若此文件存在,并以读写或只写打开,则文件长度为0
O_CREAT:若文件不存在,则创建该文件。此时,open函数需要第三个参数,用于指定该文件的访问权限位(后面描述)
O_EXCL:若同时指定了O_CREAT标志,而文件已经存在,则会出错。可用于测试文件是否存在文件访问权限文件访问权限什么是文件访问权限?为什么要有访问权限?
每一个文件都有访问权限位,可将其分成三类
文件访问权限文件访问权限
open的第三个参数open的第三个参数这些取值可以通过或操作符组合open函数open函数返回值
int open(const char* pathname, int oflag, …)
返回值:整型数据
成功时,返回文件描述符
出错时,返回-1
open函数open函数返回值
int open(const char* pathname, int oflag, …)
返回值:整型数据
成功时,返回文件描述符
出错时,返回-1
什么是文件
描述符?open函数open函数返回值
int open(const char* pathname, int oflag, …)
返回值:整型数据
成功时,返回文件描述符
出错时,返回-1
什么是文件
描述符?已打开文件
的索引open函数open函数返回值
int open(const char* pathname, int oflag, …)
返回值:整型数据
成功时,返回文件描述符
出错时,返回-1
什么是文件
描述符?已打开文件
的索引通过索引找
到已打开文件文件描述符文件描述符文件描述符的本质是什么?
通过文件描述符怎么样能找到需访问的文件?
需要了解进程打开文件时,内核创建或涉及到的一系列数据结构进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfilef_posf_dentry文件标志............dentryd_inode索引节点号文件各信息inode..............进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........进程控制块
PCB进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.........struct task_struct{
.............
struct files_struct *files;
.............
};进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files...................files_structfiles_struct该结构体包含了:
进程已打开文件表进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]files_struct..............struct file **fd;进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfile..............文件对象:代表
一个已打开的文件进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfilef_posf_dentry文件标志..............文件偏移量各种文
件标志文件相关目录项struct dentry *f_dentry;进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfilef_posf_dentry文件标志............dentry..............进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfilef_posf_dentry文件标志............dentryd_inode..............struct inode *d_inode;进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfilef_posf_dentry文件标志............dentryd_inode索引节点号文件各信息inode..............索引节点索引节点文件系统索引节点的信息,存储在磁盘上
当需要时,调入内存,填写VFS的索引节点(即inode结构)
每个文件都对应了一个索引节点
通过索引节点号,可以唯一的标识文件系统中的指定文件索引节点索引节点struct inode{
........................
unsigned long i_no;
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
off_t i_size;
time_t i_atime;
time_t i_mtime;
};
索引节点索引节点struct inode{
........................
unsigned long i_no;
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
off_t i_size;
time_t i_atime;
time_t i_mtime;
};
索引节点号索引节点索引节点struct inode{
........................
unsigned long i_no;
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
off_t i_size;
time_t i_atime;
time_t i_mtime;
};
文件类型访问权限索引节点索引节点struct inode{
........................
unsigned long i_no;
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
off_t i_size;
time_t i_atime;
time_t i_mtime;
};
文件拥有者ID索引节点索引节点struct inode{
........................
unsigned long i_no;
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
off_t i_size;
time_t i_atime;
time_t i_mtime;
};
文件拥有者
所在组ID索引节点索引节点struct inode{
........................
unsigned long i_no;
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
off_t i_size;
time_t i_atime;
time_t i_mtime;
};
文件大小索引节点索引节点struct inode{
........................
unsigned long i_no;
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
off_t i_size;
time_t i_atime;
time_t i_mtime;
};
文件最后访问时间索引节点索引节点struct inode{
........................
unsigned long i_no;
umode_t i_mode;
uid_t i_uid;
gid_t i_gid;
off_t i_size;
time_t i_atime;
time_t i_mtime;
};
文件最后修改时间进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfilef_posf_dentry文件标志............dentryd_inode索引节点号文件各信息inode文件描述符文件描述符task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfilef_posf_dentry文件标志............dentryd_inode索引节点号文件各信息inodeopen函数返回的文件描述符
已打开文件表的索引文件描述符文件描述符文件描述符是已打开文件的索引,通过该值可以在fd_array表中检索相应的文件对象
文件描述符是一个非负的整数
文件描述符0、1、2分别对应于标准输入、标准输出、标准出错,在进程创建时,已经打开。文件操作基本顺序文件操作基本顺序打开 open
创建 creat
定位 lseek
读 read
写 write
关闭 closecreat 函数creat 函数用于创建一个新文件
函数原型
int creat(const char *pathname, mode_t mode)
参数
pathname:要创建的文件名(包括路径信息)
mode:同open的第三个参数
返回值
成功返回只写打开的文件描述符
出错返回-1creat函数creat函数creat函数的功能可以用open函数实现
open(pathname,
O_WRONLY | O_CREAT | O_TRUNC,
mode);
为什么需要指定O_TRUNC标志
当文件存在时,调用creat函数,会将文件的大小变为0(程序演示2.2)creat函数creat函数
creat函数缺点:它以只写方式打开所创建的文件。若要创建一个临时文件,并先写该文件,然后又读该文件,则必须先调用creat,close,然后再open。简便方法:
open(pathname,
O_RDWR | O_CREAT | O_TRUNC,
mode);文件操作基本顺序文件操作基本顺序打开 open
创建 creat
定位 lseek
读 read
写 write
关闭 closelseek函数lseek函数lseek函数用于修改当前文件偏移量
当前文件偏移量的作用
规定了从文件什么地方开始进行读、写操作
通常,读、写操作结束时,会使文件偏移量增加读写的字节数
当打开一个文件时,除非指定了O_APPEND标志,否则偏移量被设置为0lseek函数lseek函数函数原型:
off_t lseek(int filedes, off_t offset, int whence)
参数
第一个参数filedes:open/creat函数返回的文件描述符
第二个参数offset:
相对偏移量:需结合whence才能计算出真正的偏移量
类型off_t:32位机是32位数据类型,64位是64位lseek函数lseek函数参数
第三个参数Whence:该参数取值是三个常量之一
SEEK_SET: 当前文件偏移量为:
距文件开始处的offset个字节SEEK_CUR: 当前文件偏移量为:
当前文件偏移量+offset(可正可负)SEEK_END: 当前文件偏移量为:
当前文件长度+offset(可正可负)lseek函数lseek函数返回值:
若成功,返回新的文件偏移量
若出错,返回-1
获得当前的偏移量
off_t CurrentPosition;
CurrentPosition = lseek(fd, 0, SEEK_CUR);
lseek操作并不引起任何I/O操作,只是修改内核中的记录进程打开文件的内核数据结构进程打开文件的内核数据结构task_struct.........files.......................files_structfd[0]fd[1]fd[2]fd[3]...........files_structfilef_posf_dentry文件标志............dentryd_inode索引节点号文件各信息inode空洞文件空洞文件使用lseek修改文件偏移量后,当前文件偏移量有可能大于文件的长度
在这种情况下,对该文件的下一次写操作,将加长该文件
这样文件中形成了一个空洞。对空洞区域进行读,均返回00100150200lseek(fd, 50, SEEK_END)
write(fd, buf, 50)文件长度是多少?
200?150?空洞文件空洞文件演示程序(2.3)
#od file.hole文件操作基本顺序文件操作基本顺序打开 open
创建 creat
定位 lseek
读 read
写 write
关闭 closeread函数read函数用于从文件中读出数据
函数原型
ssize_t read(int fd, void *buff,
size_t nbytes)
参数
第一个参数fd:文件描述符
第二个参数buff:指向缓冲区,用于存放从文件读出的数据
第三个参数nbytes:32位或64位;需要从文件中读出的字节数
缓冲区的大小>=nbytesread函数read函数返回值
返回值类型:ssize_t,有符号的,32位机中32bits,64位机中64bits
出错:返回-1
成功:返回从文件中实际读到的字节数
当读到文件结尾时,则返回0
read函数read函数很多情况下,read实际读出的字节数都小于要求读出的字节数
读普通文件,在读到要求的字节数之前,就到达了文件尾端
当从终端设备读时,通常一次最多读一行
当从网络读时,网络中的缓冲机构可能造成read函数返回值小于所要求读出的字节数
某些面向记录的设备,如磁带,一次最多返回一个记录
........................文件操作基本顺序文件操作基本顺序打开 open
创建 creat
定位 lseek
读 read
写 write
关闭 closewrite函数write函数用于向文件里面,写入数据
函数原型
ssize_t write(int fd, const void *buff,
size_t nbytes);
参数
第一个参数fd:文件描述符
第二个参数buff:指向缓冲区,存放了需要写入文件的数据
第三个参数nbytes:需要写入文件的字节数
write函数write函数返回值
返回值类型:ssize_t,有符号的,在32位系统中是32bits,64位是64bits
出错:返回-1
成功:返回实际写入文件的字节数
write出错的原因
磁盘满
没有访问权限
超过了给定进程的文件长度限制
.....................
write函数write函数当从文件中间某处写入数据时,是插入操作?覆盖操作?还是不能写?(程序演示2.4)
当以O_APPEND选项打开一个文件时,能否使用lseek指定文件偏移量?指定之后,从文件什么地方开始进行写?读操作又是如何?(程序演示2.5)文件操作基本顺序文件操作基本顺序打开 open
创建 creat
定位 lseek
读 read
写 write
关闭 closeclose函数close函数用于关闭一个已打开的文件
函数原型
int close(int filedes)
返回值
成功返回0
出错返回-1
参数
filedes:文件描述符
close函数close函数当clsoe函数关闭文件时,会释放进程加在该文件上的所有记录锁
内核会对进程打开文件表、文件对象、索引节点表项等结构进行修改,释放相关的资源
当进程退出时,会关闭当前所有已打开的文件描述符文件操作基本顺序文件操作基本顺序打开 open
创建 creat
定位 lseek
读 read
写 write
关闭 close日志的实现日志的实现文件的基本操作(打开、定位、读写、关闭)
I/O效率
出错处理
CLLog的实现I/O效率I/O效率程序3-3#define BUFFSIZE 4096
int main()
{
int n;
char buf[BUFFSIZE];
while((n = read(STDIN_FILENO, buf, BUFFSIZE))>0)
if(write(STDOUT_FILENO, buf, n) != n)
err_sys(“write error”);
return 0;
}从标准输入读数据
然后写至标准输出对标准输入和标准输出
进行了重定向:
$ ./test < file1 >/dev/null从文件file1读数据
然后写至设备/dev/null程序中,影响效率的关键:
BUFFSIZE的取值I/O效率I/O效率BUFFSIZE的取值系统CPU时间I/O效率I/O效率原因
Linux文件系统采用了某种预读技术
当检测到正在进行顺序读取时,系统就试图读入比应用程序所要求的更多数据
并假设应用程序很快就会读这些数据
当BUFFSIZE增加到一定程度后,预读就停止了日志的实现日志的实现文件的基本操作(打开、定位、读写、关闭)
I/O效率
出错处理
CLLog的实现出错处理出错处理API调用出错了,如何查找出错原因?
出错处理(2.1)
errno.h头文件中,定义了errno:当API调用出错时,errno说明出错的具体原因
可简单地将errno理解成整型数据
出错信息转换成可读字符串
#include
char* strerror(int errno);
出错处理出错处理出错处理
以前的定义:extern int errno;
不是线程安全的
多线程环境:
extern int *__errno_location();
#define errno (*__errno_location())
查看/usr/include/bits/errno.h中41~49行出错处理出错处理头文件中,如果
没有定义__ASSEMBLER__
且,(没有定义_LIBC
或定义了_LIBC_REENTRANT)
errno才能用于多线程
默认情况下,这些符号定义了吗?
示例(2.20)
保险起见,可以定义_LIBC_REENTRANT执行体程序库的出错处理执行体程序库的出错处理程序库对外提供若干类,每个方法出错时如何告知调用者是否出错,以及出错码?
方法很多,为了简化起见,函数将返回一个对象,该对象保存了函数的返回值和出错码执行体程序库的出错处理执行体程序库的出错处理在执行体程序库中,CLStatus类作为绝大多数方法的返回类型,其对象即保存了函数的返回值和出错码。
例:
CLStatus Run(void *pContext);
CLStatus的实现版本1(2.21)执行体程序库的出错处理执行体程序库的出错处理在2.21的实现中,需要调用GetErrorCode函数方可获得出错码,有无更为简便的方法
直接让m_lErrorCode成为public成员?
能否有类内部可读可写,而外部只能读的数据成员?(示例2.22)CLStatus的使用者效率问题CLStatus的使用者效率问题由于CLStatus作为传值类型返回,存在一些效率优化的问题
示例2.23CLStatus的使用者效率问题CLStatus的使用者效率问题为了兼顾移植性、提高效率,建议CLStatus的使用方式:
例:
CLStatus f()
{
return CLStatus(…);
}
CLStatus s = f();日志的实现日志的实现文件的基本操作(打开、定位、读写、关闭)
I/O效率
出错处理
CLLog的实现CLLog的实现CLLog的实现CLLog类用于记录程序库的日志信息到某个文件中,例如API调用出错、类库中某个方法调用出错等等
为了简化起见,文件中每条日志仅记录字符串的文字说明,以及出错码
示例2.24CLLog的实现CLLog的实现2.24示例中,每次记录一条信息,都需要打开、关闭文件,效率不高
解决方法:将打开和关闭文件的操作,放在CLLog的构造和析构函数中
示例2.25CLLog的实现CLLog的实现效率最高的情况是:在整个程序的运行过程中,日志文件只被打开、关闭一次。
这样一来,只能实例化CLLog一次,并将这次实例化的对象在整个程序中传递
如何改进?
如何保证CLLog只能实例化一次
如何保证程序任何地方,都可以方便地获取CLLog对象
示例2.26CLLog的实现CLLog的实现2.26的示例中,每次调用GetInstance,都需要调用者检查指针是否有效,能否简化
示例2.27
CLLog还存在的问题?CLLog的实现CLLog的实现CLLog还存在的问题
多线程同时记录日志如何?
多进程同时记录日志如河?
目前,CLLog不是线程安全的;后续再改进日志的实现日志的实现文件的基本操作(打开、定位、读写、关闭)
I/O效率
出错处理
CLLog的实现