Linux IO:你的 write() 需要同步吗?

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 本篇目标

本篇目标旨在回答如下问题:

. 如果程序打开了一个普通文件,程序的多个线程使用同一文件句柄,并
  发的对文件进行读写操作,需要程序加锁来进行同步吗?
. 如果程序打开了一个串口字符设备文件(如 /dev/ttyS1),程序的多个线
  程使用同一文件句柄,对设备文件进行写操作,需要程序加锁来进行同
  步吗?
  如果多次对同一设备节点调用 open(),使用不同文件句柄进行并发的写
  呢?
. 如果程序创建了一个 socket ,程序的多个线程对同一 socket 进行并发
  写,需要程序加锁进行同步吗?  
......  

我们无法列举所有的情形,对于不同底层设备,写操作操作接口实现各有不同。以上列举的3种情形,是我们实际应用中,很常见的情形,我们需要了解这些,让程序能够正确工作,减少不必要的锁。

3. 问题分析

/* 对普通文件的读写,由 position 锁保证读写同步 */
sys_read()/sys_write()
	/* 持有文件对象 position 锁保证读写同步 */
	fdget_pos(fd)
		...
		mutex_lock(&file->f_pos_lock)
	
	/* 执行读或写操作 */
	vfs_read() /vfs_write() 
	 
	/* 释放文件对象 position 锁 */
	fdput_pos(f)
		...
		mutex_unlock(&f->f_pos_lock);
/* 
 * 对串口字符设备的写,由每设备对象对应的 tty 对象写锁保证写操作
 * 之间的同步: tty_struct::atomic_write_lock
 */
sys_write()
	...
	vfs_write(f.file, buf, count, &pos)
		...
		/* 锁 atomic_write_lock 保证写同步 */
		tty_write_lock(tty, file->f_flags & O_NDELAY)
			mutex_trylock(&tty->atomic_write_lock)
		n_tty_write(tty, file, tty->write_buf, size)
			tty->ops->write(tty, b, nr) /* 调用具体 tty driver 的写接口 */
		tty_write_unlock(tty)
			mutex_unlock(&tty->atomic_write_lock);
			wake_up_interruptible_poll(&tty->write_wait, POLLOUT);
/*
 * 每 sock 对象的锁保证读、写同步。
 * 读同步在这里没有展示,读者可自行分析。
 */
sys_write()
	...
	sock_write_iter()
		sock_sendmsg(sock, &msg)
			sock_sendmsg_nosec(sock, msg)
				/* 以IPv4协议TCP通信为例 */
				inet_sendmsg()
					tcp_sendmsg()
						/* 每sock对象的锁保证写同步 */
						lock_sock(sk)
						tcp_sendmsg_locked(sk, msg, size);
						release_sock(sk)

初一看,以上这些情形,内核都贴心的为我们加上了锁,所以应该无需担心同一文件句柄的并发写操作。但事实真的如此吗?看一段 write() 的 man 手册原文:

从上图的 man 手册了解到,write() 调用写入的字节数可能少于请求的 count ,这意味 write() 操作并非原子操作。所以,当多个线程并发的使用同一文件句柄进行写操作时,可能发生类似如下的写入情形:

write(fd, "111111", 6); // 线程 1
write(fd, "222222", 6); // 线程2

// 最终写入结果可能是:
111222111222

前面虽然只分析了 write() 操作,但事实上 read() 操作也是类似的,并且 read() 操作也并非原子操作:

4. 问题结论

经过上面的分析,在同一进程的多个线程使用同一文件句柄并发进行读写操作时:

. 使用同一文件句柄,对普通文件的进行读写,应用程序如果不加锁同步,
  Linux内核使用 position 锁保证了读写之间的同步。
. 对串口字符设备的写操作,不管是使用同一文件句柄,还是不同文件句
  柄,写操作间的同步由内核驱动保证,应用程序无需加锁进行同步。
. 程序的不同线程,对同一socket对象的读写,同步由内核sock对象自身
  的锁保证(每sock对象的锁),应用程序无需加锁进行同步。

但同时:

由于读写操作的非原子性,所以对同一个文件句柄的读写操作,要保证一个读写不被打断,则需要加锁进行同步。

5. 番外

上面探讨的是同一进程的多个线程并发对同一文件句柄的并发读写,那如果是:

. 同一进程的多个线程使用不同文件句柄操作同一文件对象会发生什么?
. 不同进程操作同一文件又如何?

对于普通文件,就算不考虑读写操作的非原子性,由于内核普通文件锁是针对 struct file 对象的,所以必须通过文件锁来应对上述两种情形可能引入的问题。
对于设备对象文件,如果内核锁是每设备对象的,那就只需要再考虑读写的原子性保护了。

posted @ 2025-04-08 08:58  JiMoKuangXiangQu  阅读(26)  评论(0)    收藏  举报