转 避免读管道时的无限阻塞 (.Net 的Processl类的StandardOutput流的可读状态监测)

转自 http://www.cppblog.com/flyinghare/archive/2012/08/16/187371.html

 

因为QTP的需要,同事写了通过进程来调用Plink进行Telnet连接的接口。我测试的时候发现,他那个调用.Net 里面的Process进程的方法,通过重定向获取标准输出流的办法有点不好,就是调用了流动Read()函数之后,就会一直阻塞在那里,知道流中有数据才能正确返回,而peek函数又不能正确的监测到流中是否有数据可以读。我先去翻翻了MSDN中那个StreamReader类的办法,好像确实没有办法,反倒是在Process的StandardOutput属性的说明那里,明显写着,如果标准输出里面没有数据的话,read函数就会无限时的阻塞在那里知道有数据可以读才行,然后他还提到了一些导致死锁的问题。

我去写了个简单的.Net程序来测试了一下,可以知道那个StreamReader是一个FileStream来的,而且那个CanTimeout等属性都表明不是一个可以异步读取的流。难道真没有办法监测到这个流中是否有数据可读的状态吗? 根据常识知道,这个流应该是“匿名管道”来的,去找了一下MSDN中关于管道的api函数,还真让我找到了一个,那就是PeekNamedPipe http://msdn.microsoft.com/en-us/library/aa365779(VS.85).aspx

根据它的说明,来看看这个

The PeekNamedPipe function is similar to the ReadFile function with the following exceptions:

  • The data is read in the mode specified with CreateNamedPipe. For example, create a pipe with PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE. If you change the mode to PIPE_READMODE_BYTE with SetNamedPipeHandleStateReadFile will read in byte mode, but PeekNamedPipe will continue to read in message mode.
  • The data read from the pipe is not removed from the pipe's buffer.
  • The function can return additional information about the contents of the pipe.
  • The function always returns immediately in a single-threaded application, even if there is no data in the pipe. The wait mode of a named pipe handle (blocking or nonblocking) has no effect on the function.

Note   The PeekNamedPipe function can block thread execution the same way any I/O function can when called on a synchronous handle in a multi-threaded application. To avoid this condition, use a pipe handle created for asynchronous I/O.

If the specified handle is a named pipe handle in byte-read mode, the function reads all available bytes up to the size specified in nBufferSize. For a named pipe handle in message-read mode, the function reads the next message in the pipe. If the message is larger than nBufferSize, the function returns TRUE after reading the specified number of bytes. In this situation, lpBytesLeftThisMessage will receive the number of bytes remaining in the message.

这个函数不管命名管道是不是阻塞模式的,都会立即返回(除了在多线程环境下的某种情况下会阻塞,大概就是http://my.donews.com/yeyanbo/tag/peeknamedpipe/这个文章发现的问题。),文档又说所有的”匿名管道“其实都是一个“命名管道”来实现的,所以操作“命名管道”的函数对“匿名管道”也是有效的。这个函数明显是我想要的,可以用来检测process标准输出流中是否有数据可以读,又不会阻塞。在vb.net的测试代码里面试了一下,应该是可以工作,测试代码如下:

 

Imports System.Diagnostics.Process
Public Class Form1
    Declare Function SetNamedPipeHandleState Lib "kernel32" (ByVal hNamedPipe As Integer, ByRef lpMode As Integer, ByRef lpMaxCollectionCount As Integer, ByRef lpCollectDataTimeout As Integer) As Integer
    Declare Function PeekNamedPipe Lib "kernel32" (ByVal hNamedPipe As Integer, ByRef lpBuffer As Integer, ByVal nBufferSize As Integer, ByRef lpBytesRead As Integer, ByRef lpTotalBytesAvail As Integer, ByRef lpBytesLeftThisMessage As Integer) As Integer
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim p As Process = New Process
        p.StartInfo.FileName = "c:\windows\system32\cmd.exe"
        p.StartInfo.RedirectStandardInput = True
        p.StartInfo.RedirectStandardOutput = True
        p.StartInfo.UseShellExecute = False
        p.Start()
        p.StandardInput.WriteLine("hostname")
        Dim f As System.IO.FileStream = p.StandardOutput.BaseStream

        Dim mode As Integer
        mode = 1 ' no-wait
        Dim count As Integer
        '修改这个命名管道为 异步的,是不能成功的
        ''mode = SetNamedPipeHandleState(f.Handle, mode, System.IntPtr.Zero, System.IntPtr.Zero)
        '不过这个PeekNamePipe 函数是可以得到 管道里面有多少字节可以读到,执行完之后count里面是对的,可以读取的数据
        mode = PeekNamedPipe(f.Handle, System.IntPtr.Zero, 0, System.IntPtr.Zero, count, System.IntPtr.Zero)
        p.StandardOutput.Read()
        While p.StandardOutput.Peek > 0
            p.StandardOutput.Read()
        End While
        '在这个地方的时候就不能再read了,read就无限阻塞直到有数据来才能返回了。
        mode = p.StandardOutput.Peek() '这个时候的peek返回 -1是对的
        mode = PeekNamedPipe(f.Handle, System.IntPtr.Zero, 0, System.IntPtr.Zero, count, System.IntPtr.Zero) '这个count得到0 是对的,管道里面没有消息的了
        p.StandardInput.WriteLine("hostname")
        mode = p.StandardOutput.Peek() '这个还是返回 -1是不对的,
        mode = PeekNamedPipe(f.Handle, System.IntPtr.Zero, 0, System.IntPtr.Zero, count, System.IntPtr.Zero) '这个返回正确的count,表明管道里面有数据是对的
        p.StandardOutput.Read()
        p.StandardOutput.Peek() 'peek函数一定要在read成功调用过一次之后才能正确的得到管道的状态。但Read一次又可能引起无限时间的阻塞!!!!所以只有PeekNamedPipe才能正确的无阻塞的检测到管道的数据
    End Sub
End Class

 

 

总结一下 :感觉。Net对这个“命名管道“”匿名管道“的支持明显不够,API中都有监测到管道是否有数据可以读到函数。.Net里面却连管道对应的类都没有实现,所以相应的这种阻塞情况就也没法处理了。可能这部分的封装有待完善吧。

 

后来有用Reflector工具反汇编看了看系统Process几个类的相应实现代码,可以看到他是CreatePipe创建一个管道,然后DuplicateHandle复制了一个文件句柄的,跟我事先的猜测是一样的。Linux的输出重定向使用也要类似的这样两个步骤。如果你自己看他的代码,可以看到创建的是一个只读的、不支持异步的FileSteam来的,他代码是这样写的:

 

public bool Start()
{
    this.Close();
    ProcessStartInfo startInfo = this.StartInfo;
    if (startInfo.FileName.Length == 0)
    {
        throw new InvalidOperationException(SR.GetString("FileNameMissing"));
    }
    if (startInfo.UseShellExecute)
    {
        return this.StartWithShellExecuteEx(startInfo);
    }
    return this.StartWithCreateProcess(startInfo);
}

private bool StartWithCreateProcess(ProcessStartInfo startInfo)
{
    if ((startInfo.StandardOutputEncoding != null) && !startInfo.RedirectStandardOutput)
    {
        throw new InvalidOperationException(SR.GetString("StandardOutputEncodingNotAllowed"));
    }
    if ((startInfo.StandardErrorEncoding != null) && !startInfo.RedirectStandardError)
    {
        throw new InvalidOperationException(SR.GetString("StandardErrorEncodingNotAllowed"));
    }
    if (this.disposed)
    {
        throw new ObjectDisposedException(base.GetType().Name);
    }
    StringBuilder cmdLine = BuildCommandLine(startInfo.FileName, startInfo.Arguments);
    NativeMethods.STARTUPINFO lpStartupInfo = new NativeMethods.STARTUPINFO();
    SafeNativeMethods.PROCESS_INFORMATION lpProcessInformation = new SafeNativeMethods.PROCESS_INFORMATION();
    SafeProcessHandle processHandle = new SafeProcessHandle();
    SafeThreadHandle handle2 = new SafeThreadHandle();
    int error = 0;
    SafeFileHandle parentHandle = null;
    SafeFileHandle handle4 = null;
    SafeFileHandle handle5 = null;
    GCHandle handle6 = new GCHandle();
    try
    {
        bool flag;
        if ((startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput) || startInfo.RedirectStandardError)
        {
            if (startInfo.RedirectStandardInput)
            {
                this.CreatePipe(out parentHandle, out lpStartupInfo.hStdInput, true);
            }
            else
            {
                lpStartupInfo.hStdInput = new SafeFileHandle(NativeMethods.GetStdHandle(-10), false);
            }
            if (startInfo.RedirectStandardOutput)
            {
                this.CreatePipe(out handle4, out lpStartupInfo.hStdOutput, false);
            }
            else
            {
                lpStartupInfo.hStdOutput = new SafeFileHandle(NativeMethods.GetStdHandle(-11), false);
            }

 

 


中间省略一部分

   if (startInfo.RedirectStandardInput)

    {
        this.standardInput = new StreamWriter(new FileStream(parentHandle, FileAccess.Write, 0x1000, false), Encoding.GetEncoding(NativeMethods.GetConsoleCP()), 0x1000);
        this.standardInput.AutoFlush = true;
    }
    if (startInfo.RedirectStandardOutput)
    {
        Encoding encoding = (startInfo.StandardOutputEncoding != null) ? startInfo.StandardOutputEncoding : Encoding.GetEncoding(NativeMethods.GetConsoleOutputCP());
        this.standardOutput = new StreamReader(new FileStream(handle4, FileAccess.Read, 0x1000, false), encoding, true, 0x1000);
    }
    if (startInfo.RedirectStandardError)
    {
        Encoding encoding2 = (startInfo.StandardErrorEncoding != null) ? startInfo.StandardErrorEncoding : Encoding.GetEncoding(NativeMethods.GetConsoleOutputCP());
        this.standardError = new StreamReader(new FileStream(handle5, FileAccess.Read, 0x1000, false), encoding2, true, 0x1000);
    }
    bool flag3 = false;
    if (!processHandle.IsInvalid)
    {
        this.SetProcessHandle(processHandle);
        this.SetProcessId(lpProcessInformation.dwProcessId);
        handle2.Close();
        flag3 = true;
    }
    return flag3;
}

private void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs)
{
    NativeMethods.SECURITY_ATTRIBUTES lpPipeAttributes = new NativeMethods.SECURITY_ATTRIBUTES();
    lpPipeAttributes.bInheritHandle = true;
    SafeFileHandle hWritePipe = null;
    try
    {
        if (parentInputs)
        {
            CreatePipeWithSecurityAttributes(out childHandle, out hWritePipe, lpPipeAttributes, 0);
        }
        else
        {
            CreatePipeWithSecurityAttributes(out hWritePipe, out childHandle, lpPipeAttributes, 0);
        }
        if (!NativeMethods.DuplicateHandle(new HandleRef(this, NativeMethods.GetCurrentProcess()), hWritePipe, new HandleRef(this, NativeMethods.GetCurrentProcess()), out parentHandle, 0, false, 2))
        {
            throw new Win32Exception();
        }
    }
    finally
    {
        if ((hWritePipe != null) && !hWritePipe.IsInvalid)
        {
            hWritePipe.Close();
        }
    }
}

private static void CreatePipeWithSecurityAttributes(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, NativeMethods.SECURITY_ATTRIBUTES lpPipeAttributes, int nSize)
{
    if ((!NativeMethods.CreatePipe(out hReadPipe, out hWritePipe, lpPipeAttributes, nSize) || hReadPipe.IsInvalid) || hWritePipe.IsInvalid)
    {
        throw new Win32Exception();
    }
}

public override int Peek()
{
    if (this.stream == null)
    {
        __Error.ReaderClosed();
    }
    if ((this.charPos != this.charLen) || (!this._isBlocked && (this.ReadBuffer() != 0)))
    {
        return this.charBuffer[this.charPos];
    }
    return -1;
}

转自:http://hi.baidu.com/widebright/item/f58e2516a6bb41dcbf9042a4

posted @ 2017-08-01 14:13  以函  阅读(515)  评论(0编辑  收藏  举报