据了解您的问题,可以分为以下几部分。
我试图理解的内部细节是,对于我在应用程序中创建的 StreamWriter 类的每个新实例,是否重复上述步骤
如果每次都是同一个进程要求将某些内容写入完全相同的文件,那么操作系统(OS)或进程是否可以优化或缓存一些东西
您的特殊赏金要求“我还想了解当文件读/写请求来自同一进程或不同进程时,操作系统是否应用了一些额外的智能?或者操作系统是否仍然不知道请求读/写操作的进程”。
#回答第一个问题
免责声明:以下内容仅与您编写的实际代码相关(原样)。如果稍微改变一下,很多实现细节就变得无关紧要了。
实际上,每次创建StreamWriter
. 然而,事情确实会发生。
让我们通过.Net Source创建StreamWriter
您拥有的方式。
创建 StreamWrtier
using (StreamWriter streamWriter = new StreamWriter(LogFileDirectory + "log.txt",true))
调用链如下
public StreamWriter(String path, bool append)
StreamWriter
使用默认编码和缓冲区大小为指定文件初始化类的新实例。如果文件存在,它可以被覆盖或附加到。如果文件不存在,则此构造函数创建一个新文件。
public StreamWriter(String path, bool append, Encoding encoding, int bufferSize)
internal StreamWriter(String path, bool append, Encoding encoding, int bufferSize, bool checkHost)
private static Stream CreateFile(String path, bool append, bool checkHost)
它专门用FileMode.Append
标志调用以下内容。
如果文件存在,则打开文件并查找文件末尾,或创建一个新文件。这需要 FileIOPermissionAccess.Append 权限。FileMode.Append 只能与 FileAccess.Write 结合使用。试图在文件结尾之前寻找一个位置会引发 IOException 异常,并且任何读取尝试都会失败并引发 NotSupportedException 异常。
internal FileStream(String path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, String msgPath, bool bFromProxy, bool useLongPath, bool checkHost)
如您所见,对于在家玩的任何人,我们所做的就是创建一个文件流。从这里我们调用Marshal
一些安全属性:
private void Init(String path, FileMode mode, FileAccess access, int rights, bool useRights, FileShare share, int bufferSize, FileOptions options, Win32Native.SECURITY_ATTRIBUTES secAttrs, String msgPath, bool bFromProxy, bool useLongPath, bool checkHost)
在这一点上发生了很多事情。检查权限;检查文件类型是什么;检查手柄。然而,故事的关键在于:
internal static SafeFileHandle SafeCreateFile(String lpFileName, int dwDesiredAccess, System.IO.FileShare dwShareMode, SECURITY_ATTRIBUTES securityAttrs, System.IO.FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile)
解决在一个DllImport
[DllImport(KERNEL32, SetLastError=true, CharSet=CharSet.Auto, BestFitMapping=false)]
[ResourceExposure(ResourceScope.Machine)]
private static extern SafeFileHandle CreateFile(String lpFileName, int dwDesiredAccess, System.IO.FileShare dwShareMode, SECURITY_ATTRIBUTES securityAttrs, System.IO.FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
这是我们.Net故事的结尾,以我们心爱的KERNEL32结尾 CreateFile Function
创建或打开文件或 I/O 设备。最常用的 I/O 设备如下:文件、文件流、目录、物理磁盘、卷、控制台缓冲区、磁带驱动器、通信资源、邮槽和管道。该函数返回一个句柄,根据文件或设备以及指定的标志和属性,该句柄可用于访问文件或设备以进行各种类型的 I/O。
如果你曾经使用过,CreateFile
你会在这里知道很多关于标志、缓存和缓冲的信息,坦率地说,还有很多与File System
. 那是因为这是最古老的用户模式API 调用之一,它可以做各种各样的事情。但是,如果您遵循.Net源代码(在这种情况下),它实际上并没有充分利用其扩展特性。
唯一的主要例外是:
要写入文件末尾,请将 OVERLAPPED 结构的 Offset 和 OffsetHigh 成员都指定为0xFFFFFFFF
. 这在功能上等同于之前调用 CreateFile 函数以使用FILE_APPEND_DATA
access 打开 hFile。
FILE_FLAG_OVERLAPPED
指示异步 IO 的标志(在这种情况下您没有设置)
正在为异步 I/O 打开或创建文件或设备。
在此句柄上完成后续 I/O 操作时,OVERLAPPED
结构中指定的事件将设置为信号状态。
如果指定了此标志,则该文件可用于同时读取和写入操作。
如果未指定此标志,则 I/O 操作将被序列化,即使对 read 和 write 函数的调用指定了 OVERLAPPED 结构。
同步和异步 I/O 句柄
如果打开文件或设备以进行同步 I/O(即
FILE_FLAG_OVERLAPPED
未指定),则对函数的后续调用WriteFile
可能会阻塞调用线程的执行,直到发生以下事件之一:
- I/O 操作完成(在本例中为数据写入)。
- 发生 I/O 错误。(例如,管道从另一端封闭。)
- 调用本身出错(例如,一个或多个参数无效)。
- 进程中的另一个线程
CancelSynchronousIo
使用阻塞线程的线程句柄调用该函数,这会终止该线程的 I/O,使 I/O 操作失败。
- 被阻塞的线程被系统终止;例如,进程本身被终止,或者另一个线程
TerminateThread
使用阻塞线程的句柄调用该函数。(这通常被认为是最后的手段,而不是好的应用程序设计。)
同步和异步 I/O
在某些情况下,这种延迟对于应用程序的设计和用途来说可能是不可接受的,因此应用程序设计人员应考虑使用异步 I/O 和适当的线程同步对象,例如I/O 完成端口。有关线程同步的更多信息,请参阅关于同步。进程通过在参数中CreateFile
指定
FILE_FLAG_OVERLAPPED
标志在其调用中为异步 I/O 打开一个文件。dwFlagsAndAttributes
如果FILE_FLAG_OVERLAPPED
未指定,则打开文件以进行同步 I/O。当为异步 I/O 打开文件时,指向 OVERLAPPED 结构的指针被传递到对 and 的调用
ReadFile
中WriteFile
。ReadFile
执行同步 I/O 时,调用和时不需要此结构WriteFile
。
CreateFile
提供创建同步或异步的文件或设备句柄。同步句柄的行为使得使用该句柄的 I/O 函数调用在完成之前被阻塞,而异步文件句柄使系统可以立即从 I/O 函数调用返回,无论它们是否完成了 I/O 操作或不是。如前所述,这种同步与异步行为是通过
FILE_FLAG_OVERLAPPED
在dwFlagsAndAttributes
参数中指定来确定的。使用异步 I/O 时有几个复杂性和潜在的陷阱;有关详细信息,请参阅同步和异步 I/O,
I/O 完成端口
I/O 完成端口为在多处理器系统上处理多个异步 I/O 请求提供了一个高效的线程模型。当一个进程创建一个 I/O 完成端口时,系统会为请求创建一个关联的队列对象,这些请求的唯一目的是为这些请求提供服务。处理许多并发异步 I/O 请求的进程可以通过将 I/O 完成端口与预先分配的线程池结合使用,而不是通过在接收 I/O 请求时创建线程来更快、更有效地完成此操作。
I/O 完成端口如何工作
该CreateIoCompletionPort
函数创建一个 I/O 完成端口并将一个或多个文件句柄与该端口相关联。当对其中一个文件句柄的异步 I/O 操作完成时,I/O 完成数据包将按先进先出 (FIFO) 顺序排队到关联的 I/O 完成端口。这种机制的一个强大用途是将多个文件句柄的同步点组合到一个对象中,尽管还有其他有用的应用程序。请注意,虽然数据包按 FIFO 顺序排队,但它们可能会以不同的顺序出队。
注意:FileStreamuseAsync true
可以通过在其中一个重载中设置来使用完成端口,FileStream
但是您没有
public FileStream(String path, FileMode mode, FileAccess access, FileShare share, int bufferSize, bool useAsync)
实际写
您已经选择WriteLine()
了实际的TextWriter
方法,但是在我们开始之前,让我们先对FileStream
. 使对共享日志文件的原子附加写入工作
- 内部
FileStream
缓冲区需要足够大以容纳单次写入的所有数据
- 数据必须从位置 0 写入缓冲区,以使其适合
StreamWriter
通过包装写入的文本FileStream
必须在每次写入之前完全刷新
在这些要求中,第一个 (1) 是最不愉快的工作。创建a 时缓冲区大小是固定的FileStream
(上面的 4096 参数),因此原子写入更大事件的唯一方法是关闭并重新打开具有更大缓冲区的文件。
在写入之间刷新和StreamWriter
巧妙地处理要求(2)和(3)。FileStream
public virtual void WriteLine(String value)
public virtual void Write(char\[\] buffer, int index, int count)
这个小宝石令人惊讶:
for (int i = 0; i < count; i++) Write(buffer[index + i]);
public virtual void Write(char value)
当缓冲区已满时,它会调用一系列刷新,这有点难以理解,但我会尝试简单地
if (charPos == charLen) Flush(false, false);
默认情况下,其中charLen = DefaultBufferSize
哪个被传递到您创建StreamWriter
并定义如下的构造函数之一:
internal const int DefaultBufferSize = 1024; // char[]
private void Flush(bool flushStream, bool flushEncoder)
从这里开始,最重要的两件事是:
if (count > 0)
stream.Write(byteBuffer, 0, count);
// By definition, calling Flush should flush the stream, but this is
// only necessary if we passed in true for flushStream. The Web
// Services guys have some perf tests where flushing needlessly hurts.
if (flushStream)
stream.Flush();
注意:你必须喜欢 MS 源代码注释,那些孩子完全暴动
无论如何,第二个Flush()
(如果您遵循它)将在以下结束。记住我们StreamWriter
是由一个FileStream
类支持的,所以我们再次使用FileStream
类Write
方法
public override void Write(byte\[\] array, int offset, int count)
private unsafe void WriteCore(byte\[\] buffer, int offset, int count)
private unsafe int WriteFileNative(SafeFileHandle handle, byte\[\] bytes, int offset, int count, NativeOverlapped* overlapped, out int hr)
解决另一个DllImport
[DllImport(KERNEL32, SetLastError=true)]
[ResourceExposure(ResourceScope.None)]
internal static unsafe extern int WriteFile(SafeFileHandle handle, byte* bytes, int numBytesToWrite, out int numBytesWritten, IntPtr mustBeZero);
然后在你知道之前,我们又回到了KERNEL32,再次调用WriteFile Function
将数据写入指定的文件或输入/输出 (I/O) 设备。此函数设计用于同步和异步操作。对于专为异步操作设计的类似功能
再一次,有很多选项可以处理各种情况。再一次,.Net
(在这种情况下)并不倾向于使用它。
从这里我们的故事切换到Windows 文件缓存,我们在.Net中几乎无法控制,但是您可以在Raw Api Calls中使用很多选项。
默认情况下,Windows 缓存从磁盘读取并写入磁盘的文件数据。这意味着读取操作从系统内存中称为系统文件缓存的区域读取文件数据,而不是从物理磁盘读取文件数据。相应地,写操作将文件数据写入系统文件缓存而不是磁盘,这种缓存称为回写缓存。缓存是按文件对象管理的。
所以忽略实际的内核模式,我们在用户模式中实际做了什么?不多......当创建一个Stream
.Net已经完成了一系列检查和平衡来调用一个简单的CreateFile
Win32 Api Call时,它又拥有一个句柄。当然有一堆IL被调用,但在它的基本级别它只是使用Win32 API创建一个文件并持有一个句柄(沸腾了一些.Net安全和权限检查等......)
然后会发生什么?好吧,我们处理一些编码,将一些字节写入内存,然后以预定的缓冲区大小,我们使用Win32 Api Call将其写入/刷新到磁盘。FileWrite
操作系统是否必须做任何其他简单的用户模式文件创建和写入不需要做的事情?其实真的不是...
唯一的警告再次开始,确保.Net会唱歌跳舞,如果您真的想从用户模式对文件系统进行原子访问,那么请考虑自己调用这些函数和/或使用IO 完成端口。通过这种方式,您可以获得异步工作和/或躲避.Net的优势,并且能够访问一堆扩展参数(尽管在大多数情况下,它不会在单个应用程序中使其更快,因为API默认参数已经针对通用情况进行了优化比如写)。
如果你真的有Processor Instruction OCD,那么很容易看到它的价值,同时保持FileStream并继续以附加模式写入它(采取预防措施),或者如果你处于Win32 Api级别,创建文件并持有持续写入的句柄并使用异步 IO 工具。
但这是关键..作为用户模式程序员(在大多数情况下) ,这就是用户模式所能做的一切。
#回答第二个问题
如前所述,除此之外,您无能为力;滚动您自己的API 调用;安装快速硬盘(在某些情况下可能使用自己的驱动程序)。但不管怎样,操作系统已经无论如何都缓存和优化了。如果您想对此进行调整,您需要再次访问Windows API和/或使用更高级的异步 IO 功能
最后,不同的服务器操作系统版本在内存和缓存方面具有优势,尽管这一切都有很好的记录
#第三题的答案
在多线程应用程序(内部缓冲区除外)中写入打开文件的进程没有优先处理.Net
,而不是我知道的多个进程写入同一个文件(但是有更多经验的人可能会详细说明)。在用户模式下,每个人都可以通过相同的过滤器驱动程序和缓存管理器使用相同的 API 。您可以在此处阅读有关缓存管理器和操作系统改进的更多信息。
来自 MSDN
缓存在缓存管理器的指导下进行,该管理器在 Windows 运行时连续运行。系统文件缓存中的文件数据以操作系统确定的时间间隔写入磁盘,并释放该文件数据先前使用的内存——这称为刷新缓存。延迟将数据写入文件并将其保存在缓存中直到缓存刷新的策略称为延迟写入,它由缓存管理器以确定的时间间隔触发。刷新文件数据块的时间部分取决于它已存储在缓存中的时间量以及自上次在读取操作中访问数据以来的时间量。这可确保经常读取的文件数据在系统文件缓存中保持可访问的最长时间。
文件数据缓存提供的 I/O 性能改进量取决于正在读取或写入的文件数据块的大小。当读取和写入大块文件数据时,更有可能需要磁盘读取和写入来完成 I/O 操作。随着越来越多的此类 I/O 操作发生,I/O 性能将越来越受到损害。
#补充
以上所有内容现在纯粹是学术性的。我们可以深入研究Windows API的源代码;我们可以一直跟踪到IO Completion 端口 and Kernel Mode
;我们可以剖析驱动程序,但您的问题的答案不会变得更加清晰......除了我们已经知道的。
如果您真的想了解更多关于内部如何在压力下做出反应的知识,您需要采取的下一步是运行您自己的基准以获得实际实施和经验证据。你会这样做; 使用相同和不同的处理器在Windows Api级别测试 .Net 和自定义参数,以确定开销是否相关;使用不同的硬件;使用不同的操作系统(例如设计了对缓存进行不同细粒度控制的服务器);使用不同的驱动程序,对您的物理设备使用不同的物理路径。
#概括
简而言之,除了保持 Stream 活着(我们已经知道创建/打开文件会产生惩罚);使用异步 IO 操作;使用适当的缓冲区大小;直接将扩展参数发送到Win32 Api 中。除了在不同的操作系统、配置和硬件上运行您自己的性能测试之外,我真的看不到更多,这将能够为您提供更多答案,让您了解在您的情况下什么更高效、更高效。
我希望这会有所帮助,或者至少给人们一个有趣StreamWriter
的 API 之旅(在这两个简单的调用中)。