前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >linux 系统调用 write 的原子性

linux 系统调用 write 的原子性

作者头像
用户3147702
发布2022-06-27 12:32:49
1.7K0
发布2022-06-27 12:32:49
举报
文章被收录于专栏:小脑斧科技博客

1. 问题描述

开始阅读 nginx 源码的时候就一直伴随着一个问题,那就是多进程的 nginx 模型是怎么保证多个进程同时写入一个文件不发生数据交错呢? 猜想中,主要有以下几种解决方案: 1. 最传统的,正在写文件的进程加锁,其他进程等待,但是这样的情况是绝对不允许的,效率太过低下 2. 写 log 前测试锁状态,如果已经锁定,则写入进程自己的缓冲区中,等待下次调用时同步缓冲区,这样做的好处是无需阻塞,提高了效率,但是就无法做到 log 的实时了,这样做工程中也是绝对无法接受的,一旦发生问题,将无法保证 log 是否已经被写入,因此很难定位 3. 一个进程专门负责写 log,其他进程通过域套接字或者管道将 log 内容发送给他,他持续阻塞在 epoll_wait 上,直到收到信息,立即写入,但是众所周知,nginx 是调用同一个函数启动所有进程的,并没有专门调用函数启动所谓的 log 进程,除了 master 和 worker,nginx 也确实没有 log 进程存在 4. 那么就是进程启动后,全部去竞争某个锁,竞争到该锁的 worker 执行 log worker 的代码,其余的 worker 继续运行相应程序,这个方案看上去是一个不错的方案,如果是单 worker 的话,那么就无需去使用该锁即可

利用周末的空闲时间,终于进行了一番探究,究竟 nginx 使用的是上述方案中的哪一个呢?还是另有妙方?

2. nginx 具体实现

通过阅读源码,我们发现 nginx 只有一把互斥锁,即用来避免惊群现象的 ngx_accept_mutex 锁,其余地方完全没有用到锁机制,这么做原因很简单,在工程化的代码中,盲目使用锁会造成性能的下降,这是不可以接受的。 进一步阅读源码,发现,ngx_log_error 这个函数中调用了 ngx_log_error_core 函数,这个函数正是用来打印错误日志的:

代码语言:javascript
复制
// void ngx_cdecl ngx_log_error(ngx_uint_t level, ngx_log_t *log, ngx_err_t err,
//     const char *fmt, ...)
// 打印错误日志 {{{
void ngx_cdecl
ngx_log_error(ngx_uint_t level, ngx_log_t *log, ngx_err_t err,
    const char *fmt, ...)
{
    va_list  args;

    if (log->log_level >= level) {
        va_start(args, fmt);
        ngx_log_error_core(level, log, err, fmt, args);
        va_end(args);
    }
} // }}}

接下来,他调用了 ngx_log_error_core 这个函数,在这个函数中,将日志信息进行格式化后调用:

代码语言:javascript
复制
(void) ngx_write_fd(log->file->fd, errstr, p - errstr);

写入日志,而 ngx_write_fd 这个调用却是:

代码语言:javascript
复制
static ngx_inline ssize_t
ngx_write_fd(ngx_fd_t fd, void *buf, size_t n)
{
    return write(fd, buf, n);
}

那么,这么调用真的不用担心多进程写 log 时数据交错的发生吗?

3. SUS 标准

在 APUE (《UNIX 环境高级编程》) 中有这么一段话: 如果多个进程都需要将数据添加到某一文件,那么为了保证定位和写数据这两步是一个原子操作,需要在打开文件时设置O_APPEND标志。

那这么说,一但开启 O_APPEND 标志,write 就是一个原子操作了吗? Single UNIX Specification 标准对此进行了详细的说明,内核在调用 write 前会对文件进行加锁,在调用 write 后会对文件进行解锁,这样保证了文件写入的原子性,也就无需担心数据交错的发生了。

那么对于不同类型的文件与不同的系统实现 write 究竟是怎么处理的呢?

3.1. 普通文件

有三种情况可能导致文件写入失败: 1. 磁盘已满 2. 写入文件大小超出系统限制 3. 内核高速缓存区已满

遇到这三种情况怎么处理呢? 如果是使用 O_NONBLOCK 标识打开文件的话,write 会立即返回,返回值小于写入字符数这个参数,虽然写入了不完整数据,但是内核保证其写入过程的原子性,否则内核会让调用进程睡眠,直到文件重新可写,这样内核保证了写入数据的完整性,但是不保证写入的原子性。 也就是说,如果在打开文件时设置了 O_NONBLOCK 标识(或打开文件后用 fctnl 函数设置),则虽然可能写入部分数据,但是写入过程是原子性的。 linux 系统默认使用 O_NONBLOCK 标识打开文件,而 bsd 等 unix 系统则恰恰相反。

3.2. 管道

SUS 标准对管道写入有着明确的说明,只要一次性写入数据小于管道缓冲区长度(PIPE_BUF),那么不论 O_NONBLOCK 标识是否开启,管道写入都是原子性的,多个进程同时写入同一管道是一定不会出现数据交错的,否则,依然可能出现数据交错。

3.3. socket

linux 2.6.14 内核对 tcp socket 写操作进行了说明,他并不是原子的。 也许操作系统设计者认为,socket 是有可能永久阻塞的,所以如果保证这样的 IO 具备原子性是十分荒唐的一件事吧。 因此,对于 UNIX 日志系统服务器的操作,必须每个线程都单独进行一次 connect,保证每个线程使用不同的 fd 进行写入,这样才能防止数据交错的发生。 当然了,对于 udp socket 则无需担心这一问题。

4. 原子性的可靠性

那么问题来了,nginx 直接调用 write,这样靠谱吗? 经过上面的介绍,对于写入普通文件的情况,只要文件是使用 O_NONBLOCK 标识打开的,那么就可以保证其写入的原子性,也就是说这样写入是可以接受的。 但是这并不意味着这样做是靠谱的,这样做依然可能无法成功写入全部数据。 然而,nginx 并没有对返回结果进行判断,他并不关心是否写入成功,这显然是不严谨的,但是作为一个工程化项目,这是不得不进行的妥协。

那么,你也许会问,write 保证原子性难道不是靠加锁实现的吗?为什么我不可以在我的进程中加锁实现更加可靠的 write 呢? 虽然上文已经介绍,这里还是单独强调一下。 在用户进程中使用互斥锁加锁,内核首先需要从用户态陷入内核态,调用系统调用,操作堆栈,然后进行文件操作,然后清理堆栈,再从内核态回到用户态,这个过程是很慢的,而对于用户实现的互斥锁,在这个过程中,其他进程是无法进行文件操作的,无论是缓存到进程所使用的内存中,还是阻塞还是丢弃都不是很好的解决方法。 而对于操作系统来说,内核对文件加锁是在系统调用内实现的,也就是已经陷入内核态实现,这个过程只需几个汇编指令即可,也无需对堆栈进行操作:

代码语言:javascript
复制
mutex_lock:
    TSL REGISTER, MUTEX            '将互斥量复制到寄存器并将内存中互斥量置为 1
    CMP REGISTER, #0            '测试寄存器内容是否为 0
    JZE ok                        '未锁定状态,执行相应操作
    CALL thread_yeld            '锁定状态,调度另一线程
    JMP mutex_lock                '稍后重新检测锁是否可用
ok: CALL write_opt                '执行具体操作

...

mutex_unlock:
    MOVE MUTEX, #0                '将内存中互斥量置为 0
    RET                            '返回调用者
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 问题描述
  • 2. nginx 具体实现
  • 3. SUS 标准
    • 3.1. 普通文件
      • 3.2. 管道
        • 3.3. socket
        • 4. 原子性的可靠性
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档