MeiK / Linux 中多个进程操作同一个文件时会发生什么

转载自:https://meik2333.com/posts/linux-many-proc-write-file/

原文作者:MeiK

 

与 Windows 不同, Linux 允许一个文件在写入的时候被读取(或者在被读取的时候写入),本文就来探索一下多个进程同时读写同一个文件会产生的效果。

Read + Read

多个进程同时读取同一个文件不会出现问题的,放心去干吧。

Read + Write

本文的重点研究对象。Linux 通过文件描述符表维护了打开的文件描述符信息,而文件描述符表中的每一项都指向一个内核维护的文件表,文件表指向打开的文件的 vnode(Unix) 和 inode。同时,文件表保存了进程对文件读写的偏移量等信息。

我们通过两个简单的 Go 语言程序来测试一下在读文件的同时修改文件会发生什么:

testwrite.go

func writeFile(filename string, data string) {
	f, _ := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
	defer f.Close()
	body := []byte(data)
	_, _ = f.Write(body)
}

func main() {
	// 首先向文件中写入 “Hello World!”
	writeFile("test.txt", "Hello World!")
	time.Sleep(7 * time.Second)
	// 七秒后,修改文件内容,写入 “Author MeiK!”
	writeFile("test.txt", "Author MeiK!")
}

testread.go

func readFile(filename string) {
	f, _ := os.OpenFile(filename, unix.O_RDONLY, 0644)
	defer f.Close()

	body := make([]byte, 1)
	n := 1
	for n != 0 {
		time.Sleep(time.Second)
		var err error
		n, err = f.Read(body)
		if err == io.EOF {
			break
		}
		s, _ := f.Seek(0, os.SEEK_CUR)
		fmt.Printf("%c %d\n", body, s)
	}
}

func main() {
	readFile("test.txt")
}

同时执行两个程序:

./testwrite & ./testread

输出:

[H] 1
[e] 2
[l] 3
[l] 4
[o] 5
[ ] 6
[ ] 7
[M] 8
[e] 9
[i] 10
[K] 11
[!] 12

这个程序打印了读取到的内容以及读取到每一步的文件偏移量。我们首先写入 Hello World!,开始每秒读取一个字符,并且在 7 秒后重新将 Author MeiK! 写入文件。我们最终读取到了什么呢?既不是 Hello World!,也不是 Author MeiK!,而是 Hello MeiK!。我们每个字符串读取到了一半!

从每一步的文件偏移量来看,读取的程序只是按部就班的一个字符一个字符的读取文件,对文件内容的变化毫无感知,当读取到文件结尾的 EOF 时结束读取。

那么我们要如何保证读取与写入的一致性呢? Linux 提供了 fcntl 系统调用,可以锁定文件

我们对刚刚的文件稍作修改,使用 fcntl 进行加锁:

testwrite.go

func writeFile(filename string, data string) {
	fmt.Println("write start")
	f, _ := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
	defer f.Close()

	flockT := unix.Flock_t{
		Type:   unix.F_WRLCK,
		Whence: io.SeekStart,
		Start:  0,
		Len:    0,
	}
	_ = unix.FcntlFlock(f.Fd(), unix.F_SETLKW, &flockT)

	body := []byte(data)
	_, _ = f.Write(body)
	fmt.Println("write end")
}

func main() {
	// 首先向文件中写入 “Hello World!”
	writeFile("test.txt", "Hello World!")
	time.Sleep(7 * time.Second)
	// 七秒后,修改文件内容,写入 “Author MeiK!”
	writeFile("test.txt", "Author MeiK!")
}

testread.go

func readFile(filename string) {
	f, _ := os.OpenFile(filename, unix.O_RDONLY, 0644)
	defer f.Close()

	flockT := unix.Flock_t{
		Type:   unix.F_RDLCK,
		Whence: io.SeekStart,
		Start:  0,
		Len:    0,
	}
	_ = unix.FcntlFlock(f.Fd(), unix.F_SETLKW, &flockT)

	body := make([]byte, 1)
	n := 1
	for n != 0 {
		time.Sleep(time.Second)
		var err error
		n, err = f.Read(body)
		if err == io.EOF {
			break
		}
		s, _ := f.Seek(0, os.SEEK_CUR)
		fmt.Printf("%c %d\n", body, s)
	}
}

func main() {
	readFile("test.txt")
}

额外添加 write start 和 write end 来标识当前进度,执行结果如下:

write start
write end
[H] 1
[e] 2
[l] 3
[l] 4
[o] 5
[ ] 6
write start
[W] 7
[o] 8
[r] 9
[l] 10
[d] 11
[!] 12
write end

可以看到,第一次写入文件时,进程很快的完成了写入;而当第二次写入时,由于此时 read 进程对文件加锁了,导致写入进程阻塞,直到读取结束后, write 进程才把内容写入了文件。因此 read 进程读取到的就是第一次写入的内容 Hello World!。完美的解决了我们的问题,可喜可贺。

不过,还有两点需要注意:

  1. 文件锁是与进程相关的,一个进程中的多个线程/协程对同一个文件进行的锁操作会互相覆盖掉,从而无效。
  2. fcntl 创建的锁是建议性锁,只有写入的进程和读取的进程都遵循建议才有效;对应的有强制性锁,会在每次文件操作时进行判断,但性能较差,因此 Linux/Unix 系统默认采用的是建议性锁。

关于不同类型锁之间的交互,可以参照此表:

当前状态加读锁加写锁
无锁允许允许
一个或多个读锁允许拒绝
一个写锁拒绝拒绝

Write + Write

两个进程同时写入可以和 Write + Read 一样靠加锁来解决同步问题,不过还有其他的解决方案:假如我们现在有多个进程在将日志写入同一个日志文件,那么我们可以使用 O_APPEND 标志来打开文件,这样在每次写入时都会 lseek 到文件末尾进行写入,这是一个原子操作,因此不会产生同步问题。

结论

如果一个文件在读取的同时被修改(而没有添加任何锁机制的话),那么将可能会读到错误的数据,很多时候这比读不到数据或读到旧版本的数据的后果更加可怕……因此,如果有准确性要求较高的文件读取的情景的话,最好还是用强制性锁对文件进行保护。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
设计一个菜单驱动的车辆管理程序,管理车辆信息与司机信息。 功能要求: 1)程序包括输入输出模块、车辆信息管理模块、司机信息管理模块、文件操作模块; 2)输入输出模块主要功能是人机交互,包括程序界面显示,用户输入响应,结果输出等; 3) 车辆信息管理以菜单方式工作,从输入输出模块读取用户命令并进行相应的操作,包括车辆信息和司机信息的录入、浏览、删除、修改、查询、排序功能; a车辆信息包括:车辆型号、厂商、车型、座位数、排量、车身颜色、车辆价格; b司机信息包括:姓名、性别、手机号码、出生日期、住址、初次领证时间、执照号码、准驾车型、有效期起止日期; 录入:添加一条新的记录项,添加的时候需要考虑信息的合理性,比如司机手机号码必须是11位数字,时间要按照特定格式输入 等,若不满足要求给出提示重新录入; d)浏览:能够显示所有车辆信息、司机信息;e)查询:能够按照输入的车辆某个信息查询车辆信息,根据司机姓名查询司机信息; n修改:能够修改车辆、司机的某个或多个信息; g)删除:能够删除车辆、司机的某条信息; b)排序:能够按照车辆价格、座位、排量对车辆信息进行排序,能够按照姓名对司机信息进行排序 4)文件操作模块获取管理模块的数据或命令,进行存储文件的读,最后将结果返回给管理模块;
02-26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值