当前位置: 首页 > 新闻 > 信息荟萃
编号:5562
Linux环境编程:从应用到内核.pdf
http://www.100md.com 2020年11月15日
第1页
第8页
第14页
第27页
第40页
第760页

    参见附件(15158KB,1044页)。

     Linux环境编程:从应用到内核将从一个全新的角度带领读者重新进入Linux环境编程,从应用出发,深入内核源码,研究Linux各接口的工作机制和原理,让读者不仅知其然,还知其所以然

    内容简介

    《LUNIX环境高级编程》(简称APUE)几乎是Linux领域程序员人手必备的一本书,但在掌握和理解APU[的内容后,又该如何继续提高自己的技能,如何更深入地理解Linux环境编程及其背后的工作机制呢?本书将从一个全新的角度带领读者重新进入Linux环境编程,从应用出发,深入内核源码,研究Linux各接口的工作机制和原理,让读者不仅知其然,还知其所以然。作为Linux开发工程师,如果不仅掌握Linux的应用层开发,同时还熟悉Linux的内核源码,那么在Linux环境下设计开发任何产品都将游刀有余,稳定且高效。

    本书是Linux技术专家高峰和李彬的合力之作,是两个人多年开发经验的总结和分享,也是市场上**一本将Linux应用态与内核态相结合的技术图书,选择这种写作方式是为了向APUE的作者致敬。本书涵盖了APUE中大部分章节的内容,并针对Linux环境,根据作者多年经验,详细解析了Linux常用接口的使用方法和陷阱。为了让读者更清楚地理解接口的工作原理,对于绝大部分接口,作者都深入仁库或内核源码进行全面分析。希望本书可以帮助读者打通Linux环境的应用和内核两条脉络,使两条线融会贯通,进一步提高开发水平。

    如何阅读本书

    本书定位为APUE的补充或进阶读物,所以假设读者已具备了一定的编程基础,对Linux环境也有所了解,因此在涉及一些基本概念和知识时,只是蜻蜓点水,简单略过。因为笔者希望把更多的笔墨放在更为重要的部分,而不是各种相关图书均有讲解的基本概念上。所以如果你初学者,建议还是先习APUE、C语言编程,并且在具有一定的操作系统知识后再来阅读本书。

    Limux环境编程涉及的领域太多,很难有某个人可以在Linux的各个领域均有比较深刻的认识,尤其是已有APUE这本经典图书在前,所以本书是由高峰、李彬两个人共同完成的。

    高峰负责第0、1,2、3.4、12,13、14,15章,李彬负责第5-11章。两位不同的作者,在写作风格上很难保证一致,如果给各位读者带来了不便,在此给各位先道个歉。尽管是由两个人共同写作,并且负责的还是我们各自相对擅长的领域,可是在写作的过程中我们仍然感觉到很吃力,用了将近三年的时间才算完成本书。对比APUE,本书一方面在深度上还是有所不及,另一方面在广度上还是没有涵盖APUE涉及的所有领域,这也让我们对Stevens大师更加敬佩。

    本书使用的Linux内核源代码版本为3.2.44,glibc的源码版本为2.17.

    作者简介

    高峰,北京理工大学通信与信息系统专业硕士学位。毕业后在A10 Networks公司工作六年多,任职Staff Software Engineer,目前在创业公司全讯汇聚(爱快路由)担任技术总监。多年来一直专注于网络领域,熟悉Linux内核、应用及服务端的设计、开发和架构,对TCP/IP网络协议有深刻的认识和理解。编码功力深厚,知识领域广博,擅长产品的性能改进和调优。撰写过大量技术文章,并为多个知名开源项目贡献过代码。

    李彬,东南大学信号与信息处理专业硕士。毕业后先后任职中兴通讯、趋势科技,目前在存储公司Bigtera担任SEG部门技术负责人。一直专注于Linu**台下的开发,多年分布式存储开发经验,熟悉Linux内核,编程基本功扎实,对性能优化、bug定位有异乎寻常的爱好,属于“死磕派”研发工程师。喜欢技术分享和交流,在社区和公司内部分享过大量技术文章。

    读者对象

    根据本书的内容,我觉得适合以下几类读者:

    在Linux应用层方面有一定开发经验的程序员。

    对Linux内核有兴趣的程序员。

    热爱Linux内核和开源项目的技术人员。

    Linux环境编程:从应用到内核截图

    LinuxUnix技术丛书

    Linux环境编程:从应用到内核

    高峰 李彬 著

    ISBN:978-7-111-53610-9

    本书纸版由机械工业出版社于2016年出版,电子版由华章分社(北京华章图文信息

    有限公司,北京奥维博世图书发行有限公司)全球范围内制作与发行。

    版权所有,侵权必究

    客服热线:+ 86-10-68995265

    客服信箱:service@bbbvip.com

    官方网址:www.hzmedia.com.cn

    新浪微博 @华章数媒

    微信公众号 华章电子书(微信号:hzebook)目录

    前言

    第0章 基础知识

    0.1 一个Linux程序的诞生记

    0.2 程序的构成

    0.3 程序是如何“跑”的

    0.4 背景概念介绍

    0.4.1 系统调用

    0.4.2 C库函数

    0.4.3 线程安全

    0.4.4 原子性

    0.4.5 可重入函数

    0.4.6 阻塞与非阻塞

    0.4.7 同步与非同步

    第1章 文件IO

    1.1 Linux中的文件

    1.1.1 文件、文件描述符和文件表

    1.1.2 内核文件表的实现

    1.2 打开文件

    1.2.1 open介绍

    1.2.2 更多选项

    1.2.3 open源码跟踪

    1.2.4 如何选择文件描述符

    1.2.5 文件描述符fd与文件管理结构file

    1.3 creat简介

    1.4 关闭文件1.4.1 close介绍

    1.4.2 close源码跟踪

    1.4.3 自定义files_operations

    1.4.4 遗忘close造成的问题

    1.4.5 如何查找文件资源泄漏

    1.5 文件偏移

    1.5.1 lseek简介

    1.5.2 小心lseek的返回值

    1.5.3 lseek源码分析

    1.6 读取文件

    1.6.1 read源码跟踪

    1.6.2 部分读取

    1.7 写入文件

    1.7.1 write源码跟踪

    1.7.2 追加写的实现

    1.8 文件的原子读写

    1.9 文件描述符的复制

    1.10 文件数据的同步

    1.11 文件的元数据

    1.11.1 获取文件的元数据

    1.11.2 内核如何维护文件的元数据

    1.11.3 权限位解析

    1.12 文件截断

    1.12.1 truncate与ftruncate的简单介绍

    1.12.2 文件截断的内核实现

    1.12.3 为什么需要文件截断

    第2章 标准IO库2.1 stdin、stdout和stderr

    2.2 IO缓存引出的趣题

    2.3 fopen和open标志位对比

    2.4 fdopen与fileno

    2.5 同时读写的痛苦

    2.6 ferror的返回值

    2.7 clearerr的用途

    2.8 小心fgetc和getc

    2.9 注意fread和fwrite的返回值

    2.10 创建临时文件

    第3章 进程环境

    3.1 main是C程序的开始吗

    3.2 “活雷锋”exit

    3.3 atexit介绍

    3.3.1 使用atexit

    3.3.2 atexit的局限性

    3.3.3 atexit的实现机制

    3.4 小心使用环境变量

    3.5 使用动态库

    3.5.1 动态库与静态库

    3.5.2 编译生成和使用动态库

    3.5.3 程序的“平滑无缝”升级

    3.6 避免内存问题

    3.6.1 尴尬的realloc

    3.6.2 如何防止内存越界

    3.6.3 如何定位内存问题

    3.7 “长跳转”longjmp3.7.1 setjmp与longjmp的使用

    3.7.2 “长跳转”的实现机制

    3.7.3 “长跳转”的陷阱

    第4章 进程控制:进程的一生

    4.1 进程ID

    4.2 进程的层次

    4.2.1 进程组

    4.2.2 会话

    4.3 进程的创建之fork

    4.3.1 fork之后父子进程的内存关系

    4.3.2 fork之后父子进程与文件的关系

    4.3.3 文件描述符复制的内核实现

    4.4 进程的创建之vfork

    4.5 daemon进程的创建

    4.6 进程的终止

    4.6.1 _exit函数

    4.6.2 exit函数

    4.6.3 return退出

    4.7 等待子进程

    4.7.1 僵尸进程

    4.7.2 等待子进程之wait

    4.7.3 等待子进程之waitpid

    4.7.4 等待子进程之等待状态值

    4.7.5 等待子进程之waitid

    4.7.6 进程退出和等待的内核实现

    4.8 exec家族

    4.8.1 execve函数4.8.2 exec家族

    4.8.3 execve系统调用的内核实现

    4.8.4 exec与信号

    4.8.5 执行exec之后进程继承的属性

    4.9 system函数

    4.9.1 system函数接口

    4.9.2 system函数与信号

    4.10 总结

    第5章 进程控制:状态、调度和优先级

    5.1 进程的状态

    5.1.1 进程状态概述

    5.1.2 观察进程状态

    5.2 进程调度概述

    5.3 普通进程的优先级

    5.4 完全公平调度的实现

    5.4.1 时间片和虚拟运行时间

    5.4.2 周期性调度任务

    5.4.3 新进程的加入

    5.4.4 睡眠进程醒来

    5.4.5 唤醒抢占

    5.5 普通进程的组调度

    5.6 实时进程

    5.6.1 实时调度策略和优先级

    5.6.2 实时调度相关API

    5.6.3 限制实时进程运行时间

    5.7 CPU的亲和力

    第6章 信号6.1 信号的完整生命周期

    6.2 信号的产生

    6.2.1 硬件异常

    6.2.2 终端相关的信号

    6.2.3 软件事件相关的信号

    6.3 信号的默认处理函数

    6.4 信号的分类

    6.5 传统信号的特点

    6.5.1 信号的ONESHOT特性

    6.5.2 信号执行时屏蔽自身的特性

    6.5.3 信号中断系统调用的重启特性

    6.6 信号的可靠性

    6.6.1 信号的可靠性实验

    6.6.2 信号可靠性差异的根源

    6.7 信号的安装

    6.8 信号的发送

    6.8.1 kill、tkill和tgkill

    6.8.2 raise函数

    6.8.3 sigqueue函数

    6.9 信号与线程的关系

    6.9.1 线程之间共享信号处理函数

    6.9.2 线程有独立的阻塞信号掩码

    6.9.3 私有挂起信号和共享挂起信号

    6.9.4 致命信号下,进程组全体退出

    6.10 等待信号

    6.10.1 pause函数

    6.10.2 sigsuspend函数6.10.3 sigwait函数和sigwaitinfo函数

    6.11 通过文件描述符来获取信号

    6.12 信号递送的顺序

    6.13 异步信号安全

    6.14 总结

    第7章 理解Linux线程(1)

    7.1 线程与进程

    7.2 进程ID和线程ID

    7.3 pthread库接口介绍

    7.4 线程的创建和标识

    7.4.1 pthread_create函数

    7.4.2 线程ID及进程地址空间布局

    7.4.3 线程创建的默认属性

    7.5 线程的退出

    7.6 线程的连接与分离

    7.6.1 线程的连接

    7.6.2 为什么要连接退出的线程

    7.6.3 线程的分离

    7.7 互斥量

    7.7.1 为什么需要互斥量

    7.7.2 互斥量的接口

    7.7.3 临界区的大小

    7.7.4 互斥量的性能

    7.7.5 互斥锁的公平性

    7.7.6 互斥锁的类型

    7.7.7 死锁和活锁

    7.8 读写锁7.8.1 读写锁的接口

    7.8.2 读写锁的竞争策略

    7.8.3 读写锁总结

    7.9 性能杀手:伪共享

    7.10 条件等待

    7.10.1 条件变量的创建和销毁

    7.10.2 条件变量的使用

    第8章 理解Linux线程(2)

    8.1 线程取消

    8.1.1 函数取消接口

    8.1.2 线程清理函数

    8.2 线程局部存储

    8.2.1 使用NPTL库函数实现线程局部存储

    8.2.2 使用__thread关键字实现线程局部存储

    8.3 线程与信号

    8.3.1 设置线程的信号掩码

    8.3.2 向线程发送信号

    8.3.3 多线程程序对信号的处理

    8.4 多线程与fork

    第9章 进程间通信:管道

    9.1 管道

    9.1.1 管道概述

    9.1.2 管道接口

    9.1.3 关闭未使用的管道文件描述符

    9.1.4 管道对应的内存区大小

    9.1.5 shell管道的实现

    9.1.6 与shell命令进行通信(popen)9.2 命名管道FIFO

    9.2.1 创建FIFO文件

    9.2.2 打开FIFO文件

    9.3 读写管道文件

    9.4 使用管道通信的示例

    第10章 进程间通信:System V IPC

    10.1 System V IPC概述

    10.1.1 标识符与IPC Key

    10.1.2 IPC的公共数据结构

    10.2 System V消息队列

    10.2.1 创建或打开一个消息队列

    10.2.2 发送消息

    10.2.3 接收消息

    10.2.4 控制消息队列

    10.3 System V信号量

    10.3.1 信号量概述

    10.3.2 创建或打开信号量

    10.3.3 操作信号量

    10.3.4 信号量撤销值

    10.3.5 控制信号量

    10.4 System V共享内存

    10.4.1 共享内存概述

    10.4.2 创建或打开共享内存

    10.4.3 使用共享内存

    10.4.4 分离共享内存

    10.4.5 控制共享内存

    第11章 进程间通信:POSIX IPC11.1 POSIX IPC概述

    11.1.1 IPC对象的名字

    11.1.2 创建或打开IPC对象

    11.1.3 关闭和删除IPC对象

    11.1.4 其他

    11.2 POSIX消息队列

    11.2.1 消息队列的创建、打开、关闭及删除

    11.2.2 消息队列的属性

    11.2.3 消息的发送和接收

    11.2.4 消息的通知

    11.2.5 IO多路复用监控消息队列

    11.3 POSIX信号量

    11.3.1 创建、打开、关闭和删除有名信号量

    11.3.2 信号量的使用

    11.3.3 无名信号量的创建和销毁

    11.3.4 信号量与futex

    11.4 内存映射mmap

    11.4.1 内存映射概述

    11.4.2 内存映射的相关接口

    11.4.3 共享文件映射

    11.4.4 私有文件映射

    11.4.5 共享匿名映射

    11.4.6 私有匿名映射

    11.5 POSIX共享内存

    11.5.1 共享内存的创建、使用和删除

    11.5.2 共享内存与tmpfs

    第12章 网络通信:连接的建立12.1 socket文件描述符

    12.2 绑定IP地址

    12.2.1 bind的使用

    12.2.2 bind的源码分析

    12.3 客户端连接过程

    12.3.1 connect的使用

    12.3.2 connect的源码分析

    12.4 服务器端连接过程

    12.4.1 listen的使用

    12.4.2 listen的源码分析

    12.4.3 accept的使用

    12.4.4 accept的源码分析

    12.5 TCP三次握手的实现分析

    12.5.1 SYN包的发送

    12.5.2 接收SYN包,发送SYN+ACK包

    12.5.3 接收SYN+ACK数据包

    12.5.4 接收ACK数据包,完成三次握手

    第13章 网络通信:数据报文的发送

    13.1 发送相关接口

    13.2 数据包从用户空间到内核空间的流程

    13.3 UDP数据包的发送流程

    13.4 TCP数据包的发送流程

    13.5 IP数据包的发送流程

    13.5.1 ip_send_skb源码分析

    13.5.2 ip_queue_xmit源码分析

    13.6 底层模块数据包的发送流程

    第14章 网络通信:数据报文的接收14.1 系统调用接口

    14.2 数据包从内核空间到用户空间的流程

    14.3 UDP数据包的接收流程

    14.4 TCP数据包的接收流程

    14.5 TCP套接字的三个接收队列

    14.6 从网卡到套接字

    14.6.1 从硬中断到软中断

    14.6.2 软中断处理

    14.6.3 传递给协议栈流程

    14.6.4 IP协议处理流程

    14.6.5 大师的错误?原始套接字的接收

    14.6.6 注册传输层协议

    14.6.7 确定UDP套接字

    14.6.8 确定TCP套接字

    第15章 编写安全无错代码

    15.1 不要用memcmp比较结构体

    15.2 有符号数和无符号数的移位区别

    15.3 数组和指针

    15.4 再论数组首地址

    15.5 “神奇”的整数类型转换

    15.6 小心volatile的原子性误解

    15.7 有趣的问题:“x==x”何时为假?

    15.8 小心浮点陷阱

    15.8.1 浮点数的精度限制

    15.8.2 两个特殊的浮点值

    15.9 Intel移位指令陷阱前言

    为什么要写这本书

    我从事Linux环境的开发工作已有近十年的时间,但我一直认为工作时间并不

    等于经验,更不等于能力。如何才能把工作时间转换为自己的经验和能力呢?我认

    为无非是多阅读、多思考、多实践、多分享。这也是我在ChinaUnix上的博客座右

    铭,目前我的博客一共有247篇博文,记录的大都是Linux内核网络部分的源码分

    析,以及相关的应用编程。机械工业出版社华章公司的Lisa正是通过我的博客找到

    我的,而这也促成了本书的出版。

    其实在Lisa之前,就有另外一位编辑与我聊过,但当时我没有下好决心,认为

    自己无论是在技术水平,还是时间安排上,都不足以完成一本技术图书的创作。等

    到与Lisa洽谈的时候,我感觉自己的技术已经有了一些沉淀,同时时间也相对比较

    充裕,因此决定开始撰写自己技术生涯的第一本书。

    对于Linux环境的开发人员,《Unix环境高级编程》(后文均简称为APUE)无

    疑是最为经典的入门书籍。其作者Stevens是我从业以来最崇拜的技术专家。他的

    Advanced Programming in the Unix Environment、Unix Network Programming系列及

    TCPIP Illustrated系列著作,字字珠玑,本本经典。在我从业的最初几年,这几本

    书每本都阅读了好几遍,而这也为我进行Linux用户空间的开发奠定了坚实的基

    础。在掌握了这些知识以后,如何继续提高自己的技能呢?经过一番思考,我选择

    了阅读Linux内核源码,并尝试将内核与应用融会贯通。在阅读了一定量的内核源

    码之后,我才真正理解了Linux专家的这句话“Read the fucking codes”。只有阅读了

    内核源码,才能真正理解Linux内核的原理和运行机制,而此时,我也发现了

    Stevens著作的一个局限——APUE和UNP毕竟是针对Unix环境而写的,Linux虽然大

    部分与Unix兼容,但是在很多行为上与Unix还是完全不同的。这就导致了书中的一些内容与Linux环境中的实际效果是相互矛盾的。

    现在有机会来写一本技术图书,我就想在向Stevens致敬的同时,写一本类似于

    APUE风格的技术图书,同时还要在Linux环境下,对APUE进行突破。大言不惭地

    说,我期待这本书可以作为APUE的补充,还可以作为Linux开发人员的进阶读物。

    事实上,本书的写作布局正是以APUE的章节作为参考,针对Linux环境,不仅对用

    户空间的接口进行阐述,同时还引导读者分析该接口在内核的源码实现,使得读者

    不仅可以知道接口怎么用,同时还可以理解接口是怎么工作的。对于Linux的系统

    调用,做到知其然,知其所以然。

    读者对象

    根据本书的内容,我觉得适合以下几类读者:

    ·在Linux应用层方面有一定开发经验的程序员。

    ·对Linux内核有兴趣的程序员。

    ·热爱Linux内核和开源项目的技术人员。

    如何阅读本书

    本书定位为APUE的补充或进阶读物,所以假设读者已具备了一定的编程基

    础,对Linux环境也有所了解,因此在涉及一些基本概念和知识时,只是蜻蜓点

    水,简单略过。因为笔者希望把更多的笔墨放在更为重要的部分,而不是各种相关

    图书均有讲解的基本概念上。所以如果你是初学者,建议还是先学习APUE、C语

    言编程,并且在具有一定的操作系统知识后再来阅读本书。

    Linux环境编程涉及的领域太多,很难有某个人可以在Linux的各个领域均有比

    较深刻的认识,尤其是已有APUE这本经典图书在前,所以本书是由高峰、李彬两个人共同完成的。

    高峰负责第0、1、2、3、4、12、13、14、15章,李彬负责第5~11章。两位不

    同的作者,在写作风格上很难保证一致,如果给各位读者带来了不便,在此给各位

    先道个歉。尽管是由两个人共同写作,并且负责的还是我们各自相对擅长的领域,可是在写作的过程中我们仍然感觉到很吃力,用了将近三年的时间才算完成本书。

    对比APUE,本书一方面在深度上还是有所不及,另一方面在广度上还是没有涵盖

    APUE涉及的所有领域,这也让我们对Stevens大师更加敬佩。

    本书使用的Linux内核源代码版本为3.2.44,glibc的源码版本为2.17。

    勘误和支持

    由于作者的水平有限,主题又过于宏大,书中难免会出现一些错误或不准确的

    地方,如有不妥之处,恳请读者批评指正。如果你发现有什么问题,或者有什么疑

    问,都可以发邮件至我的邮箱gfree.wind@gmail.com,期待您的指导!

    致谢

    首先要感谢伟大的Linux内核创始人Linus,他开创了一个影响世界的操作系

    统。

    其次要感谢机械工业出版社华章公司的编辑杨绣国老师(Lisa),感谢你的魄

    力,敢于找新人来写作,并敢于信任新人,让其完成这么大的一个项目。感谢你的

    耐心,正常的一年半的写作时间,被我们生生地延长到了将近三年的时间,感谢你

    在写作过程中对我们的鼓励和帮助。

    然后要感谢我的搭档李彬,在我加入当前的创业公司后,只有很少的空闲时间

    和精力来投入写作。这时,是李彬在更紧张的时间内,承担了本书的一半内容。并且其写作态度极其认真,对质量精益求精。没有李彬的加入,本书很可能就半途而

    废了。再次感谢李彬,我的好搭档。

    最后我要感谢我的亲人。感谢我的父母,没有你们的培养,绝没有我的今天;

    感谢我的妻子,没有你的支持,就没有我事业上的进步;感谢我的岳父岳母对我女

    儿的照顾,使我没有后顾之忧;最后要感谢的是我可爱的女儿高一涵小天使,你的

    诞生为我带来了无尽的欢乐和动力!

    谨以此书,献给我最亲爱的家人,以及众多热爱Linux的朋友们。

    高峰

    中国北京

    2016年3月第0章 基础知识

    基础知识是构建技术大厦不可或缺的稳定基石,因此,本书首先来介绍一下书

    中所涉及的一些基础知识。这里以第0章命名,表明我们要注重基础,从0开始,同

    时也是向伟大的C语言致敬。

    基础知识看似简单,但是想要真正理解它们,是需要花一番功夫的。除了需要

    积累经验以外,更需要对它们进行不断的思考和理解,这样,才能写出高可靠性的

    程序。这些基础知识很多都可以独立成文,限于篇幅,这里只能是简单的介绍,都

    是笔者根据自己的经验和理解进行的总结和概括,相信对读者会有所帮助。感兴趣

    的朋友可以自己查找更多的资料,以得到更准确、更细致的介绍。

    注意 本书中的示例代码为了简洁明了,没有考虑代码的健壮性,例如不

    检查函数的返回值、使用全局变量等。0.1 一个Linux程序的诞生记

    一本编程书籍如果开篇不写一个“hello world”,就违背了“自古以来”的传统

    了。因此本节也将以hello world为例来说明一个Linux程序的诞生过程,示例代码如

    下:

    include

    int main(void)

    {

    printf(Hello world!\n);

    return 0;

    }

    下面使用gcc生成可执行程序:gcc-g-Wall 0_1_hello_world.c-o hello_world。这

    样,一个Linux可执行程序就诞生了。

    整个过程看似简单,其实涉及预处理、编译、汇编和链接等多个步骤。只不过

    gcc作为一个工具集自动完成了所有的步骤。下面就分别来看看其中所涉及的各个

    步骤。

    首先来了解一下什么是预处理。预处理用于处理预处理命令。对于上面的代码

    来说,唯一的预处理命令就是include。它的作用是将头文件的内容包含到本文件

    中。注意,这里的“包含”指的是该头文件中的所有代码都会在include处展开。可

    以通过“gcc-E 0_1_hello_world.c”在预处理后自动停止后面的操作,并把预处理的结

    果输出到标准输出。因此使用“gcc-E 0_1_hello_world.c>0_1_hello_world.i”,可得到

    预处理后的文件。

    理解了预处理,在出现一些常见的错误时,才能明白其中的原因。比如,为什

    么不能在头文件中定义全局变量?这是因为定义全局变量的代码会存在于所有以

    include包含该头文件的文件中,也就是说所有的这些文件,都会定义一个同样的

    全局变量,这样就不可避免地造成了冲突。编译环节是指对源代码进行语法分析,并优化产生对应的汇编代码的过程。同

    样,可以使用gcc得到汇编代码,而非最终的二进制文件,即“gcc-S

    0_1_hello_world.c-o 0_1_hello_world.s”。gcc的-S选项会让gcc在编译完成后停止后面

    的工作,这样只会产生对应的汇编文件。

    汇编的过程比较简单,就是将源代码翻译成可执行的指令,并生成目标文件。

    对应的gcc命令为“gcc-c 0_1_hello_world.c-o 0_1_hello_world.o”。

    链接是生成最终可执行程序的最后一个步骤,也是比较复杂的一步。它的工作

    就是将各个目标文件——包括库文件(库文件也是一种目标文件)链接成一个可执

    行程序。在这个过程中,涉及的概念比较多,如地址和空间的分配、符号解析、重

    定位等。在Linux环节下,该工作是由GNU的链接器ld完成的。

    实际上我们可以使用-v选项来查看完整和详细的gcc编译过程,命令如下。

    gcc -g -Wall -v 0_1_hello_word.c -o hello_world。

    由于输出过多,此处就不粘贴结果了。感兴趣的朋友可以自行执行命令,查看

    输出。通过-v选项,可以看到gcc在背后做了哪些具体的工作。0.2 程序的构成

    Linux下二进制可执行程序的格式一般为ELF格式。以0.1节的hello world为例,使用readelf查看其ELF格式,内容如下:

    ELF Header:

    Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00

    Class: ELF32

    Data: 2's complement, little endian

    Version: 1 (current)

    OSABI: UNIX - System V

    ABI Version: 0

    Type: EXEC (Executable file)

    Machine: Intel 80386

    Version: 0x1

    Entry point address: 0x8048320

    Start of program headers: 52 (bytes into file)

    Start of section headers: 5148 (bytes into file)

    Flags: 0x0

    Size of this header: 52 (bytes)

    Size of program headers: 32 (bytes)

    Number of program headers: 9

    Size of section headers: 40 (bytes)

    Number of section headers: 36

    Section header string table index: 33

    Section Headers:

    [Nr] Name Type Addr Off Size ES Flg Lk Inf Al

    [ 0] NULL 00000000 000000 000000 00 0 0 0

    [ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1

    [ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4

    [ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4

    [ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4

    [ 5] .dynsym DYNSYM 080481cc 0001cc 000050 10 A 6 1 4

    [ 6] .dynstr STRTAB 0804821c 00021c 00004a 00 A 0 0 1

    [ 7] .gnu.version VERSYM 08048266 000266 00000a 02 A 5 0 2

    [ 8] .gnu.version_r VERNEED 08048270 000270 000020 00 A 6 1 4

    [ 9] .rel.dyn REL 08048290 000290 000008 08 A 5 0 4

    [10] .rel.plt REL 08048298 000298 000018 08 A 5 12 4

    [11] .init PROGBITS 080482b0 0002b0 000024 00 AX 0 0 4

    [12] .plt PROGBITS 080482e0 0002e0 000040 04 AX 0 0 16

    [13] .text PROGBITS 08048320 000320 000188 00 AX 0 0 16

    [14] .fini PROGBITS 080484a8 0004a8 000015 00 AX 0 0 4

    [15] .rodata PROGBITS 080484c0 0004c0 000015 00 A 0 0 4

    [16] .eh_frame_hdr PROGBITS 080484d8 0004d8 000034 00 A 0 0 4

    [17] .eh_frame PROGBITS 0804850c 00050c 0000c4 00 A 0 0 4

    [18] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4

    [19] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4

    [20] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4

    [21] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4

    [22] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4

    [23] .got.plt PROGBITS 0804a000 001000 000018 04 WA 0 0 4

    [24] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4

    [25] .bss NOBITS 0804a020 001020 000004 00 WA 0 0 4

    [26] .comment PROGBITS 00000000 001020 00006b 01 MS 0 0 1

    [27] .debug_aranges PROGBITS 00000000 00108b 000020 00 0 0 1

    [28] .debug_info PROGBITS 00000000 0010ab 000094 00 0 0 1

    [29] .debug_abbrev PROGBITS 00000000 00113f 000044 00 0 0 1

    [30] .debug_line PROGBITS 00000000 001183 000043 00 0 0 1

    [31] .debug_str PROGBITS 00000000 0011c6 0000cb 01 MS 0 0 1

    [32] .debug_loc PROGBITS 00000000 001291 000038 00 0 0 1

    [33] .shstrtab STRTAB 00000000 0012c9 000151 00 0 0 1

    [34] .symtab SYMTAB 00000000 0019bc 000490 10 35 51 4

    [35] .strtab STRTAB 00000000 001e4c 00025a 00 0 0 1由于输出过多,后面的结果并没有完全展示出来。ELF文件的主要内容就是由

    各个section及symbol表组成的。在上面的section列表中,大家最熟悉的应该是text

    段、data段和bss段。text段为代码段,用于保存可执行指令。data段为数据段,用于

    保存有非0初始值的全局变量和静态变量。bss段用于保存没有初始值或初值为0的

    全局变量和静态变量,当程序加载时,bss段中的变量会被初始化为0。这个段并不

    占用物理空间——因为完全没有必要,这些变量的值固定初始化为0,因此何必占

    用宝贵的物理空间?

    其他段没有这三个段有名,下面来介绍一下其中一些比较常见的段:

    ·debug段:顾名思义,用于保存调试信息。

    ·dynamic段:用于保存动态链接信息。

    ·fini段:用于保存进程退出时的执行程序。当进程结束时,系统会自动执行这

    部分代码。

    ·init段:用于保存进程启动时的执行程序。当进程启动时,系统会自动执行这

    部分代码。

    ·rodata段:用于保存只读数据,如const修饰的全局变量、字符串常量。

    ·symtab段:用于保存符号表。

    其中,对于与调试相关的段,如果不使用-g选项,则不会生成,但是与符号相

    关的段仍然会存在,这时可以使用strip去掉符号信息,感兴趣的朋友可以自己参考

    strip的说明进行实验。一般在嵌入式的产品中,为了减少程序占用的空间,都会使

    用strip去掉非必要的段。0.3 程序是如何“跑”的

    在日常工作中,我们经常会说“程序“跑”起来了”,那么它到底是怎么“跑”的

    呢?在Linux环境下,可以使用strace跟踪系统调用,从而帮助自己研究系统程序加

    载、运行和退出的过程。此处仍然以hello_world为例。

    strace .hello_world

    execve(.hello_world, [.hello_world], [ 59 vars ]) = 0

    brk(0) = 0x872a000

    access(etcld.so.nohwcap, F_OK) = -1 ENOENT (No such file or directory)

    mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7778000

    access(etcld.so.preload, R_OK) = -1 ENOENT (No such file or directory)

    open(etcld.so.cache, O_RDONLY|O_CLOEXEC) = 3

    fstat64(3, {st_mode=S_IFREG|0644, st_size=80063, ...}) = 0

    mmap2(NULL, 80063, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7764000

    close(3) = 0

    access(etcld.so.nohwcap, F_OK) = -1 ENOENT (No such file or directory)

    open(libi386-linux-gnulibc.so.6, O_RDONLY|O_CLOEXEC) = 3

    read(3, \177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0000\226\1\0004\0\0\0..., 512) = 512

    fstat64(3, {st_mode=S_IFREG|0755, st_size=1730024, ...}) = 0

    mmap2(NULL, 1743580, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0)= 0xb75ba000

    mprotect(0xb775d000, 4096, PROT_NONE) = 0

    mmap2(0xb775e000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a3) = 0xb775e000

    mmap2(0xb7761000, 10972, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7761000

    close(3) = 0

    mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb75b9000

    set_thread_area({entry_number:-1 -> 6, base_addr:0xb75b9900, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0

    mprotect(0xb775e000, 8192, PROT_READ) = 0

    mprotect(0x8049000, 4096, PROT_READ) = 0

    mprotect(0xb779b000, 4096, PROT_READ) = 0

    munmap(0xb7764000, 80063) = 0

    fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 3), ...}) = 0

    mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7777000

    write(1, Hello world!\n, 13Hello world!) = 13

    exit_group(0) = ?

    下面就针对strace输出说明其含义。在Linux环境中,执行一个命令时,首先是

    由shell调用fork,然后在子进程中来真正执行这个命令(这一过程在strace输出中无

    法体现)。strace是hello_world开始执行后的输出。首先是调用execve来加载

    hello_world,然后ld会分别检查ld.so.nohwcap和ld.so.preload。其中,如果

    ld.so.nohwcap存在,则ld会加载其中未优化版本的库。如果ld.so.preload存在,则ld

    会加载其中的库——在一些项目中,我们需要拦截或替换系统调用或C库,此时就

    会利用这个机制,使用LD_PRELOAD来实现。之后利用mmap将ld.so.cache映射到

    内存中,ld.so.cache中保存了库的路径,这样就完成了所有的准备工作。接着ld加

    载c库——libc.so.6,利用mmap及mprotect设置程序的各个内存区域,到这里,程序运行的环境已经完成。后面的write会向文件描述符1(即标准输出)输出Hello

    world!\n,返回值为13,它表示write成功的字符个数。最后调用exit_group退出程

    序,此时参数为0,表示程序退出的状态——此例中hello-world程序返回0。0.4 背景概念介绍

    0.4.1 系统调用

    系统调用是操作系统提供的服务,是应用程序与内核通信的接口。在x86平台

    上,有多种陷入内核的途径,最早是通过int 0x80指令来实现的,后来Intel增加了一

    个新的指令sysenter来代替int 0x80——其他CPU厂商也增加了类似的指令。新指令

    sysenter的性能消耗大约是int 0x80的一半左右。即使是这样,相对于普通的函数调

    用来说,系统调用的性能消耗也是巨大的。所以在追求极致性能的程序中,都在尽

    力避免系统调用,譬如C库的gettimeofday就避免了系统调用。

    用户空间的程序默认是通过栈来传递参数的。对于系统调用来说,内核态和用

    户态使用的是不同的栈,这使得系统调用的参数只能通过寄存器的方式进行传递。

    有心的朋友可能会想到一个问题:在写代码的时候,程序员根本不用关心参数

    是如何传递的,编译器已经默默地为我们做了一切——压栈、出栈、保存返回地址

    等操作,但是编译器如何知道调用的函数是普通函数,还是系统调用呢?如果是后

    者,编译器就不能简单地使用栈来传递参数了。

    为了解决这个问题,我们就要看0.4.2节介绍的C库函数了。0.4.2 C库函数

    0.4.1节提到C库函数为编译器解决了系统调用的问题。Linux环境下,使用的C

    库一般都是glibc,它封装了几乎所有的系统调用,代码中使用的“系统调用”,实际

    上就是调用C库中的函数。C库函数同样位于用户态,所以编译器可以统一处理所

    有的函数调用,而不用区分该函数到底是不是系统调用。

    下面以具体的系统调用open来看看glibc库是如何封装系统调用的。在glibc的代

    码中,用了大量的编译器特性以及编程的技巧,可读性不高。open在glibc中对应的

    实现函数实际上是__open_nocancel。至于如何定位到它,感兴趣的朋友可以用

    __open_nocancel或open作为关键字,在glibc的源码中搜索,找出它们之间的关系。

    int

    __open_nocancel (const char file, int oflag, ...)

    {

    int mode = 0;

    if (oflag O_CREAT)

    {

    va_list arg;

    va_start (arg, oflag);

    mode = va_arg (arg, int);

    va_end (arg);

    }

    return INLINE_SYSCALL (openat, 4, AT_FDCWD, file, oflag, mode);

    }

    其中INLINE_SYSCALL是我们关心的内容,这个宏完成了对真正系统调用的

    封装:INLINE_SYSCALL->INTERNAL_SYSCALL。实现INTERNAL_SYSCALL的

    一个实例为。

    define INTERNAL_SYSCALL(name, err, nr, args...) ({ register unsigned int resultvar; EXTRAVAR_nr asm volatile ( LOADARGS_nr movl %1, %%eax\n\t int 0x80\n\t RESTOREARGS_nr : =a (resultvar) : i (__NR_name) ASMFMT_nr(args) : memory, cc); (int) resultvar; })其中,关键的代码是用嵌入式汇编写的,在此只做简单说

    明。“move%1,%%eax”表示将第一个参数(即__NR_name)赋给寄存器eax。

    __NR_name为对应的系统调用号,对于本例中的open来说,其为__NR_openat。

    系统调用号在文件usrincludeasmunitstd_32(64).h中定义,代码如下:

    [fgao@fgao understanding_apue]cat usrincludeasmunistd_32.h | grep openat

    define __NR_openat 295

    也就是说,在Linux平台下,系统调用的约定是使用寄存器eax来传递系统调用

    号的。至于参数的传递,在glibc中也有详细的说明,参见文件

    sysdepsunixsysvlinuxi386sysdep.h。0.4.3 线程安全

    线程安全,顾名思义是指代码可以在多线程环境下“安全”地执行。何谓安全?

    即符合正确的逻辑结果,是程序员期望的正常执行结果。为了实现线程安全,该代

    码要么只能使用局部变量或资源,要么就是利用锁等同步机制,来实现全局变量或

    资源的串行访问。

    下面是一个经典的多线程不安全代码:

    include

    include

    include

    static int counter = 0;

    define LOOPS 10000000

    static void thread(void unused)

    {

    int i;

    for (i = 0; i < LOOPS; ++i) {

    ++counter;

    }

    return NULL;

    }

    int main(void)

    {

    pthread_t t1, t2;

    pthread_create(t1, NULL, thread, NULL);

    pthread_create(t2, NULL, thread, NULL);

    pthread_join(t1, NULL);

    pthread_join(t2, NULL);

    printf(Counter is %d by threads\n, counter);

    return 0;

    }

    注意 之所以这里的LOOPS选用了一个比较大的数“10000000”,是为了保

    证第一个线程不要在第二个线程开始执行前就退出了。大家可以根据自己的运行环

    境来修改这个数值。

    以上代码创建了两个线程,用来实现对同一个全局变量进行自加运算,循环次

    数为一千万次。下面来看一下运行结果:

    [fgao@fgao chapter0].threads_counter

    Counter is 10843915 by threads

    为什么最后的结果不是期望的20000000呢?下面反汇编将来揭开这个秘密——反汇编是理解程序行为的不二利器,因为它更贴近机器语言,也就是说,反汇编更

    贴近CPU运行的真相。

    下面对线程函数thread进行反汇编,代码如下:

    080484a4 :

    80484a4: 55 push %ebp

    80484a5: 89 e5 mov %esp,%ebp

    80484a7: 83 ec 10 sub 0x10,%esp

    80484aa: c7 45 fc 00 00 00 00 movl 0x0,-0x4(%ebp)

    80484b1: eb 11 jmp 80484c4 80484b3:

    a1 94 98 04 08

    mov

    0x8049894,%eax

    80484b8:

    83 c0 01

    add

    0x1,%eax

    80484bb:

    a3 94 98 04 08

    mov

    %eax,0x8049894 80484c0: 83 45 fc 01 addl 0x1,-0x4(%ebp)

    80484c4: 81 7d fc 7f 96 98 00 cmpl 0x98967f,-0x4(%ebp)

    80484cb: 7e e6 jle 80484b3

    80484cd: b8 00 00 00 00 mov 0x0,%eax

    80484d2: c9 leave

    80484d3: c3 ret

    其中加粗部分对应的是++counter的汇编代码,其逻辑如下:

    1)将counter的值赋给寄存器EAX;

    2)对寄存器EAX的值加1;

    3)将EAX的值赋给counter。

    假设目前counter的值为0,那么当两个线程同时执行++counter时,会有如下情

    况(每个线程会有独立的上下文执行环境,所以可视为每个线程都有一个“独立”的

    EAX):

    thread1 thread2

    eax = counter => eax = 0

    eax = counter => 0 eax = 0

    eax = eax+1 => eax = 1

    counter = eax => counter = 1

    eax = eax + 1 => eax = 1

    counter = eax => counter = 1

    上面两个线程都对counter执行了自增动作,但是最终的结果是“1”而不是“2”。

    这只是众多错误时序情况中的一种。之所以会产生这样的错误,就是因为++counter

    的执行指令并不是原子的,多个线程对counter的并发访问造成了最后的错误结果。

    利用锁就可以保证counter自增指令的串行化,如下所示:

    thread1 thread2

    lock

    eax = counter => eax =0

    eax = eax +1 => eax = 1

    counter = eax => counter = 1

    unlock

    lock

    eax = counter => eax = 1

    eax = eax+1 => eax = 2

    counter = eax => counter = 2

    unlock通过加锁,可以视counter的自增指令为“原子指令”,最后的结果终于是期望的

    答案了。0.4.4 原子性

    以前原子被认为是物理组成的最小单元,所以在计算机领域,就借其不可分割

    的这层含义作为隐喻。对于计算机科学来说,如果变量是原子的,那么对这个变量

    的任何访问和更改都是原子的。如果操作是原子的,那么这个操作将是不可分割

    的,要么成功,要么失败,不会有任何的中间状态。

    列举一个原子操作的例子,用户A向用户B转账1000元。简单来说,这里最起

    码有两个步骤:

    1)用户A的账号减少1000元;

    2)用户B的账号增加1000元。

    如果在上述步骤1结束的时候,转账发生了故障,比如电力中断,是否会造成

    用户A的账号减少了1000元,而用户B的账号没有变化呢?这种情况对于原子操作

    是不会发生的。当电力中断导致转账操作进行到一半就失败时,用户A的账号肯定

    不会减少1000元。因为这个操作的原子性,保证了用户A减少1000元和用户B增加

    1000元,必须同时成立,而不会存在一个中间结果。至于这个操作是如何做到原子

    性的,可以参看数据库的事务是如何实现的——原子性是事务的一个特性之一。0.4.5 可重入函数

    从字面上理解,可重入就是可重复进入。在编程领域,它不仅仅意味着可以重

    复进入,还要求在进入后能成功执行。这里的重复进入,是指当前进程已经处于该

    函数中,这时程序会允许当前进程的某个执行流程再次进入该函数,而不会引发问

    题。这里的执行流程不仅仅包括多线程,还包括信号处理、longjump等执行流程。

    所以,可重入函数一定是线程安全的,而线程安全函数则不一定是可重入函数。

    从以上定义来看,很难说出哪些函数是可重入函数,但是可以很明显看出哪些

    函数是不可以重入的函数。当函数使用锁的时候,尤其是互斥锁的时候,该函数是

    不可重入的,否则会造成死锁。若函数使用了静态变量,并且其工作依赖于这个静

    态变量时,该函数也是不可重入的,否则会造成该函数工作不正常。

    下面来看一个死锁的例子代码如下:

    include

    include

    include

    include

    include

    include

    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    static const char const caller[2] = {mutex_thread, signal handler};

    static pthread_t mutex_tid;

    static pthread_t sleep_tid;

    static volatile int signal_handler_exit = 0;

    static void hold_mutex(int c)

    {

    printf(enter hold_mutex [caller %s]\n, caller[c]);

    pthread_mutex_lock(mutex);

    这里的循环是为了保证锁不会在信号处理函数退出前被释放掉

    while (!signal_handler_exit c != 1) {

    sleep(5);

    }

    pthread_mutex_unlock(mutex);

    printf(leave hold_mutex [caller %s]\n, caller[c]);

    }

    static void mutex_thread(void arg)

    {

    hold_mutex(0);

    }

    static void sleep_thread(void arg)

    {

    sleep(10);

    }static void signal_handler(int signum)

    {

    hold_mutex(1);

    signal_handler_exit = 1;

    }

    int main

    {

    signal(SIGUSR1, signal_handler);

    pthread_create(mutex_tid, NULL, mutex_thread, NULL);

    pthread_create(sleep_tid, NULL, sleep_thread, NULL);

    pthread_kill(sleep_tid, SIGUSR1);

    pthread_join(mutex_tid, NULL);

    pthread_join(sleep_tid, NULL);

    return 0;

    }

    先看看运行结果:

    [fgao@fgao chapter0]gcc -g 0_8_signal_mutex.c -o signal_mutex -lpthread

    [fgao@fgao chapter0].signal_mutex

    enter hold_mutex [caller signal handler]

    enter hold_mutex [caller mutex_thread]

    为什么会死锁呢?就是因为函数hold_mutex是不可重入的函数——其中使用了

    pthread_mutex互斥量。当mutex_thread获得mutex时,sleep_thread就收到了信号,再

    次调用就进入了hold_mutex。结果始终无法拿到mutex,信号处理函数无法返回,正常的程序流程也无法继续,这就造成了死锁。0.4.6 阻塞与非阻塞

    这里的阻塞与非阻塞,都是指IO操作。在Linux环境下,所有的IO系统调用默

    认都是阻塞的。那么何谓阻塞呢?阻塞的系统调用是指,当进行系统调用时,除非

    出错(被信号打断也视为出错),进程将会一直陷入内核态直到调用完成。非阻塞

    的系统调用是指无论IO操作成功与否,调用都会立刻返回。0.4.7 同步与非同步

    这里的同步与非同步,也是指IO操作。当把阻塞、非阻塞、同步和非同步放

    在一起时,不免会让人眼花缭乱。同步是否就是阻塞,非同步是否就是非阻塞呢?

    实际上在IO操作中,它们是不同的概念。同步既可以是阻塞的,也可以是非阻塞

    的,而常用的Linux的IO调用实际上都是同步的。这里的同步和非同步,是指IO数

    据的复制工作是否同步执行。

    以系统调用read为例。阻塞的read会一直陷入内核态直到read返回;而非阻塞的

    read在数据未准备好的情况下,会直接返回错误,而当有数据时,非阻塞的read同

    样会一直陷入内核态,直到read完成。这个read就是同步的操作,即IO的完成是在

    当前执行流程下同步完成的。

    如果是非同步即异步,则IO操作不是随系统调用同步完成的。调用返回后,IO操作并没有完成,而是由操作系统或者某个线程负责真正的IO操作,等完成后

    通知原来的线程。第1章 文件IO

    文件IO是操作系统不可或缺的部分,也是实现数据持久化的手段。对于Linux

    来说,其“一切皆是文件”的思想,更是突出了文件在Linux内核中的重要地位。本

    章主要讲述Linux文件IO部分的系统调用。

    注意 为了分析系统调用的实现,从本章开始会涉及Linux内核源码。但是

    本书并不是一本介绍内核源码的书籍,所以书中对内核源码的分析不会面面俱到。

    分析内核源码的目的是为了更好地理解系统调用,是为应用而服务的。因此,本书

    对内核源码的追踪和分析,只是浅尝辄止。1.1 Linux中的文件

    1.1.1 文件、文件描述符和文件表

    Linux内核将一切视为文件,那么Linux的文件是什么呢?其既可以是事实上的

    真正的物理文件,也可以是设备、管道,甚至还可以是一块内存。狭义的文件是指

    文件系统中的物理文件,而广义的文件则可以是Linux管理的所有对象。这些广义

    的文件利用VFS机制,以文件系统的形式挂载在Linux内核中,对外提供一致的文

    件操作接口。

    从数值上看,文件描述符是一个非负整数,其本质就是一个句柄,所以也可以

    认为文件描述符就是一个文件句柄。那么何为句柄呢?一切对于用户透明的返回

    值,即可视为句柄。用户空间利用文件描述符与内核进行交互;而内核拿到文件描

    述符后,可以通过它得到用于管理文件的真正的数据结构。

    使用文件描述符即句柄,有两个好处:一是增加了安全性,句柄类型对用户完

    全透明,用户无法通过任何hacking的方式,更改句柄对应的内部结果,比如Linux

    内核的文件描述符,只有内核才能通过该值得到对应的文件结构;二是增加了可扩

    展性,用户的代码只依赖于句柄的值,这样实际结构的类型就可以随时发生变化,与句柄的映射关系也可以随时改变,这些变化都不会影响任何现有的用户代码。

    Linux的每个进程都会维护一个文件表,以便维护该进程打开文件的信息,包

    括打开的文件个数、每个打开文件的偏移量等信息。1.1.2 内核文件表的实现

    内核中进程对应的结构是task_struct,进程的文件表保存在task_struct->files

    中。其结构代码如下所示。

    struct files_struct {

    count为文件表

    files_struct的引用计数

    atomic_t count;

    文件描述符表

    为什么有两个

    fdtable呢?这是内核的一种优化策略。

    fdt为指针,而

    fdtab为普通变量。一般情况下,fdt是指向

    fdtab的,当需要它的时候,才会真正动态申请内存。因为默认大小的文件表足以应付大多数

    情况,因此这样就可以避免频繁的内存申请。

    这也是内核的常用技巧之一。在创建时,使用普通的变量或者数组,然后让指针指向它,作为默认情况使 用。只有当进程使用量超过默认值时,才会动态申请内存。

    struct fdtable __rcu fdt;

    struct fdtable fdtab;

    written part on a separate cache line in SMP

    使用

    ____cacheline_aligned_in_smp可以保证

    file_lock是以

    cache

    line 对齐的,避免了

    false sharing

    spinlock_t file_lock ____cacheline_aligned_in_smp;

    用于查找下一个空闲的

    fd

    int next_fd;

    保存执行

    exec需要关闭的文件描述符的位图

    struct embedded_fd_set close_on_exec_init;

    保存打开的文件描述符的位图

    struct embedded_fd_set open_fds_init;

    fd_array为一个固定大小的

    file结构数组。struct file是内核用于文

    件管理的结构。这里使用默认大小的数组,就是为了可以涵盖大多数情况,避免动

    态分配

    struct file __rcu fd_array[NR_OPEN_DEFAULT];

    };

    下面看看files_struct是如何使用默认的fdtab和fd_array的,init是Linux的第一个

    进程,它的文件表是一个全局变量,代码如下:

    struct files_struct init_files = {

    .count = ATOMIC_INIT(1),.fdt = init_files.fdtab,.fdtab = {

    .max_fds = NR_OPEN_DEFAULT,.fd = init_files.fd_array[0],.close_on_exec = (fd_set )init_files.close_on_exec_init,.open_fds = (fd_set )init_files.open_fds_init,},.file_lock = __SPIN_LOCK_UNLOCKED(init_task.file_lock),};

    init_files.fdt和init_files.fdtab.fd都分别指向了自己已有的成员变量,并以此作为

    一个默认值。后面的进程都是从init进程fork出来的。fork的时候会调用dup_fd,而

    在dup_fd中其代码结构如下:

    newf = kmem_cache_alloc(files_cachep, GFP_KERNEL);

    if (!newf)

    goto out;

    atomic_set(newf->count, 1);

    spin_lock_init(newf->file_lock);

    newf->next_fd = 0;

    new_fdt = newf->fdtab;

    new_fdt->max_fds = NR_OPEN_DEFAULT;

    new_fdt->close_on_exec = (fd_set )newf->close_on_exec_init;

    new_fdt->open_fds = (fd_set )newf->open_fds_init;

    new_fdt->fd = newf->fd_array[0];

    new_fdt->next = NULL;

    初始化new_fdt,同样是为了让new_fdt和new_fdt->fd指向其本身的成员变量

    fdtab和fd_array。 说明 procpidstatus为对应pid的进程的当前运行状态,其中FDSize值即为

    当前进程max_fds的值。

    因此,初始状态下,files_struct、fdtable和files的关系如图1-1所示。

    图1-1 文件表、文件描述符表及文件结构关系图1.2 打开文件

    1.2.1 open介绍

    open在手册中有两个函数原型,如下所示:

    int open(const char pathname, int flags);

    int open(const char pathname, int flags, mode_t mode);

    这样的函数原型有些违背了我们的直觉。C语言是不支持函数重载的,为什么

    open的系统调用可以有两个这样的open原型呢?内核绝对不可能为这个功能创建两

    个系统调用。在Linux内核中,实际上只提供了一个系统调用,对应的是上述两个

    函数原型中的第二个。那么open有两个函数原型又是怎么回事呢?当我们调用open

    函数时,实际上调用的是glibc封装的函数,然后由glibc通过自陷指令,进行真正的

    系统调用。也就是说,所有的系统调用都要先经过glibc才会进入操作系统。这样的

    话,实际上是glibc提供了一个变参函数open来满足两个函数原型,然后通过glibc的

    变参函数open实现真正的系统调用来调用原型二。

    可以通过一个小程序来验证我们的猜想,代码如下:

    include

    include

    include

    include

    int main(void)

    {

    int fd = open(test_open.txt, O_CREAT, 0644, test);

    close(fd);

    return 0;

    }

    在这个程序中,调用open的时候,传入了4个参数,如果open不是变参函数,就会报错,如“too many arguments to function‘open’”。但是请看下面的编译输出:

    [fgao@fgao-ThinkPad-R52 chapter2]gcc -g -Wall 2_2_1_test_open.c

    [fgao@fgao-ThinkPad-R52 chapter2]没有任何的警告和错误。这就证实了我们的猜想,open是glibc的一个变参函

    数。fcntl.h中open函数的声明也确定了这点:

    extern int open (__const char __file, int __oflag, ...) __nonnull ((1));

    下面来说明一下open的参数:

    ·pathname:表示要打开的文件路径。

    ·flags:用于指示打开文件的选项,常用的有O_RDONLY、O_WRONLY和

    O_RDWR。这三个选项必须有且只能有一个被指定。为什么O_RDWR!

    =O_RDONLY|O_WRONLY呢?Linux环境中,O_RDONLY被定义为0,O_WRONLY被定义为1,而O_RDWR却被定义为2。之所以有这样违反常规的设计

    遗留至今,就是为了兼容以前的程序。除了以上三个选项,Linux平台还支持更多

    的选项,APUE中对此也进行了介绍。

    ·mode:只在创建文件时需要,用于指定所创建文件的权限位(还要受到umask

    环境变量的影响)。1.2.2 更多选项

    除了常用的几个打开文件的选项,APUE还介绍了一些常用的POSIX定义的选

    项。下面列出了Linux平台支持的大部分选项:

    ·O_APPEND:每次进行写操作时,内核都会先定位到文件尾,再执行写操

    作。

    ·O_ASYNC:使用异步IO模式。

    ·O_CLOEXEC:在打开文件的时候,就为文件描述符设置FD_CLOEXEC标

    志。这是一个新的选项,用于解决在多线程下fork与用fcntl设置FD_CLOEXEC的竞

    争问题。某些应用使用fork来执行第三方的业务,为了避免泄露已打开文件的内

    容,那些文件会设置FD_CLOEXEC标志。但是fork与fcntl是两次调用,在多线程

    下,可能会在fcntl调用前,就已经fork出子进程了,从而导致该文件句柄暴露给子

    进程。关于O_CLOEXEC的用途,将会在第4章详细讲解。

    ·O_CREAT:当文件不存在时,就创建文件。

    ·O_DIRECT:对该文件进行直接IO,不使用VFS Cache。

    ·O_DIRECTORY:要求打开的路径必须是目录。

    ·O_EXCL:该标志用于确保是此次调用创建的文件,需要与O_CREAT同时使

    用;当文件已经存在时,open函数会返回失败。

    ·O_LARGEFILE:表明文件为大文件。

    ·O_NOATIME:读取文件时,不更新文件最后的访问时间。

    ·O_NONBLOCK、O_NDELAY:将该文件描述符设置为非阻塞的(默认都是阻塞的)。

    ·O_SYNC:设置为IO同步模式,每次进行写操作时都会将数据同步到磁盘,然后write才能返回。

    ·O_TRUNC:在打开文件的时候,将文件长度截断为0,需要与O_RDWR或

    O_WRONLY同时使用。在写文件时,如果是作为新文件重新写入,一定要使用

    O_TRUNC标志,否则可能会造成旧内容依然存在于文件中的错误,如生成配置文

    件、pid文件等——在第2章中,我会例举一个未使用截断标志而导致问题的示例代

    码。

    注意 并不是所有的文件系统都支持以上选项。1.2.3 open源码跟踪

    我们经常这样描述“打开一个文件”,那么这个所谓的“打开”,究竟“打开”了什

    么?内核在这个过程中,又做了哪些事情呢?这一切将通过分析内核源码来得到答

    案。

    跟踪内核open源码open->do_sys_open,代码如下:

    long do_sys_open(int dfd, const char __user filename, int flags, int mode)

    {

    struct open_flags op;

    flags为用户层传递的参数,内核会对

    flags进行合法性检查,并根据

    mode生成新的

    flags值赋给

    lookup

    int lookup = build_open_flags(flags, mode, op);

    将用户空间的文件名参数复制到内核空间

    char tmp = getname(filename);

    int fd = PTR_ERR(tmp);

    if (!IS_ERR(tmp)) {

    未出错则申请新的文件描述符

    fd = get_unused_fd_flags(flags);

    if (fd >= 0) {

    申请新的文件管理结构

    file

    struct file f = do_filp_open(dfd, tmp, op, lookup);

    if (IS_ERR(f)) {

    put_unused_fd(fd);

    fd = PTR_ERR(f); } else {

    产生文件打开的通知事件

    fsnotify_open(f);

    将文件描述符

    fd与文件管理结构

    file对应起来,即安装

    fd_install(fd, f);

    }

    }

    putname(tmp);

    }

    return fd;

    }

    从do_sys_open可以看出,打开文件时,内核主要消耗了两种资源:文件描述

    符与内核管理文件结构file。1.2.4 如何选择文件描述符

    根据POSIX标准,当获取一个新的文件描述符时,要返回最低的未使用的文件

    描述符。Linux是如何实现这一标准的呢?

    在Linux中,通过do_sys_open->get_unused_fd_flags->alloc_fd(0,(flags))

    来选择文件描述符,代码如下:

    int alloc_fd(unsigned start, unsigned flags)

    {

    struct files_struct files = current->files;

    unsigned int fd;

    int error;

    struct fdtable fdt;

    files为进程的文件表,下面需要更改文件表,所以需要先锁文件表

    spin_lock(files->file_lock);

    repeat:

    得到文件描述符表

    fdt = files_fdtable(files);

    从

    start开始,查找未用的文件描述符。在打开文件时,start为

    0

    fd = start;

    files->next_fd为上一次成功找到的

    fd的下一个描述符。使用

    next_fd,可以快速找到未用的文 件描述符;

    if (fd < files->next_fd)

    fd = files->next_fd;

    当小于当前文件表支持的最大文件描述符个数时,利用位图找到未用的文件描述符。

    如果大于

    max_fds怎么办呢?如果大于当前支持的最大文件描述符,那它肯定是未

    用的,就不需要用位图来确认了。

    if (fd < fdt->max_fds)

    fd = find_next_zero_bit(fdt->open_fds->fds_bits,fdt->max_fds, fd);

    expand_files用于在必要时扩展文件表。何时是必要的时候呢?比如当前文件描述符已经超过了当

    前文件表支持的最大值的时候。

    error = expand_files(files, fd);

    if (error < 0)

    goto out;

    If we needed to expand the fs array we

    might have blocked - try again.

    if (error)

    goto repeat;

    只有在

    start小于

    next_fd时,才需要更新

    next_fd,以尽量保证文件描述符的连续性。

    if (start <= files->next_fd)

    files->next_fd = fd + 1;

    将打开文件位图

    open_fds对应

    fd的位置置位

    FD_SET(fd, fdt->open_fds);

    根据

    flags是否设置了

    O_CLOEXEC,设置或清除

    fdt->close_on_exec

    if (flags O_CLOEXEC)

    FD_SET(fd, fdt->close_on_exec);

    else

    FD_CLR(fd, fdt->close_on_exec);

    error = fd;

    if 1

    Sanity check

    if (rcu_dereference_raw(fdt->fd[fd]) != NULL) {

    printk(KERN_WARNING alloc_fd: slot %d not NULL!\n, fd);

    rcu_assign_pointer(fdt->fd[fd], NULL);

    }

    endif

    out:

    spin_unlock(files->file_lock);

    return error;

    }1.2.5 文件描述符fd与文件管理结构file

    前文已经说过,内核使用fd_install将文件管理结构file与fd组合起来,具体操作

    请看如下代码:

    void fd_install(unsigned int fd, struct file file)

    {

    struct files_struct files = current->files;

    struct fdtable fdt;

    spin_lock(files->file_lock);

    得到文件描述符表

    fdt = files_fdtable(files);

    BUG_ON(fdt->fd[fd] != NULL);

    将文件描述符表中的

    file类型的指针数组中对应

    fd的项指向

    file。

    这样文件描述符

    fd与

    file就建立了对应关系

    rcu_assign_pointer(fdt->fd[fd], file);

    spin_unlock(files->file_lock);

    }

    当用户使用fd与内核交互时,内核可以用fd从fdt->fd[fd]中得到内部管理文件的结构struct file。1.3 creat简介

    creat函数用于创建一个新文件,其等价于open(pathname,O_WRONLY|O_CREAT|O_TRUNC,mode)。APUE介绍了引入creat的原因:

    由于历史原因,早期的Unix版本中,open的第二个参数只能是0、1或者2。这

    样就没有办法打开一个不存在的文件。因此,一个独立系统调用creat被引入,用于

    创建新文件。现在的open函数,通过使用O_CREAT和O_TRUNC选项,可以实现

    creat的功能,因此creat已经不是必要的了。

    内核creat的实现代码如下所示:

    SYSCALL_DEFINE2(creat, const char __user , pathname, int, mode)

    {

    return sys_open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);

    }

    这样就确定了creat无非是open的一种封装实现。1.4 关闭文件

    1.4.1 close介绍

    close用于关闭文件描述符。而文件描述符可以是普通文件,也可以是设备,还

    可以是socket。在关闭时,VFS会根据不同的文件类型,执行不同的操作。

    下面将通过跟踪close的内核源码来了解内核如何针对不同的文件类型执行不同

    的操作。1.4.2 close源码跟踪

    首先,来看一下close的源码实现,代码如下:

    SYSCALL_DEFINE1(close, unsigned int, fd)

    {

    struct file filp;

    得到当前进程的文件表

    struct files_struct files = current->files;

    struct fdtable fdt;

    int retval;

    spin_lock(files->file_lock);

    通过文件表,取得文件描述符表

    fdt = files_fdtable(files);

    参数

    fd大于文件描述符表记录的最大描述符,那么它一定是非法的描述符

    if (fd >= fdt->max_fds)

    goto out_unlock;

    利用

    fd作为索引,得到

    file结构指针

    filp = fdt->fd[fd];

    检查

    filp是否为

    NULL。正常情况下,filp一定不为

    NULL。

    if (!filp)

    goto out_unlock;

    将对应的

    filp置为

    0

    rcu_assign_pointer(fdt->fd[fd], NULL);

    清除

    fd在

    close_on_exec位图中的位

    FD_CLR(fd, fdt->close_on_exec);

    释放该

    fd,或者说将其置为

    unused。

    __put_unused_fd(files, fd);

    spin_unlock(files->file_lock);

    关闭

    file结构

    retval = filp_close(filp, files);

    can't restart close syscall because file table entry was cleared

    if (unlikely(retval == -ERESTARTSYS ||

    retval == -ERESTARTNOINTR ||

    retval == -ERESTARTNOHAND ||

    retval == -ERESTART_RESTARTBLOCK))

    retval = -EINTR;

    return retval;

    out_unlock:

    spin_unlock(files->file_lock);

    return -EBADF;

    }

    EXPORT_SYMBOL(sys_close);

    __put_unused_fd源码如下所示:

    static void __put_unused_fd(struct files_struct files, unsigned int fd)

    {

    取得文件描述符表

    struct fdtable fdt = files_fdtable(files);

    清除

    fd在

    open_fds位图的位

    __FD_CLR(fd, fdt->open_fds);

    如果

    fd小于

    next_fd,重置

    next_fd为释放的

    fd

    if (fd < files->next_fd)

    files->next_fd = fd;

    }看到这里,我们来回顾一下之前分析过的alloc_fd函数,就可以总结出完整的

    Linux文件描述符选择策略:

    ·Linux选择文件描述符是按从小到大的顺序进行寻找的,文件表中next_fd用于

    记录下一次开始寻找的起点。当有空闲的描述符时,即可分配。

    ·当某个文件描述符关闭时,如果其小于next_fd,则next_fd就重置为这个描述

    符,这样下一次分配就会立刻重用这个文件描述符。

    以上的策略,总结成一句话就是“Linux文件描述符策略永远选择最小的可用的

    文件描述符”。——这也是POSIX标准规定的。

    其实我并不喜欢这样的策略。因为这样迅速地重用刚刚释放的文件描述符,容

    易引发难以调试和定位的bug——尽管这样的bug是应用层造成的。比如一个线程关

    闭了某个文件描述符,然后又创建了一个新的文件描述符,这时文件描述符就被重

    用了,但其值是一样的。如果有另外一个线程保存了之前的文件描述符的值,那它

    就会再次访问这个文件描述符。此时,如果是普通文件,就会读错或写错文件。如

    果是socket,就会与错误的对端通信。这样的错误发生时,可能并不会被察觉到。

    即使发现了错误,要找到根本原因,也非常困难。

    如果不重用这个描述符呢?在文件描述符被关闭后,创建新的描述符且不使用

    相同的值。这样再次访问之前的描述符时,内核可以返回错误,应用层可以更早地

    得知错误的发生。

    虽然造成这样错误的原因是应用层自己,但是如果内核可以尽早地让错误发

    生,对于应用开发人员来说,会是一个福音。因为调试bug的时候,bug距离造成错

    误的地点越近,时间发生得越早,就越容易找到根本原因。这也是为什么释放内存

    以后,要将指针置为NULL的原因。

    从__put_unused_fd退出后,close会接着调用filp_close,其调用路径为filp_close->fput。在fput中,会对当前文件struct file的引用计数减一并检查其值是否

    为0。当引用计数为0时,表示该struct file没有被其他人使用,则可以调用__fput执

    行真正的文件释放操作,然后调用要关闭文件所属文件系统的release函数,从而实

    现针对不同的文件类型来执行不同的关闭操作。

    下一节让我们来看看Linux如何针对不同的文件类型,挂载不同的文件操作函

    数files_operations。1.4.3 自定义files_operations

    不失一般性,这里也选择socket文件系统作为示例,来说明Linux如何挂载文件

    系统指定的文件操作函数files_operations。

    socket.c中定义了其文件操作函数file_operations,代码如下:

    static const struct file_operations socket_file_ops = {

    .owner = THIS_MODULE,.llseek = no_llseek,.aio_read = sock_aio_read,.aio_write = sock_aio_write,.poll = sock_poll,.unlocked_ioctl = sock_ioctl,ifdef CONFIG_COMPAT

    .compat_ioctl = compat_sock_ioctl,endif

    .mmap = sock_mmap,.open = sock_no_open, special open code to disallow open via proc

    .release = sock_close,.fasync = sock_fasync,.sendpage = sock_sendpage,.splice_write = generic_splice_sendpage,.splice_read = sock_splice_read,};

    函数sock_alloc_file用于申请socket文件描述符及文件管理结构file结构。它调用

    alloc_file来申请管理结构file,并将socket_file_ops作为参数,如下所示:

    file = alloc_file(path, FMODE_READ | FMODE_WRITE,socket_file_ops);

    进入alloc_file,来看看如下代码:

    struct file alloc_file(struct path path, fmode_t mode,const struct file_operations fop)

    {

    struct file file;

    申请一个

    file

    file = get_empty_filp;

    if (!file)

    return NULL;

    file->f_path = path;

    file->f_mapping = path->dentry->d_inode->i_mapping;

    file->f_mode = mode;

    将自定义的文件操作函数赋给file->f_op

    file->f_op = fop;……

    }

    在初始化file结构的时候,socket文件系统将其自定义的文件操作赋给了file-

    >f_op,从而实现了在VFS中可以调用socket文件系统自定义的操作。1.4.4 遗忘close造成的问题

    我们只需要关注close文件的时候内核做了哪些事情,就可以确定遗忘close会带

    来什么样的后果,如下:

    ·文件描述符始终没有被释放。

    ·用于文件管理的某些内存结构没有被释放。

    对于普通进程来说,即使应用忘记了关闭文件,当进程退出时,Linux内核也

    会自动关闭文件,释放内存(详细过程见后文)。但是对于一个常驻进程来说,问

    题就变得严重了。

    先看第一种情况,如果文件描述符没有被释放,那么再次申请新的描述符时,就不得不扩展当前的文件表了,代码如下:

    int expand_files(struct files_struct files, int nr)

    {

    struct fdtable fdt;

    fdt = files_fdtable(files);

    N.B. For clone tasks sharing a files structure, this test

    will limit the total number of files that can be opened.

    if (nr >= rlimit(RLIMIT_NOFILE))

    return -EMFILE;

    Do we need to expand?

    if (nr < fdt->max_fds)

    return 0;

    Can we expand?

    if (nr >= sysctl_nr_open)

    return -EMFILE;

    All good, so we try

    return expand_fdtable(files, nr);

    }

    从上面的代码可以看出,在扩展文件表的时候,会检查打开文件的个数是否超

    出系统的限制。如果文件描述符始终不释放,其个数迟早会到达上限,并返回

    EMFILE错误(表示Too many open files(POSIX.1))。

    再看第二种情况,即文件管理的某些内存结构没有被释放。仍然是查看打开文件的代码,代码如下其中,get_empty_filp用于获得空闲的file结构。

    struct file get_empty_filp(void)

    {

    const struct cred cred = current_cred;

    static long old_max;

    struct file f;

    Privileged users can go above max_files

    这里对打开文件的个数进行检查,非特权用户不能超过系统的限制

    if (get_nr_files >= files_stat.max_files !capable(CAP_SYS_ADMIN)) {

    再次检查

    per cpu的文件个数的总和,为什么要做两次检查呢。后文会详细介绍

    if (percpu_counter_sum_positive(nr_files) >= files_stat.max_files)

    goto over;

    }

    未到达上限,申请一个新的

    file结构

    f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);

    if (f == NULL)

    goto fail;

    增加

    file结构计数

    percpu_counter_inc(nr_files);

    f->f_cred = get_cred(cred);

    if (security_file_alloc(f))

    goto fail_sec;

    INIT_LIST_HEAD(f->f_u.fu_list);

    atomic_long_set(f->f_count, 1);

    rwlock_init(f->f_owner.lock);

    spin_lock_init(f->f_lock);

    eventpoll_init_file(f);

    f->f_version: 0 return f;

    over:

    用完了

    file配额,打印

    log报错

    Ran out of filps - report that

    if (get_nr_files > old_max) {

    pr_info(VFS: file-max limit %lu reached\n, get_max_files);

    old_max = get_nr_files;

    }

    goto fail;

    fail_sec:

    file_free(f);

    fail:

    return NULL;

    }

    下面来说说为什么上面的代码要做两次检查——这也是我们学习内核代码的好

    处之一,可以学到很多的编程技巧和设计思路。

    对于file的个数,Linux内核使用两种方式来计数。一是使用全局变量,另外一

    个是使用percpu变量。更新全局变量时,为了避免竞争,不得不使用锁,所以Linux

    使用了一种折中的解决方案。当percpu变量的个数变化不超过正负

    percpu_counter_batch(默认为32)的范围时,就不更新全局变量。这样就减少了对

    全局变量的更新,可是也造成了全局变量的值不准确的问题。于是在全局变量的

    file个数超过限制时,会再对所有的percpu变量求和,再次与系统的限制相比较。想

    了解这个计数手段的详细信息,可以阅读percpu_counter_add的相关代码。1.4.5 如何查找文件资源泄漏

    在前面的小节中,我们看到了常驻进程忘记关闭文件的危害。可是,软件不可

    能不出现bug,如果常驻进程程序真的出现了这样的问题,如何才能快速找到根本

    原因呢?通过审查打开文件的代码?时间长效率低。那是否还有其他办法呢?下面

    我们来介绍一种能快速查找文件资源泄漏的方法。

    首先,创建一个“错误”的程序,代码如下:

    include

    include

    include

    int main(void)

    {

    int cnt = 0;

    while (1) {

    char name[64];

    snprintf(name, sizeof(name),%d.txt, cnt);

    int fd = creat(name, 644);

    sleep(10);

    ++cnt;

    }

    return 0;

    }

    在这段代码的循环过程中,打开了一个文件,但是一直没有被关闭,以此来模

    拟服务程序的文件资源泄漏,然后让程序运行一段时间:

    [fgao@fgao chapter1].hold_file

    [1] 3000

    接下来请出利器lsof,查看相关信息,如下所示:

    [fgao@fgao chapter1]lsof -p 3000

    COMMAND PID USER FD TYPE DEVICE SIZEOFF NODE NAME

    a.out 3000 fgao cwd DIR 253,2 4096 1321995 homefgaoworksmy_git_codesmy_booksunderstanding_apuesample_codeschapter1

    a.out 3000 fgao rtd DIR 253,1 4096 2

    a.out 3000 fgao txt REG 253,2 6115 1308841 homefgaoworksmy_git_codesmy_booksunderstanding_apuesample_codeschapter1a.out

    a.out 3000 fgao mem REG 253,1 157200 1443950 libld-2.14.90.so

    a.out 3000 fgao mem REG 253,1 2012656 1443951 liblibc-2.14.90.so

    a.out 3000 fgao 0u CHR 136,3 0t0 6 devpts3

    a.out 3000 fgao 1u CHR 136,3 0t0 6 devpts3

    a.out 3000 fgao 2u CHR 136,3 0t0 6 devpts3

    a.out 3000 fgao 3w REG 253,2 0 1309088 homefgaoworksmy_git_codes my_booksunderstanding_apuesample_codeschapter10.txt

    a.out 3000 fgao 4w REG 253,2 0 1312921 homefgaoworksmy_git_codesmy_booksunderstanding_apuesample_codeschapter11.txt

    a.out 3000 fgao 5w REG 253,2 0 1327890 homefgaoworksmy_git_codesmy_booksunderstanding_apuesample_codeschapter12.txt

    a.out 3000 fgao 6w REG 253,2 0 1327891 homefgaoworksmy_git_codesmy_booksunderstanding_apuesample_codeschapter13.txt

    a.out 3000 fgao 7w REG 253,2 0 1327892 homefgaoworksmy_git_codesmy_booksunderstanding_apuesample_codeschapter14.txta.out 3000 fgao 8w REG 253,2 0 1327893 homefgaoworksmy_git_codesmy_booksunderstanding_apuesample_codeschapter15.txt

    a.out 3000 fgao 9w REG 253,2 0 1327894 homefgaoworksmy_git_codesmy_booksunderstanding_apuesample_codeschapter16.txt

    从lsof的输出结果可以清晰地看出,hold_file打开的哪些文件没有被关闭。其实

    从proc3000fd中也可以得到类似的结果。但是lsof拥有更多的选项和功能(如指定

    某个目录),可以应对更复杂的情况。具体细节就需要读者自行阅读lsof的说明文

    档了。1.5 文件偏移

    文件偏移是基于某个打开文件来说的,一般情况下,读写操作都会从当前的偏

    移位置开始读写(所以read和write都没有显式地传入偏移量),并且在读写结束后

    更新偏移量。1.5.1 lseek简介

    lseek的原型如下:

    off_t lseek(int fd, off_t offset, int whence);

    该函数用于将fd的文件偏移量设置为以whence为起点,偏移为offset的位置。其

    中whence可以为三个值:SEEK_SET、SEEK_CUR和SEEK_END,分别表示为“文

    件的起始位置”、“文件的当前位置”和“文件的末尾”,而offset的取值正负均可。

    lseek执行成功后,会返回新的文件偏移量。

    在Linux 3.1以后,Linux又增加了两个新的值:SEEK_DATA和SEEK_HOLE,分别用于查找文件中的数据和空洞。1.5.2 小心lseek的返回值

    对于Linux中的大部分系统调用来说,如果返回值是负数,那它一般都是错误

    的,但是对于lseek来说这条规则不适用。且看lseek的返回值说明:

    注意 当lseek执行成功时,它会返回最终以文件起始位置为起点的偏移位

    置。如果出错,则返回-1,同时errno被设置为对应的错误值。

    也就是说,一般情况下,对于普通文件来说,lseek都是返回非负的整数,但是

    对于某些设备文件来说,是允许返回负的偏移量。因此要想判断lseek是否真正出

    错,必须在调用lseek前将errno重置为0,然后再调用lseek,同时检查返回值是否

    为-1及errno的值。只有当两个同时成立时,才表明lseek真正出错了。

    因为这里的文件偏移都是内核的概念,所以lseek并不会引起任何真正的IO操

    作。1.5.3 lseek源码分析

    lseek的源码位于read_write.c中,如下:

    SYSCALL_DEFINE3(lseek, unsigned int, fd, off_t, offset, unsigned int, origin)

    {

    off_t retval;

    struct file file;

    int fput_needed;

    retval = -EBADF;

    根据

    fd得到

    file指针

    file = fget_light(fd, fput_needed);

    if (!file)

    goto bad;

    retval = -EINVAL;

    对初始位置进行检查,目前

    linux内核支持的初始位置有

    1.5.1节中提到的五个值

    if (origin <= SEEK_MAX) {

    loff_t res = vfs_llseek(file, offset, origin);

    下面这段代码,先使用

    res来给

    retval赋值,然后再次判断

    res

    是否与retval相等。为什么会有这样的逻辑呢?什么时候两者会不相等呢?

    只有在

    retval与

    res的位数不相等的情况下。

    retval的类型是

    off_t->__kernel_off_t->long;

    而

    res的类型是

    loff_t->__kernel_off_t->long long;

    在

    32位机上,前者是

    32位,而后者是

    64位。当

    res的值超过了

    retval

    的范围时,两者将会不等。即实际偏移量超过了long类型的表示范围。

    retval = res;

    if (res != (loff_t)retval)

    retval = -EOVERFLOW; LFS: should only happen on 32 bit platforms

    }

    fput_light(file, fput_needed);

    bad:

    return retval;

    }

    然后进入vfs_llseek,代码如下:

    loff_t vfs_llseek(struct file file, loff_t offset, int origin)

    {

    loff_t (fn)(struct file , loff_t, int);

    默认的

    lseek操作是

    no_llseek,当

    file没有对应的

    llseek实现时,就

    会调用

    no_llseek,并返回

    -ESPIPE错误

    fn = no_llseek;

    if (file->f_mode FMODE_LSEEK) {

    if (file->f_op file->f_op->llseek)

    fn = file->f_op->llseek;

    } return fn(file, offset, origin);

    }

    当file支持llseek操作时,就会调用具体的llseek函数。在此,选择default_llseek

    作为实例,代码如下:

    loff_t default_llseek(struct file file, loff_t offset, int origin)

    {

    struct inode inode = file->f_path.dentry->d_inode;

    loff_t retval;

    mutex_lock(inode->i_mutex);

    switch (origin) {

    case SEEK_END:

    最终偏移等于文件的大小加上指定的偏移量

    offset += i_size_read(inode);

    break;

    case SEEK_CUR:

    offset为

    0时,并不改变当前的偏移量,而是直接返回当前偏移量

    if (offset == 0) {

    retval = file->f_pos;

    goto out;

    }

    若

    offset不为

    0,则最终偏移等于指定偏移加上当前偏移

    offset += file->f_pos;

    break;

    case SEEK_DATA:

    In the generic case the entire file is data, so as

    long as offset isn't at the end of the file then the

    offset is data.

    如注释所言,对于一般文件,只要指定偏移不超过文件大小,那么指 定偏移的位置就是数据位置

    if (offset >= inode->i_size) {

    retval = -ENXIO;

    goto out;

    }

    break;

    case SEEK_HOLE:

    There is a virtual hole at the end of the file, so

    as long as offset isn't i_size or larger, return

    i_size.

    只要指定偏移不超过文件大小,那么下一个空洞位置就是文件的末尾

    if (offset >= inode->i_size) {

    retval = -ENXIO;

    goto out;

    }

    offset = inode->i_size;

    break;

    }

    retval = -EINVAL;

    对于一般文件来说,最终的

    offset必须大于或等于

    0,或者该文件的模式要求只能产生无符号的偏移量。否则就会报错

    if (offset >= 0 || unsigned_offsets(file)) {

    当最终偏移不等于当前位置时,则更新文件的当前位置

    if (offset != file->f_pos) {

    file->f_pos = offset;

    file->f_version = 0;

    }

    retval = offset;

    }

    out:

    mutex_unlock(inode->i_mutex);

    return retval;

    }1.6 读取文件

    Linux中读取文件操作时,最常用的就是read函数,其原型如下:

    ssize_t read(int fd, void buf, size_t count);

    read尝试从fd中读取count个字节到buf中,并返回成功读取的字节数,同时将文

    件偏移向前移动相同的字节数。返回0的时候则表示已经到了“文件尾”。read还有可

    能读取比count小的字节数。

    使用read进行数据读取时,要注意正确地处理错误,也是说read返回-1时,如

    果errno为EAGAIN、EWOULDBLOCK或EINTR,一般情况下都不能将其视为错

    误。因为前两者是由于当前fd为非阻塞且没有可读数据时返回的,后者是由于read

    被信号中断所造成的。这两种情况基本上都可以视为正常情况。1.6.1 read源码跟踪

    先来看看read的源码,代码如下:

    SYSCALL_DEFINE3(read, unsigned int, fd, char __user , buf, size_t, count)

    {

    struct file file;

    ssize_t ret = -EBADF;

    int fput_needed;

    通过文件描述符

    fd得到管理结构

    file

    file = fget_light(fd, fput_needed);

    if (file) {

    得到文件的当前偏移量

    loff_t pos = file_pos_read(file);

    利用

    vfs进行真正的

    read

    ret = vfs_read(file, buf, count, pos);

    更新文件偏移量

    file_pos_write(file, pos);

    归还管理结构

    file,如有必要,就进行引用计数操作

    fput_light(file, fput_needed);

    }

    return ret;

    }再进入vfs_read,代码如下:

    ssize_t vfs_read(struct file file, char __user buf, size_t count, loff_t pos)

    {

    ssize_t ret;

    检查文件是否为读取打开

    if (!(file->f_mode FMODE_READ))

    return -EBADF;

    检查文件是否支持读取操作

    if (!file->f_op || (!file->f_op->read !file->f_op->aio_read))

    return -EINVAL;

    检查用户传递的参数

    buf的地址是否可写

    if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))

    return -EFAULT;

    检查要读取的文件范围实际可读取的字节数

    ret = rw_verify_area(READ, file, pos, count);

    if (ret >= 0) {

    根据上面的结构,调整要读取的字节数

    count = ret;

    如果定义

    read操作,则执行定义的

    read操作

    如果没有定义

    read操作,则调用do_sync_read—其利用异步

    aio_read来完成同步的

    read操作。

    if (file->f_op->read)

    ret = file->f_op->read(file, buf, count, pos);

    else

    ret = do_sync_read(file, buf, count, pos);

    if (ret > 0) {

    读取了一定的字节数,进行通知操作

    fsnotify_access(file);

    增加进程读取字节的统计计数

    add_rchar(current, ret);

    }

    增加进程系统调用的统计计数

    inc_syscr(current);

    }

    return ret;

    }

    上面的代码为read公共部分的源码分析,具体的读取动作是由实际的文件系统

    决定的。1.6.2 部分读取

    前文中介绍read可以返回比指定count少的字节数,那么什么时候会发生这种情

    况呢?最直接的想法是在fd中没有指定count大小的数据时。但这种情况下,系统是

    不是也可以阻塞到满足count个字节的数据呢?那么内核到底采取的是哪种策略

    呢?

    让我们来看看socket文件系统中UDP协议的read实现:socket文件系统只定义了

    aio_read操作,没有定义普通的read函数。根据前文,在这种情况下do_sync_read会

    利用aio_read实现同步读操作。

    其调用链为sock_aio_read->do_sock_read->__sock_recvmsg-

    >__sock_recvmsg_nose->udp_recvmsg,代码如下所示:

    int udp_recvmsg(struct kiocb iocb, struct sock sk, struct msghdr msg,size_t len, int noblock, int flags, int addr_len)……

    ulen = skb->len - sizeof(struct udphdr);

    copied = len;

    if (copied > ulen)

    copied = ulen;……

    当UDP报文的数据长度小于参数len时,就会只复制真正的数据长度,那么对

    于read操作来说,返回的读取字节数自然就小于参数count了。

    看到这里,是否已经得到本小节开头部分问题的答案了呢?当fd中的数据不够

    count大小时,read会返回当前可以读取的字节数?很可惜,答案是否定的。这种行

    为完全由具体实现来决定。即使同为socket文件系统,TCP套接字的读取操作也会

    与UDP不同。当TCP的fd的数据不足时,read操作极可能会阻塞,而不是直接返回。注:TCP是否阻塞,取决于当前缓存区可用数据多少,要读取的字节数,以及

    套接字设置的接收低水位大小。

    因此在调用read的时候,只能根据read接口的说明,小心处理所有的情况,而

    不能主观臆测内核的实现。比如本文中的部分读取情况,阻塞和直接返回两种策略

    同时存在。1.7 写入文件

    Linux中写入文件操作,最常用的就是write函数,其原型如下:

    ssize_t write(int fd, const void buf, size_t count);

    write尝试从buf指向的地址,写入count个字节到文件描述符fd中,并返回成功

    写入的字节数,同时将文件偏移向前移动相同的字节数。write有可能写入比指定

    count少的字节数。1.7.1 write源码跟踪

    write的源码与read的很相似,位于read_write.c中,代码如下:

    SYSCALL_DEFINE3(write, unsigned int, fd, const char __user , buf,size_t, count)

    {

    struct file file;

    ssize_t ret = -EBADF;

    int fput_needed;

    得到

    file管理结构指针

    file = fget_light(fd, fput_needed);

    if (file) {

    得到当前的文件偏移

    loff_t pos = file_pos_read(file);

    利用

    VFS写入

    ret = vfs_write(file, buf, count, pos);

    更新文件偏移量

    file_pos_write(file, pos);

    释放文件管理指针

    file

    fput_light(file, fput_needed);

    }

    return ret;

    }

    进入vfs_write,代码如下:ssize_t vfs_write(struct file file, const char __user buf, size_t count, loff_t pos)

    {

    ssize_t ret;

    检查文件是否为写入打开

    if (!(file->f_mode FMODE_WRITE))

    return -EBADF;

    检查文件是否支持打开操作

    if (!file->f_op || (!file->f_op->write !file->f_op->aio_write))

    return -EINVAL;

    检查用户给定的地址范围是否可读取

    if (unlikely(!access_ok(VERIFY_READ, buf, count)))

    return -EFAULT;

    验证文件从

    pos起始是否可以写入

    count个字节数

    并返回可以写入的字节数

    ret = rw_verify_area(WRITE, file, pos, count);

    if (ret >= 0) {

    更新写入字节数

    count = ret;

    如果定义

    write操作,则执行定义的

    write操作 如果没有定义

    write操作,则调用

    do_sync_write—其利用异步

    aio_write来完成同步的

    write操作

    if (file->f_op->write)

    ret = file->f_op->write(file, buf, count, pos);

    else

    ret = do_sync_write(file, buf, count, pos);

    if (ret > 0) {

    写入了一定的字节数,进行通知操作

    fsnotify_modify(file);

    增加进程读取字节的统计计数

    add_wchar(current, ret);

    }

    增加进程系统调用的统计计数

    inc_syscw(current);

    }

    return ret;

    }

    write同样有部分写入的情况,这个与read类似,都是由具体实现来决定的。在

    此就不再深入探讨write的部分写入的情况了。1.7.2 追加写的实现

    前面说过,文件的读写操作都是从当前文件的偏移处开始的。这个文件偏移量

    保存在文件表中,而每个进程都有一个文件表。那么当多个进程同时写一个文件

    时,即使对write进行了锁保护,在进行串行写操作时,文件依然不可避免地会被写

    乱。根本原因就在于文件偏移量是进程级别的。

    当使用O_APPEND以追加的形式来打开文件时,每次写操作都会先定位到文件

    末尾,然后再执行写操作。

    Linux下大多数文件系统都是调用generic_file_aio_write来实现写操作的。在

    generic_file_aio_write中,有如下代码:

    mutex_lock(inode->i_mutex);

    blk_start_plug(plug);

    ret = __generic_file_aio_write(iocb, iov, nr_segs, iocb->ki_pos);

    mutex_unlock(inode->i_mutex);

    这里有一个关键的语句,就是使用mutex_lock对该文件对应的inode进行保护,然后调用__generic_file_aio_write->generic_write_check。其部分代码如下:

    if (file->f_flags O_APPEND)

    pos = i_size_read(inode);

    上面的代码中,如果发现文件是以追加方式打开的,则将从inode中读取到的

    最新文件大小作为偏移量,然后通过__generic_file_aio_write再进行写操作,这样就

    能保证写操作是在文件末尾追加的。1.8 文件的原子读写

    使用O_APPEND可以实现在文件的末尾原子追加新数据,Linux还提供pread和

    pwrite从指定偏移位置读取或写入数据。

    它们的实现很简单,代码如下:

    SYSCALL_DEFINE(pread64)(unsigned int fd, char __user buf,size_t count, loff_t pos)

    {

    struct file file;

    ssize_t ret = -EBADF;

    int fput_needed;

    if (pos < 0)

    return -EINVAL;

    file = fget_light(fd, fput_needed);

    if (file) {

    ret = -ESPIPE;

    if (file->f_mode FMODE_PREAD)

    ret = vfs_read(file, buf, count, pos);

    fput_light(file, fput_needed);

    }

    return ret;

    }

    看到这段代码,是不是有一种似曾相识的感觉?让我们再来回顾一下read的实

    现,代码如下所示。

    得到文件的当前偏移量

    loff_t pos = file_pos_read(file);

    利用

    vfs进行真正的

    read

    ret = vfs_read(file, buf, count, pos);

    更新文件偏移量

    file_pos_write(file, pos);这就是它与read的主要区别。pread不会从文件表中获取当前偏移,而是直接使

    用用户传递的偏移量,并且在读取完毕后,不会更改当前文件的偏移量。

    pwrite的实现与pread类似,在此就不再重复描述了。1.9 文件描述符的复制

    Linux提供了三个复制文件描述符的系统调用,分别为:

    int dup(int oldfd);

    int dup2(int oldfd, int newfd);

    int dup3(int oldfd, int newfd, int flags);

    其中:

    ·dup会使用一个最小的未用文件描述符作为复制后的文件描述符。

    ·dup2是使用用户指定的文件描述符newfd来复制oldfd的。如果newfd已经是打

    开的文件描述符,Linux会先关闭newfd,然后再复制oldfd。

    ·对于dup3,只有定义了feature宏“_GNU_SOURCE”才可以使用,它比dup2多了

    一个参数,可以指定标志——不过目前仅仅支持O_CLOEXEC标志,可在newfd上

    设置O_CLOEXEC标志。定义dup3的原因与open类似,可以在进行dup操作的同时

    原子地将fd设置为O_CLOEXEC,从而避免将文件内容暴露给子进程。

    为什么会有dup、dup2、dup3这种像兄弟一样的系统调用呢?这是因为随着软

    件工程的日益复杂,已有的系统调用已经无法满足需求,或者存在安全隐患,这

    时,就需要内核针对已有问题推出新的接口。

    话说在很久以前,程序员在写daemon服务程序时,基本上都有这样的流程:首

    先关闭标准输出stdout、标准出错stderr,然后进行dup操作,将stdout或stderr重定

    向。但是在多线程程序成为主流以后,由于close和dup操作不是原子的,这就造成

    了在某些情况下,重定向会失败。因此就引入了dup2将close和dup合为一个系统调

    用,以保证原子性,然而这依然有问题。大家可以回顾1.2.2节中对O_CLOEXEC的

    介绍。在多线程中进行fork操作时,dup2同样会有让相同的文件描述符暴露的风

    险,dup3也就随之诞生了。这三个系统调用看起来有些冗余重复,但实际上它们也是软件工程发展的结果。从这个dup的发展过程来看,我们也可以领会到编写健壮

    代码的不易。正如前文所述,对于一个现代接口,一般都会有一个flag标志参数,这样既可以保证兼容性,还可以通过引用新的标志来改善或纠正接口的行为。

    下面先看dup的实现,如下所示:

    SYSCALL_DEFINE1(dup, unsigned int, fildes)

    {

    int ret = -EBADF;

    必须先得到文件管理结构

    file,同时也是对描述符

    fildes的检查

    struct file file = fget_raw(fildes);

    if (file) {

    得到一个未使用的文件描述符

    ret = get_unused_fd;

    if (ret >= 0) {

    将文件描述符与

    file指针关联起来

    fd_install(ret, file);

    }

    else

    fput(file);

    }

    return ret;

    }

    然后,再看看fd_install的实现,代码如下所示:

    void fd_install(unsigned int fd, struct file file)

    {

    struct files_struct files = current->files;

    struct fdtable fdt;

    对文件表进行保护

    spin_lock(files->file_lock);

    得到文件表

    fdt = files_fdtable(files);

    BUG_ON(fdt->fd[fd] != NULL);

    让文件表中

    fd对应的指针等于该文件关联结构

    file

    rcu_assign_pointer(fdt->fd[fd], file);

    spin_unlock(files->file_lock);

    }

    在dup中调用get_unused_fd,只是得到一个未用的文件描述符,那么如何实现

    在dup接口中使用最小的未用文件描述符呢?这就需要回顾1.4.2节中总结过的Linux

    文件描述符的选择策略了。

    Linux总是尝试给用户最小的未用文件描述符,所以get_unused_fd得到的文件

    描述符始终是最小的可用文件描述符。

    在fd_install中,fd与file的关联是利用fd来作为指针数组的索引的,从而让对应

    的指针指向file。对于dup来说,这意味着数组中两个指针都指向了同一个file。而

    file是进程中真正的管理文件的结构,文件偏移等信息都是保存在file中的。这就意

    味着,当使用oldfd进行读写操作时,无论是oldfd还是newfd的文件偏移都会发生变

    化。

    再来看一下dup2的实现,如下所示:

    SYSCALL_DEFINE2(dup2, unsigned int, oldfd, unsigned int, newfd)

    {

    如果

    oldfd与newfd相等,这是一种特殊的情况

    if (unlikely(newfd == oldfd)) { corner case

    struct files_struct files = current->files;

    int retval = oldfd;

    检查

    oldfd的合法性,如果是合法的

    fd,则直接返回

    oldfd的值;

    如果是不合法的,则返回

    EBADF

    rcu_read_lock;

    if (!fcheck_files(files, oldfd))

    retval = -EBADF;

    rcu_read_unlock;

    return retval;

    }

    如果

    oldfd与

    newfd不同,则利用

    sys_dup3来实现

    dup2

    return sys_dup3(oldfd, newfd, 0);

    }再来查看一下dup3的实现代码,如下所示:

    SYSCALL_DEFINE3(dup3, unsigned int, oldfd, unsigned int, newfd, int, flags)

    {

    int err = -EBADF;

    struct file file, tofree;

    struct files_struct files = current->files;

    struct fdtable fdt;

    对标志

    flags进行检查,支持

    O_CLOEXEC

    if ((flags ~O_CLOEXEC) != 0)

    return -EINVAL;

    与

    dup2不同,当

    oldfd与

    newfd相同的时候,dup3返回错误

    if (unlikely(oldfd == newfd))

    return -EINVAL;

    spin_lock(files->file_lock);

    根据

    newfd决定是否需要扩展文件表的大小

    err = expand_files(files, newfd);

    检查

    oldfd,如果是非法的,就直接返回 不过我更倾向于先检查

    oldfd后扩展文件表,如果是非法的,就不需要扩展文件表了

    file = fcheck(oldfd);

    if (unlikely(!file))

    goto Ebadf;

    if (unlikely(err < 0)) {

    if (err == -EMFILE)

    goto Ebadf;

    goto out_unlock;

    }

    err = -EBUSY;

    得到文件表

    fdt = files_fdtable(files);

    通过

    newfd得到对应的

    file结构

    tofree = fdt->fd[newfd];

    tofree是

    NULL,但是

    newfd已经分配的情况

    if (!tofree FD_ISSET(newfd, fdt->open_fds))

    goto out_unlock;

    增加

    file的引用计数

    get_file(file);

    将文件表

    newfd对应的指针指向

    file

    rcu_assign_pointer(fdt->fd[newfd], file);

    将

    newfd加到打开文件的位图中

    如果

    newfd已经是一个合法的

    fd,重复设置位图则没有影响;

    如果

    newfd没有打开,则必须将其加入位图中

    那么为什么不对

    newfd进行检查呢?因为检查比设置位图更消耗

    CPU

    FD_SET(newfd, fdt->open_fds);

    如果

    flags设置了O_CLOEXEC,则将

    newfd加到

    close_on_exec位图;

    如果没有设置,则清除

    close_on_exec位图中对应的位

    if (flags O_CLOEXEC)

    FD_SET(newfd, fdt->close_on_exec);

    else

    FD_CLR(newfd, fdt->close_on_exec);

    spin_unlock(files->file_lock);

    如果

    tofree不为空,则需要关闭

    newfd之前的文件

    if (tofree)

    filp_close(tofree, files);

    return newfd;

    Ebadf:

    err = -EBADF;

    out_unlock:

    spin_unlock(files->file_lock);

    return err;

    }1.10 文件数据的同步

    为了提高性能,操作系统会对文件的IO操作进行缓存处理。对于读操作,如

    果要读取的内容已经存在于文件缓存中,就直接读取文件缓存。对于写操作,会先

    将修改提交到文件缓存中,在合适的时机或者过一段时间后,操作系统才会将改动

    提交到磁盘上。

    Linux提供了三个同步接口:

    void sync(void);

    int fsync(int fd);

    int fdatasync(int fd);

    APUE上说sync只是让所有修改过的缓存进入提交队列,并不用等待这个工作

    完成。Linux手册上则表示从1.3.20版本开始,Linux就会一直等待,直到提交工作

    完成。

    实际情况到底是怎样的呢,让代码告诉我们真相,具体如下:

    SYSCALL_DEFINE0(sync)

    {

    唤醒后台内核线程,将“脏”缓存冲刷到磁盘上

    wakeup_flusher_threads(0, WB_REASON_SYNC);

    为什么要调用两次

    sync_filesystems呢?

    这是一种编程技巧,第一次

    sync_filesystems(0),参数0表示不等待,可以

    迅速地将没有上锁的

    inode同步。第二次

    sync_filesystems(1),参数

    1表示等待。

    对于上锁的

    inode会等待到解锁,再执行同步,这样可以提高性能。因为第一次操作

    中,上锁的

    inode很可能在第一次操作结束后,就已经解锁,这样就避免了等待

    sync_filesystems(0);

    sync_filesystems(1);

    如果是

    laptop模式,那么因为此处刚刚做完同步,因此可以停掉后台同步定时器

    if (unlikely(laptop_mode))

    laptop_sync_completion;

    return 0;

    }

    再看一下sync_filesystems->iterate_supers->sync_one_sb->__sync_filesystem,代码如下:

    static int __sync_filesystem(struct super_block sb, int wait)

    {

    This should be safe, as we require bdi backing to actually

    write out data in the first place

    if (sb->s_bdi == noop_backing_dev_info)

    return 0;

    磁盘配额同步

    if (sb->s_qcop sb->s_qcop->quota_sync)

    sb->s_qcop->quota_sync(sb, -1, wait);

    如果

    wait为

    true,则一直等待直到所有的脏

    inode写入磁盘

    如果

    wait为

    false,则启动脏

    inode回写工作,但不必等待到结束

    if (wait)

    sync_inodes_sb(sb);

    else

    writeback_inodes_sb(sb, WB_REASON_SYNC);

    如果该文件系统定义了自己的同步操作,则执行该操作

    if (sb->s_op->sync_fs)

    sb->s_op->sync_fs(sb, wait);

    调用

    block设备的

    flush操作,真正地将数据写到设备上

    return __sync_blockdev(sb->s_bdev, wait);

    }

    从sync的代码实现上看,Linux的sync是阻塞调用,这里与APUE的说明是不一

    样的。

    下面来看看fsync与fdatasync,fsync只同步fd指定的文件,并且直到同步完成才

    返回。fdatasync与fsync类似,但是其只同步文件的实际数据内容,和会影响后面数

    据操作的元数据。而fsync不仅同步数据,还会同步所有被修改过的文件元数据,代

    码如下所示:

    SYSCALL_DEFINE1(fsync, unsigned int, fd)

    {

    return do_fsync(fd, 0);

    }

    SYSCALL_DEFINE1(fdatasync, unsigned int, fd)

    {

    return do_fsync(fd, 1);

    }

    事实上,真正进行工作的是do_fsync,代码如下所示:

    static int do_fsync(unsigned int fd, int datasync)

    {

    struct file file;

    int ret = -EBADF;

    得到

    file管理结构

    file = fget(fd); if (file) {

    利用

    vfs执行

    sync操作

    ret = vfs_fsync(file, datasync);

    fput(file);

    }

    return ret;

    }

    进入vfs_fsync->vfs_fsync_range,代码如下:

    int vfs_fsync_range(struct file file, loff_t start, loff_t end, int datasync)

    {

    调用具体操作系统的同步操作

    if (!file->f_op || !file->f_op->fsync)

    return -EINVAL;

    return file->f_op->fsync(file, start, end, datasync);

    }

    真正执行同步操作的fsync是由具体的文件系统的操作函数file_operations决定

    的。下面选择一个常用的文件系统同步函数generic_file_fsync,代码如下。

    int generic_file_fsync(struct file file, loff_t start, loff_t end,int datasync)

    {

    struct inode inode = file->f_mapping->host;

    int err;

    int ret;

    同步该文件缓存中处于

    start到

    end范围内的脏页

     ......

您现在查看是摘要介绍页, 详见PDF附件(15158KB,1044页)