首页 > 系统相关 >linux 多线程写同一个文件

linux 多线程写同一个文件

时间:2023-12-11 13:44:39浏览次数:39  
标签:文件 include 同一个 写入 描述符 linux 进程 多线程 读取

来自:

https://blog.popkx.com/linux-multithreaded-programming-in-io-read-write-security-functions-pread-pwrite-and-read-write-what-is-the-difference-and-relat/

 

#include <unistd.h> ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

 

#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <pthread.h> #include <stdlib.h> int fd; #define MAX 255 #define filename "test.bin" void* r_thread(void* p) { int i = 0, j = 0; for(i=0; i<MAX; i++){ usleep(100); lseek(fd, i, SEEK_SET); read(fd, &j, 1); // pread(fd, &j, 1, i); printf("%s: %d\n", __FUNCTION__, j); } return NULL; } void* w_thread(void* p) { int i = 0, j = 0; for(i=0; i<MAX; i++){ usleep(100); lseek(fd, i, SEEK_SET); write(fd, &i, 1); // pwrite(fd, &j, 1, i); printf("%s: %d\n", __FUNCTION__, j); } return NULL; } int main(int argc, char* argv[]) { pthread_t ppid; fd = open(filename, O_RDWR|O_CREAT, 0777); if(argc<2){ printf("\n usage: %s [1(read),2(write),3(both read and write)]\n\n", argv[0]); exit(1); } switch(atoi(argv[1])){ case 1: pthread_create(&ppid, NULL, r_thread, NULL); break; case 2: pthread_create(&ppid, NULL, w_thread, NULL); break; case 3: pthread_create(&ppid, NULL, r_thread, NULL); pthread_create(&ppid, NULL, w_thread, NULL); break; default: printf("\n usage: %s [1(read),2(write),3(both read and write)]\n\n", argv[0]); break; } getchar(); return 0; }

 

来自:

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

与 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 系统默认采用的是建议性锁。

 

来自:

https://www.cnblogs.com/starrysky77/p/9974130.html

1、flock,lockf,fcntl之间区别

  先上结论:flock是文件锁,锁的粒度是整个文件,就是说如果一个进程对一个文件加了LOCK_EX类型的锁,别的进程是不能对这个文件加锁的。

  lockf是对fcntl的封装,这两个东西在内核上的实现是一样的。它们的粒度是字节,不同的进程可以对相同的文件不同字节加LOCK_EX类型的锁。

2、linux文件系统

  在详解锁的实现机制前,我们先来看一下linux文件系统的实现。

 

  相信大家都看过这样一副图。与进程相关的是文件描述符表,文件表和i-node都是系统级别的。当我们在进程中打开一个文件时,文件描述符里就会产生一个文件描述符表项与之对应,同样的系统内也会有文件句柄和相应的i-node,我们需要注意的是多个文件表项(同一个进程或不同进程)可以指向同一个文件句柄。

2、 flock锁的实现机制

  flock在实现上关联到的是文件描述符(上图中文件描述符部分),这就意味着如果我们在进程中复制了一个文件描述符,那么使用flock对这个描述符加的锁也会在新复制出的描述符中继续引用。我们可以写如下代码测试:

复制代码
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <wait.h>
#define PATH "/tmp/lock"
int main()
{
    int fd;
    pid_t pid;
    fd = open(PATH, O_RDWR | O_CREAT | O_TRUNC, 0644);
    flock(fd, LOCK_EX);
    printf("%d: locked!\n", getpid());
    pid = fork();
    if (pid == 0) {
        // fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
        flock(fd, LOCK_EX);
        printf("%d: locked!\n", getpid());
        exit(0);
    }
    wait(NULL);
    unlink(PATH);
    exit(0);
}
复制代码

输出结果:

Sangfor:aCMP/acmp-fefcfe3a1674 ~/test o ./a.out 
118333: locked!
118334: locked!

由结果可见,父进程已经持有互斥锁的情况下,子进程应该对文件加锁失败才符合我们的预期。但是子进程确加锁成功。原因就在于flock的实现是关联文件描述符。

子进程由父进程创建,子进程的文件描述符表和父进程的一模一样,在fork()子进程后,子进程本身就持有该文件的互斥锁。同样的道理,对文件描述符dup(), dup2()都会有这样的问题。怎么解决这个问题呢?

1、重新open这个文件,使用新的文件描述符

fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);

我们将上述注释掉的代码打开,重新编译执行,输出结果如下:

Sangfor:aCMP/acmp-fefcfe3a1674 ~/test o ./a.out 
172199: locked!

这次子进程没有能上锁,重新open这个文件会创建一个新的文件描述符与父进程进程而来的描述符是不相关的,所以就符合我们预期的效果。

另外要注意:除非文件描述符被标记了close-on-exec标记,flock锁和lockf锁都可以穿越exec,在当前进程变成另一个执行镜像之后仍然保留。

3、lockf的实现机制

  lockf的实现是关联到内核i-node的(上图内核部分),每次加锁都会在i-node节点上挂一个flock的结构:

复制代码
  struct flock 
  {
      short l_type;/*F_RDLCK, F_WRLCK, or F_UNLCK*/
      off_t l_start;/*相对于l_whence的偏移值,字节为单位*/
      short l_whence;/*从哪里开始:SEEK_SET, SEEK_CUR, or SEEK_END*/
      off_t l_len;/*长度, 字节为单位; 0 意味着缩到文件结尾*/
      pid_t l_pid;/*returned with F_GETLK*/
  };
复制代码

  对LOCK_EX类型的锁来说,内核中最多只有一份这样的数据,所以即使文件描述符是从父进程进程过来或dup()产生的,对同一个节点加锁都会失败。我们写如下代码测试一下:

复制代码
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <wait.h>
#define PATH "/tmp/lock"

int main()
{
    int fd;
    pid_t pid;
    fd = open(PATH, O_RDWR | O_CREAT | O_TRUNC, 0644);
    lockf(fd, F_LOCK, 0);
    printf("%d: locked!\n", getpid());

    pid = fork();
    if (pid == 0) {
        lockf(fd, F_LOCK, 0);
        printf("%d: locked!\n", getpid());
        exit(0);
    }
    wait(NULL);
    unlink(PATH);
    exit(0);
}
复制代码

执行结果:

Sangfor:aCMP/acmp-fefcfe3a1674 ~/test o ./a1.out 
47087: locked!

这次我们没有对文件重新open,子进程就一直等在那里。完全符合我们的预期。这也印证了lockf的实现是内核中i-node相关的。

 

此外有一篇文章介绍建议性锁和强制性锁,这篇文章是以flock为例说明的,flock和lockf的加锁规则是一致的。需要了解加锁规则请参看:

强制性锁和建议锁

 

 

 

 

 

 

参考:

 

标签:文件,include,同一个,写入,描述符,linux,进程,多线程,读取
From: https://www.cnblogs.com/rebrobot/p/17894192.html

相关文章

  • Linux学习36- python3.9出现ImportError: urllib3 v2.0 only supports OpenSSL 1.1.1+
    遇到问题python3.9上安装requests库,requests包引入了urllib3,而新版本v2.x的urllib3需要OpenSSL1.1.1+以上版本所以就出现了报错File"/root/python39/lib/python3.9/site-packages/_pytest/assertion/rewrite.py",line186,inexec_moduleexec(co,module.__dict__......
  • Linux下删除当前目录下的所有文件夹及文件保留最新的几个文件夹及文件
    一、查找目录或文件1.1、查找指定文件夹和文件具体的示例:查找当前目录下指定文件夹和文件find./-maxdepth1-name"jobs"-o-name"config.xml"命令说明-maxdepth目录深度,1表示只搜索一级目录-name后面跟文件夹或文件,多个文件夹或文件,用-o组合连接jobs、config.xml指定的文......
  • Linux学习35- python3.9出现ModuleNotFoundError: No module named '_ctypes'的解决
    遇到问题pip安装第三方库的时候报错ModuleNotFoundError:Nomodulenamed'_ctypes'File"/usr/local/python3/lib/python3.9/ctypes/__init__.py",line7,in<module>from_ctypesimportUnion,Structure,ArrayModuleNotFoundError:Nomodulen......
  • [linux] [Centos8] 一台虚拟机的安装配置全过程
    今年7月的时候刚学linux,写过几篇配置,结果学得越多才发现已经过时了,这两天重装的时候被自己的文章搞晕了......
  • Kylin Linux Advanced Server V10 上安装 Nacos详细步骤
    要在KylinLinuxAdvancedServerV10上安装Nacos,可以按照以下进行操作:1.安装JavaJDK:首先确保已在KylinLinuxAdvancedServerV10上安装了JavaJDK。你可以按照前面提到的步骤进行JDK的安装和配置。2.下载Nacos:前往Nacos的官方GitHub仓库(https://github.com/ali......
  • Linux操作系统 文件查找、打包压缩及解压读书笔记
    当涉及Linux文件查找、打包压缩和解压时,确实有很多详细的内容。以下是更详细的解释和示例:1.文件查找在Linux中,find命令用于在文件系统中搜索文件和目录。下面是一些常见用法:基本用法:在整个文件系统中查找文件或目录:bashCopycodefind/-namefilename在当前目录及......
  • Linux课程随堂博文三
    一、基本权限UGO1、r、w、x对文件的影响要在file01.txt文件写入“date”,查看文件权限为644,普通用户alice只有读取权限。在root用户下,使用chmod命令给other身份增加执行权限“x”与写入权限“w”。2、r、w、x对目录的影响创建dir10目录,在该目录下创建file01文件,使用chmod命令......
  • linux 开机自动启动python程序
    可以使用systemd服务来开机自动启用程序。假设要开机自动启动的python程序是:/opt/app.py可以创建一个systemd服务cd/etc/systemd/systemvimstart-python.service内容如下:[Unit]Description=PythonStartupServiceAfter=network.target[Service]ExecStart=/usr/b......
  • Linux问题总结(1)
    export和declare底层实现原理在Bash中,export和declare都用于处理变量,但它们在底层的实现和使用上有一些区别。export命令:export主要用于设置环境变量,使得变量在当前进程及其子进程中可见。其底层实现涉及到将变量添加到环境变量列表中。环境变量是一个由键值对组成的......
  • 7、Linux学习文件查找、打包压缩及解压
    一、文件查找1.1which命令(搜索某个系统命令的位置)which命令的作用是,在PATH变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。也就是说,使用which命令,就可以看到某个系统命令是否存在,以及执行的到底是哪一个位置的命令。1.2find命令find是在硬盘上遍历查找,因......