linux内核编译及添加系统调用(hdu)_浅谈关于Linux内核write系统调用操作的原子性

Linux系统的write调用到底是不是原子的。网上能搜出一大堆文章,基本上要么是翻译一些文献,要么就是胡扯,本文中我来结合实例来试着做一个稍微好一点的回答。


先摆出结论吧。结论包含两点,即write调用不能保证什么以及write调用能保证什么

首先,write调用不能保证你要求的调用是原子的,以下面的调用为例:

ret = write(fd, buff, 512);

Linux无法保证将512字节的buff写入文件这件事是原子的,因为:

  1. 即便你写了512字节那也只是最大512字节,buff不一定有512字节这么大;
  2. write操作有可能被信号中途打断,进而使得ret实际上小于512;
  3. 实现根据不同的系统而不同,且几乎都是分层,作为接口无法确保所有层资源预留。磁盘的缓冲区可能空间不足,导致底层操作失败。

如果不考虑以上这些因素,write调用为什么不设计成直接返回True或者False呢?要么成功写入512字节,要么一点都不写入,这样岂不更好?之所以不这么设计,正是基于上述不可回避的因素来考虑的。

  在系统调用设计的意义上,不信任的价值大于信任,最坏的考虑优先于乐观地盲进

  其次,write调用能保证的是,不管它实际写入了多少数据,比如写入了n字节数据,在写入这n字节数据的时候,在所有共享文件描述符的线程或者进程之间,每一个write调用是原子的,不可打断的。举一个例子,比如线程1写入了3个字符’a’,线程2写入了3个字符’b’,结果一定是‘aaabbb’或者是‘bbbaaa’,不可能是类似‘abaabb’这类交错的情况。

  也许你自然而然会问一个问题,如果两个进程没有共享文件描述符呢?比如进程A和进程B分别独立地打开了一个文件,进程A写入3个字符’a’,进程B写入了3个字符’b’,结果怎样呢?

  答案是,这种情况下没有任何保证,最终的结果可能是‘aaabbb’或者是‘bbbaaa’,也可能是‘abaabb’这种交错的情况。如果你希望不交错,那么怎么办呢?答案也是有的,那就是在所有写进程打开文件的时候,采用O_APPEND方式打开即可。

  作为一个和用户态交互的典型系统调用,write无法保证用户要求的事情是原子的,但它在共享文件的范围内能保证它实际完成的事情是原子的,在非共享文件的情况下,虽然它甚至无法保证它完成的事情是原子的,但是却提供了一种机制可以做到这种保证。可见,write系统调用设计的非常之好,边界十分清晰!

  关于以上的这些保证是如何做到的,下面简要地解释下。我本来是不想解释的,但是看了下面的解释后,对于理解上述的保证很有帮助,所以就不得不追加了。解释归于下图所示:

646a1229f1ccad65d2814fbadfb20cc6.png

总结一下套路:

  1. APPEND模式通过锁inode,保证每次写操作均在inode中获取的文件size后追加数据,写完后释放锁;
  2. 非APPEND模式通过锁file结构体后获取file结构体的pos字段,并将数据追加到pos后,写完更新pos字段后释放锁。

由此可见,APPEND模式提供了文件层面的全局写安全,而非APPEND模式则提供了针对共享file结构体的进程/线程之间的写安全。

  值得一再重申的是,由于write调用只是在inode或者file层面上保证一次写操作的原子性,但无法保证用户需要写入的数据的一次肯定被写完,所以在多线程多进程文件共享情况下就需要用户态程序自己来应对short write问题,比如设计一个锁保护一个循环,直到写完成或者写出错,不然循环不退出,锁不释放…

  此外,我们知道,apache,nginx以及另外一些服务器写日志都是通过APPEND来保证独立原子写入的,要知道这些日志对于这类服务器而言是极端重要的。


本文写到这里貌似应该可以结束了,但是下面才是重头戏!

需要C/C++ Linux服务器架构师学习资料后台私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

64d85fd7acef28996e5144a90f96f623.png

  我写了一个分析TCP数据包的程序,通过不断打日志的方式把数据包的信息记录在文件里,程序是个多线程程序,大概10多个线程同时写一个内存文件系统的文件,最后我发现少了一条日志!程序本身不是重点,我可以通过以下的小程序代之解释:

#include #include #include #include #include #include #include #include char a[512];char b[16];int main(){        int fd;        memset(a, 'a', 512);        memset(b, '-', 16);        fd = open("/usr/src/probe/test.txt", O_RDWR|O_CREAT|O_TRUNC, 0660);        if (fork() == 0) {                prctl(PR_SET_NAME, (unsigned long)"child");                write(fd, b, 16);                exit(0);        }        write(fd, a, 512);        exit(0);}

编译为parent并运行,你猜猜最后test.txt里面是什么内容?

  由于父子进程是共享fd指示的file结构体的,按照上面的解释,最终的文件内容肯定是下面两种中的一种:

----------------aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1

或者:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa----------------1

可是,事实并不是这样!事实上,在很小的概率下,文件中只有512个字符‘a’,没有看到任何字符‘-‘(当然还会有别的情况)!Why?

  你能理解,当事实和理论分析不符的时候是多么痛苦,标准上明明就是说要保证共享file结构体的进程/线程一次写操作的原子性,然而事实证明有部分内容确实是被覆盖了,这显然并不合理。

  再者说了,系统调用在设计之初就要做出某种级别的保证,比如一次操作的原子性等等,这样的系统API才更友好,我相信标准是对的,所以我就觉得这是代码的BUG所致。是这么个思路吗?

  不!上面的这段话是事后诸葛亮的言辞,本文其实是一篇倒叙,是我先发现了写操作被覆盖,进而去逐步排查,最终才找到本文最开始的那段理论的,而不是反过来。所以,在我看到这个莫名其妙的错误后,我并不知道这是否合理,我只是依靠信仰觉得这次又是内核的BUG!然而我如何来证明呢?

  首先我要想到一个写操作被覆盖的场景,然后试着去重现这个场景,最终去修复它。首先第一步还是看代码,出问题的内核是3.10社区版内核,于是我找到源码:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,        size_t, count){    struct fd f = fdget(fd);    ssize_t ret = -EBADF;    if (f.file) {        loff_t pos = file_pos_read(f.file);        ret = vfs_write(f.file, buf, count, &pos);        file_pos_write(f.file, pos);        fdput(f);    }    return ret;}

说实话,这段代码我是分析了足足10分钟才发现一个race的。简单讲,我把这个系统调用分解为了三部分:

  1. get pos
  2. vfs_write
  3. update pos

race发生在1和2或者2和3之间。以下图示之:

8fecdbc052745a5d94a1c9444805752f.png

既然找到了就容易重现了,方法有两类,一类是拼命那个写啊写,碰运气重现,但这不是我的方式,另一种方法我比较喜欢,即故意放大race的条件!

  对于本文的场景,我使用jprobe机制故意在1和2之间插入了一个schedule。试着加载包含下面代码的模块:

ssize_t jvfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos){        if (!strcmp(current->comm, "parent")) {                msleep(2000);        }        jprobe_return();        return 0;}static struct jprobe delay_stub = {        .kp = {                .symbol_name    = "vfs_write",        },        .entry  = jvfs_write,};

我是HZ1000的机器,上述代码即在1和2之间睡眠2秒钟,这样几乎可以100%重现问题。

  试着跑了一遍,真的就重现了!文件中有512个字符‘a’,没有看到任何字符‘-‘

  看起来这问题在多CPU机器上是如此地容易重现,以至于任何人都会觉得这问题不可能会留到3.10内核还不被修补啊!但是内核源码摆在那里,确实是有问题啊!这个时候,我才想起去看一些文档,看看这到底是一个问题呢还是说这本身是合理的,只是需要用户态程序采用某种手段去规避。曲折之路就不多赘述了,直接man 2 write,看BUGS section

BUGS       According to POSIX.1-2008/SUSv4 Section XSI 2.9.7 ("Thread Interactions with Regular File Operations"):           All of the following functions shall be atomic with respect to each other in the effects specified in POSIX.1-2008 when they operate on regular files or           symbolic links: ...       Among the APIs subsequently listed are write() and writev(2).  And among the effects that should be atomic across threads (and processes) are updates of the       file offset.  However, on Linux before version 3.14, this was not the case: if two processes that share an open file description  (see  open(2))  perform  a       write()  (or  writev(2)) at the same time, then the I/O operations were not atomic with respect updating the file offset, with the result that the blocks of       data output by the two processes might (incorrectly) overlap.  This problem was fixed in Linux 3.14.

嗯,说明3.10的内核真的是BUG,3.14以后的内核解决了,非常OK!看了4.14的内核,问题没有了,这问题早就在3.14社区内核中解决:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,        size_t, count){    struct fd f = fdget_pos(fd);  // 这里会锁file的pos锁    ssize_t ret = -EBADF;    if (f.file) {        loff_t pos = file_pos_read(f.file);        ret = vfs_write(f.file, buf, count, &pos);        if (ret >= 0)            file_pos_write(f.file, pos);        fdput_pos(f);    }    return ret;}

针对该问题的patch说明:

From: Linus Torvalds Date: Mon, 3 Mar 2014 09:36:58 -0800Subject: [PATCH 1/2] vfs: atomic f_pos accesses as per POSIXOur write() system call has always been atomic in the sense that you getthe expected thread-safe contiguous write, but we haven't actuallyguaranteed that concurrent writes are serialized wrt f_pos accesses, sothreads (or processes) that share a file descriptor and use "write()"concurrently would quite likely overwrite each others data.This violates POSIX.1-2008/SUSv4 Section XSI 2.9.7 that says: "2.9.7 Thread Interactions with Regular File Operations  All of the following functions shall be atomic with respect to each  other in the effects specified in POSIX.1-2008 when they operate on  regular files or symbolic links: [...]"and one of the effects is the file position update.This unprotected file position behavior is not new behavior, and nobodyhas ever cared.  Until now.  Yongzhi Pan reported unexpected behavior toMichael Kerrisk that was due to this.This resolves the issue with a f_pos-specific lock that is taken byread/write/lseek on file descriptors that may be shared across threadsor processes.

一波三折的事情貌似结束了,总结一下收获就是,碰到问题直接看文档而不是代码估计可能会更快速解决问题。


这绝对是本文的最后一部分,如果再发生故事,我保证会放弃!因为这个问题本来就是碰到了顺便拿来玩玩的。

  当我把机器重启到Centos 2.6.32内核(我认为低版本内核更容易重现,更容易说明问题)时,依然载入我那个jprobe内核模块,运行我那个parent程序,然而并没有重现问题,相反地,当parent被那个msleep阻塞后,child同样也被阻塞了,看样子是修复bug后的行为啊。

  第一感觉这可能性不大,毕竟3.10内核都有的问题,2.6.32怎么可能避开?!然而事后仔细一想,不对,3.10的问题内核是社区内核,2.6.32的是Centos内核,后者会拉取很多的上游patch来解决一些显然的问题的,对于衍生自Redhat公司的稳定版内核,这并不稀奇。

  最后,我找到了write的实现:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,                 size_t, count){        struct file *file;        ssize_t ret = -EBADF;        int fput_needed;        file = fget_light_pos(fd, &fput_needed);  // 这里是关键        if (file) {                loff_t pos = file_pos_read(file);                ret = vfs_write(file, buf, count, &pos);                file_pos_write(file, pos);                fput_light_pos(file, fput_needed);        }        return ret;}

请注意fget_light_pos是一个新的实现:

struct file *fget_light_pos(unsigned int fd, int *fput_needed){        struct file *file = fget_light(fd, fput_needed);        if (file && (file->f_mode & FMODE_ATOMIC_POS)) {                if (file_count(file) > 1) {                        *fput_needed |= FDPUT_POS_UNLOCK;                        // 如果有超过一个进程/线程在操作同一个file,则先lock它!                        mutex_lock(&file->f_pos_lock);                }        }        return file;}

事情就是在这里起了变化!Centos早就拉取了修复该问题的patch,解决了问题便无法重现问题。

  所以,社区版内核和发行版内核是完全不同的,侧重点不同吧,社区版内核可能更在意内核本身的子系统以及性能因素,而发行版内核则更看重稳定性以及系统调用,毕竟系统就是用来跑应用的,系统调用作为一个接口,一定要稳定无BUG!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/434111.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java 判断对象为控制_Java流程控制

Java流程控制1、Scanner对象①java.util.Scanner是Java5的新特性,可以通过Scanner类来获取用户的输入。②基本语法:1 Scanner snew Scanner(System.in);③通过next()和nextLine()方法接受用户输入,通过hasNext()和hasNextLine()方法来判断用户…

directx最终用户运行时_运维定位服务故障时,前5分钟都在忙啥?

遇到服务器故障,问题出现的原因很少可以一下就想到。我们基本上都会从以下步骤入手,这些也是绝大多数运维工程师在定位故障时前几分钟的主要排查点:一、尽可能搞清楚问题的前因后果不要一下子就扎到服务器前面,你需要先搞明白对这…

IDE--ubuntu下安装 Source insight

2013-06-03 09:05 74人阅读 评论(0) 收藏 举报 习惯了在source insight下编辑阅读源码,在linux下用vi总是用不好 ,还是在ubuntu上用回熟悉的source insight。 在ubuntu中,安装windows程序用wine,然后用wine安装windows软件即可。…

python中什么是数据驱动_利用Python如何实现数据驱动的接口自动化测试

前言 大家在接口测试的过程中,很多时候会用到对CSV的读取操作,本文主要说明Python3对CSV的写入和读取。下面话不多说了,来一起看看详细的介绍吧。 1、需求 某API,GET方法,token,mobile,email三个参数 token为必填项 mo…

密码锁 java接口_从synchronized和lock区别入手聊聊java锁机制

写这篇文章之前,我去百度了一下啥叫锁,百度百科上写道:置于可启闭的器物上,以钥匙或暗码开启。确实我们一般理解的锁就是门锁,密码锁,但是在计算机科学中,锁又是啥,说实话&#xff0…

lua 给userdata设置元表_lua学习之复习汇总篇

第六日笔记1. 基础概念程序块定义在 lua 中任何一个源代码文件或在交互模式中输入的一行代码程序块可以是任意大小的程序块可以是一连串语句或一条命令也可由函数定义构成,一般将函数定义写在文件中,然后用解释器执行这个文件换行在代码中不起任何作用&a…

集群服务负载均衡------LVS

个人的理解,以一种通俗易懂的方式讲述出来,如果有哪些地方说的不正确的话,希望大家留言指出来。笔者会非是常的感谢! Cluster服务器集群,直接理解为一些单一的服务器的集合通过某种方式组合起来,为客户端提…

tomcat jsp导入java_[导入]Tomcat JSP Web 开发中的乱码问题小姐

1. 静态页面的乱码问题文件的编码和浏览器要显示的编码不一致。1) 检查文件原始的编码, 可以用记事本打开, 然后选择另存为来看;2) 给当前页面加入一个指令来建议浏览器用指定的编码来显示文件字符内容.3) 如果系统是英文XP,没装东亚字符集支持, 也会显示乱码.2. JSP 页面的乱码…

四大开源分布式存储_ipfs分布式存储行业面临着四大主要风险,你知道是哪些吗?...

为了响应国家号召、推动分布式存储技术落地、防御行业风险,中国分布式存储产业联盟启动,全国从事IPFS以及分布式存储从业者对行业风险及联盟成立的必要性达成了高度共识,目前有36家以上的IPFS分布式存储行业企业填写了联盟申请表。几位国内知…

pjsua帮助手册(中文)

原文地址 : http://www.pjsip.org/pjsua.htm 介绍 PJSUA是一个开源的命令行SIP用户代理(软电话),用PJSIP协议,PJNATH,和PJMEDIA实现。 它虽然只有很简单的命令行界面,但是功能齐全。 SIP功能: 多…

js date转成 时间字符串_秋招快要开始了,前端笔试中的坑位-JS隐式转换问题

我们在写笔试题的时候,经常碰到涉及隐式转换的题目,例如"1" 2 obj 1 [] ![] [null] false 和 叫做严格运算符,对象类型指向地址相同或原始类型( 数值、字符串、布尔值)值相同;叫做相等运算…

Java中快速处理集合_简洁又快速地处理集合——Java8 Stream(上)

作者:Howie_Y,系原创投稿主页:www.jianshu.com/u/79638e5f0743Java 8 发布至今也已经好几年过去,如今 Java 也已经向 11 迈去,但是 Java 8 作出的改变可以说是革命性的,影响足够深远,学习 Java …

eclipse编译java项目class文件_动态编译 Java 代码以及生成 Jar 文件

导读: 最近在看 Flink 源码的时候发现到一段实用的代码,该代码实现了 java 动态编译以及生成 jar 文件。将其进行改进后可以应用到我们的平台上,实现在平台页面上编写 java 代码语句,提交后由后台进行编译和打成 Jar 包再上传到指…

Dx11DemoBase 基类(三) 实例应用 【已实现】【附带源码】

现在我已经到哪了? 读书时,尤其是技术知识书籍, 我一般会担忧自己是否陷得太深, 细节关注得太多, 而忘了整体的过程; 一直以来对Direct3D 很畏惧, 因为太多函数和细节;现在我必须暂缓下&#x…

修改 decimal 默认值为0.00 sql_被经理邀请去“爬山”,只是因为我写错了一条SQL语句?...

作者:isysc1链接:https://juejin.im/post/5f06a2156fb9a07e5f5180df来源:掘金前戏SQL 写的妙,涨薪呱呱叫!新来的实习生小杨写了一条 SQL 语句SELECT wx_id from user WHERE wx_id 2当小杨迫不及待准备下班回家的时候&…

JS中关于clientWidth、offsetWidth、scrollWidth

网页可见区域宽: document.body.clientWidth;网页可见区域高: document.body.clientHeight;网页可见区域宽: document.body.offsetWidth (包括边线的宽);网页可见区域高: document.body.offsetHeight (包括边线的宽);网页正文全…

shell 执行失败重试_Uipath 机器人总是运行失败怎么办?

要知道为什么RPA机器人容易失败,首先了解下它和常规的应用系统有哪些区别。常规应用系统,就像程序员自己创造了一个世界、一个域,在这个世界里创造它的人就是主宰。出现BUG的风险是相对可控的,顶多是功能用不了。而RPA项目&#x…

c mysql安装教程视频_MySQL安装教程 - Windows安装MySQL教程 - 小白式安装MySQL教程 - 青衫慧博客...

版权声明本文转发自旧站点萧瑟云日志,近期考虑准备将旧站进行关闭(没有精力维护),部分文章将会迁移至本站。文章发表于:2017-10-28 12:32:03前言上次给大家带来了SQL Server的小白式安装教程,这次再次带来一个MySQL的小白式安装教…

PJSIP UA分析(1)--PJSUA主函数

1 intmain(intargc, char*argv[])2 {3 do{4 app_restart PJ_FALSE; //PJ_FALSE是一个宏,一旦用户调用pjsua可执行文件进入该循环,那么默认只执行一次退出5 //如果需要再次循环,那么在下面函数中…

锁定表头和固定列(Fixed table head and columns)

前段时间需要这个功能,但是找了很多都不能完美的实现,不是只能锁定表头,就是浏览器兼容问题什么的,在此就自己做了一个锁定表头和列的js方法,依赖于JQuery。 因为方法很简单,就未封装成插件的形式&#xff…