在本章中,我们接着讨论文件I/O。
  继续open()系统调用的讨论,我们会解释 原子(atomicity) 的概念–系统调用以单个不间断的步骤执行的行为。这是许多系统调用正确执行的必要步骤。
我们介绍另一个文件相关的系统调用–多用途的 fcntl() ,并且展示它的一个用途:获取和设置打开文件的状态标志(status flags)
  接下来,我们看一下内核中用于表示文件描述符和打开文件的数据结构。理解这些结构之间的关系可以有助于我们理解后续章节中文件I/O的细节。然后说明如何复制文件描述符。
  然后我们会考虑一些继承了read和write功能的一些系统调用。这些系统调用允许我们在文件的特定位置执行I/O操作而不改变文件偏移量。并且在程序中将数据在多个缓冲(buffer)中传递。
  我们会简要介绍 非阻塞(nonblocking) I/O的概念。以及描述一些支持 非常大文件 I/O的扩展函数(系统调用)。
  因为很多系统程序中用到 临时文件(temporary files),所以我们还会介绍一些用于创建临时文件的函数,这些临时文件的名称是随机生成的,并具有唯一性。

5.1 Atomicity and Race Conditions

  在我们讨论系统调用的操作时经常会遇到 原子性(Atomicity) 的概念。所有的系统调用都是以原子的方式执行的。内核确保系统调用中的所有步骤以单个操作完成,不会被其他进程或线程打断。
  原子性对于一些操作的成功完成是很重要的。尤其是,它允许我们可以避免 竞态条件(race conditions or race hazards) 的发生。竞态条件是一种情形:两个进程(或线程)因为获取CPU(s)时序的不同,导致对相同共享资源的操作产生的结果不可预料 (百度百科:竞态条件,从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序)。
接下来,我们看两种会发生竞态条件的文件I/O情形,并且通过对open() flags参数的使用,保证相关文件操作的原子性,从而消除竞态条件。
  当我们讲到章节22.9中的sigsuspend()和章节24.4的fork()时,会重新回顾竞态条件这个主题。

Creating a file exclusively

  在章节4.3.1中,我们看到同时在open()中指定O_EXCL和O_CREAT时,如果想要打开的文件已经存在,就会返回一个错误。这就为进程提供了一种方式:可以保证文件的创建者是该进程。以原子性的方式执行文件存在性的检查和文件的创建。想要知道这种方式的重要性,可以考虑Listing 5-1的代码,我们会测试O_EXCL flag缺省的情况(在代码中,我们会打印出由getpid()系统调用返回的进程ID,用于区分两个不同程序运行的输出结果)。
Listing 5-1: Incorrect code to exclusively open a file

//fileio/bad_exclusive_open.c
fd = open(argv[1], O_WRONLY); /* Open 1: check if file exists */
if (fd != -1) { /* Open succeeded */
	printf("[PID %ld] File \"%s\" already exists\n",(long) getpid(), argv[1]);
	close(fd);
} else {
	if (errno != ENOENT) {  /* Failed for unexpected reason */
		errExit("open");
	} else {
		fd = open(argv[1], O_WRNOLY | O_CREAT, S_IRUSR | S_IWUSR);
		if (fd == -1) 
			errExit("open");
		printf("[PID %ld] Created file \"%s\" exclusively\n", (long) getpid(), argv[1]); 	/*MAY NOT BE TRUE! */
	}
}

  Listing 5-1中的代码除了冗长地使用了两次open()调用外,还存在一个bug。假定当一个进程首先调用了open(),发现文件不存在。但是当要调用第二次open()时,其他的进程已经创建了该文件。正如Figure 5-1所示,如果当进程的时间切片过期,内核调度器(kernel shcduler)决定将控制权给另一个进程时,或者,如果两个进程同时运行在多处理器系统中,就可能发生这种情况。Figure 5-1 描绘了两个进程同时执行Listing 5.1中的代码时可能发生的情况。在这种情况下,进程A在执行第二个open()前,其实不能确定文件是否已经创建。不管该文件是否存在,第二个open()都能顺利执行。虽然,这种情况发生的概率相对较小,但是这代码的不可靠性,导致可能会发生这种情况。事实上,这些结果的产生取决于两个进程的调度顺序,也就是说,这就是一种竞态条件。
第5章 文件IO:更多详情
使用单个open()调用,同时指定O_CREAT和O_EXCL flags可以保证检查和创建的步骤以单个原子的操作执行,从而防止竞态条件的发生。

Appending data to a file

需要用到 原子性 的第二个例子是多个进程将数据追加到同一个文件中(例如,一个全局日志文件)。为此,在每个writer中我们考虑使用如下的代码块:

if (lseek(fd, 0, SEEK_END) == -1) /*将文件偏移量定位到文件最后。表示对于打开的文件,从文件最后开始写入数据*/
	errExit("lseek");
if (write(fd, buf, len) != len)
	fatal("Partial/failed write");

但是,这块代码与之前的例子具有相同的缺陷。如果第一个进程执行在lseek()和write()调用之间被另一个执行这块代码的进程中断,那么两个进程在写入之前都会将文件偏移量设置成同一个位置。当第一个进程被重新调度时,它会覆盖第二个进程刚才写入的数据。这也是一种竞态条件,因为最后的结果取决于两个进程的调度顺序。
为避免这个问题,需要这两个步骤以原子操作完成:1.找到文件末尾下个字节的位置(偏移量),2.执行写入操作。打开文件时指定O_APPEND flag就能确保这两个步骤以一个原子操作完成。

相关文章:

  • 2021-05-26
  • 2022-01-01
  • 2021-07-07
  • 2022-12-23
  • 2021-12-30
  • 2022-01-16
  • 2022-12-23
猜你喜欢
  • 2022-12-23
  • 2021-12-28
  • 2021-12-06
  • 2022-12-23
  • 2021-12-25
相关资源
相似解决方案