1.Console Handle(控制台句柄)
1.1 进程的三种标准句柄
每个console进程都有standard input(STDIN), standard output(STDOUT), standard error(STDERR)三种句柄与之相关联,当系统创建console进程时,系统默认地将该进程的STDIN与该进程的控制台的输入缓冲区(input buffer)相关联,将该进程的STDOUT,STDERR与该进程的控制台的活动屏幕缓冲区(active screen buffer)相关联,也就是说standard input(STDIN), standard output(STDOUT), standard error(STDERR)三种句柄本身是与进程相关的,它们与进程的控制台的输入/输出缓冲区没有关联!既然如此,我们的程序可以使用 SetStdHandle 来对进程的三种标准句柄进行重定向,比如可以重定向到文件等。注:我们可以使用 GetStdHandle 来获得当前进程的三种标准句柄。
1.2 控制台的输入缓冲区(input buffer)和活动屏幕缓冲区(active screen buffer)
进程可以通过 CrateFile 函数来获得与该进程相关的控制台的输入缓冲区(input buffer)和活动屏幕缓冲区(active screen buffer),Use the CONIN$ (lpFileName of CreateFile) value to specify console input. Use the CONOUT$(lpFileName of CreateFile) value to specify console output. CONIN$ gets a handle to the console input buffer, even if the SetStdHandle function redirects the standard input handle. To get the standard input handle, use the GetStdHandle function. CONOUT$ gets a handle to the active screen buffer, even if SetStdHandle redirects the standard output handle. To get the standard output handle, use GetStdHandle.
1.3 创建新的屏幕缓冲区
进程可以使用 CreateConsoleScreenBuffer 来创建一个新的屏幕缓冲区,使用 SetConsoleActiveScreenBuffer 来重新设置console screen buffer.
Note that changing the active screen buffer does not affect the handle returned by GetStdHandle. Similarly, using SetStdHandle to change the STDOUT handle does not affect the active screen buffer.
2.获得 Console Handle
应用程序可以通过下面了的代码获得Console输入,输出缓冲区的句柄
1 HANDLE hinput = INVALID_HANDLE_VALUE,houtput = INVALID_HANDLE_VALUE; 2 if(INVALID_HANDLE_VALUE == (hinput = CreateFile(TEXT("CONIN$"), GENERIC_READ, 3 FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL))) 4 { 5 return -1; 6 } 7 if(INVALID_HANDLE_VALUE == (houtput = CreateFile(TEXT("CONOUT$"), GENERIC_WRITE, 8 FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL))) 9 { 10 return -1; 11 }
CUI程序在启动时其进程默认的标准输入和标准输出,标准错误句柄是关联到,控制台的输入,输出缓冲区的,我们可以通过 GetStdHandle 方法来获得。
HANDLE stdinput, stdoutput, stderr;
stdinput = GetStdHandle(STD_INPUT_HANDLE);
stdoutput = GetStdHandle(STD_INPUT_HANDLE);
stderr = GetStdHandle(STD_ERROR_HANDLE);
考虑此时,理论上应该有stdinput == hinput, stdoutput == houtput, stderr == houtput,然而,事实上,却并非如此。控制台的输入和输出缓冲区相当于两种资源,系统并没有使用指针来让我们引用这些资源,目的是为了保护这些资源。而句柄它是进程句柄表的ENTRY的index,所以尽管stdinput,stdoutput,stderr和houtput,hinput不相同,但是其内部的的指针却一定是指向相同的对应的缓冲区地址的,证明如下:
1 #include"../../Common/UnicodeSupport.h" 2 #include<windows.h> 3 #include<stdio.h> 4 int _tmain() 5 { 6 HANDLE hinput = INVALID_HANDLE_VALUE,houtput = INVALID_HANDLE_VALUE; 7 if(INVALID_HANDLE_VALUE == (hinput = CreateFile(TEXT("CONIN$"), GENERIC_READ, 8 FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL))) 9 { 10 return -1; 11 } 12 if(INVALID_HANDLE_VALUE == (houtput = CreateFile(TEXT("CONOUT$"), GENERIC_WRITE,FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL))) 13 { 14 return -1; 15 } 16 if(GetStdHandle(STD_INPUT_HANDLE) != hinput) 17 { 18 _tprintf_s(TEXT("input handle is not same ")); 19 } 20 if(GetStdHandle(STD_OUTPUT_HANDLE) != houtput) 21 { 22 _tprintf_s(TEXT("output handle is not same ")); 23 } 24 LPCTSTR lpBuffer = TEXT("hello, world"); 25 DWORD nums; 26 WriteConsole(houtput, lpBuffer, 13, &nums, NULL); 27 WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), lpBuffer, 13, &nums, NULL); 28 _tprintf_s(TEXT(" houtput = %x stdoutput = %x "), houtput, GetStdHandle(STD_OUTPUT_HANDLE)); 29 _tprintf_s(TEXT("hinput = %x stdinput = %x "), hinput, GetStdHandle(STD_INPUT_HANDLE)); 30 return 0; 31 }
//运行以上程序,输出的结果如下:
input handle is not same
output handle is not same
hello, world hello, world
houtput = 13 stdoutput = b
binput = 7 stdinput = 3
------------------------------------------------------------------------
1.4 关于 CRT(c/c++ runtime library)中Stream I/O中的stdout,stdin,stderr和每个console进程都有standard input(STDIN), standard output(STDOUT), standard error(STDERR)三种句柄之间的关系。
struct _iobuf {
char *_ptr;
int _cnt;
char *_base; // 待输出的字符串的地址
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
在stdio.h文件中我们找到了如下定义:
_CRTIMP extern FILE _iob[];
#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])
可以看出 stdin, stdout, stder 都是 FILE* 类型,也就是 struct _iobuf* 类型, 顾名思义,FILE 类型的变量中存放着 I/O Stream 的缓冲区信息(缓冲区基址,缓冲区的大小,待输出字符串的大小等).
CRT(c/c++ runtime library)中Stream I/O中的stdout,stdin,stderr和每个console 进程都有的 standard input(STDIN), standard output(STDOUT), standard error(STDERR)三种句柄之间的关系是没有任何关系,就是指它们之间并不存在这某种映射关系,使得stdout变化时STDOUT也发生变化。
但是,当我们对CRT中的stdout, stdin, stderr进行重定向的时候,C-RunTime中Stream I/O将对进程的三种标准句柄(STDIN,STDOUT,STDERR)也进行重定向,使其正确地定向到CRT中定向的文件中去。而当我们对进程的三种标准句柄(STDIN,STDOUT,STDERR)进行重定向时,CRT中的stdout,stdin,stderr不会进行重定向。
从逻辑角度上来理解这种重定向行为,Mircorsoft的控制台(Console)API先于其C/C++运行时库中的流类(I/O Stream)API被设计和开发出来,并且C/C++运行时库中的流类(I/O Stream)API实际上是对控制台(Console)API的二次封装。控制台(Console)API中的函数(例如:进行重定向的函数:SetStdHandle),自然不会知道C/C++运行时库中的流类(I/O Stream)API中的任何细节,所以当我们对进程的三种标准句柄(STDIN,STDOUT,STDERR)进行重定向时(使用SetStdHandle),CRT中的stdout,stdin,stderr不会进行重定向,这是自然的。而当设计C/C++运行时库中的流类(I/O Stream)API时,其提供的函数freopen,自然可以调用SetStdHandle对进程的标准句柄进行重定向。
C/C++运行时库中的流类(I/O Stream)API设计思路推测:
printf等输出函数,使用默认的 stdout(FILE*) 进行输出时,其底层必然使用 CreateFile(TEXT("CONOUT$"),GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING, 0, NULL)获得屏幕缓冲区句柄进行输出(使用WriteFile)。
以下代码,对上述推断进行了证明:
1 //使用Console API SetStdHandle 进行重定向 2 if(!SetStdHandle(STD_OUTPUT_HANDLE, CreateFile(TEXT("filex.txt"), GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL))) 3 { 4 _tprintf_s(TEXT("set stdhandle fail. ")); 5 } 6 LPCTSTR lpBuffer = TEXT("hello, world"); 7 DWORD nums; 8 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), lpBuffer, 13 * sizeof(TCHAR), &nums, NULL); 9 _tprintf_s(TEXT("I'm in child process. ")); 10 _ftprintf_s(stdout, TEXT("This is go to file.")); 11 //注意:以上代码使用SetStdHandle进行重定向后,_tprintf_s和_ftprintf_s仍在屏幕上输出
1 //使用CRT流类API _tfreopen_s 进行重定向 2 LPCTSTR lpBuffer = TEXT("hello, world"); 3 DWORD nums; 4 FILE *fwrite; 5 _tfreopen_s(&fwrite, TEXT("file.txt"), TEXT("w"), stdout); 6 _tprintf_s(TEXT("I'm in child process. ")); 7 _ftprintf_s(fwrite, TEXT("This is go to file.")); 8 9 if(!WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), lpBuffer, 13, &nums, NULL)) 10 {_tprintf_s(TEXT("stdoutput fail."));} 11 WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), lpBuffer, 13 * sizeof(TCHAR), &nums, NULL); 12 //注意: 红色代码执行后也将字符串输出到文件file.txt中,可见_tfreopen_s的确对Console的STDOUT也进行了重定向
1.4
当使用以下代码创建新的Console进程时
CreateProcess(TEXT("child.exe"), NULL, NULL, NULL, FALSE,
DETACHED_PROCESS, NULL, NULL, &si, &pi);
注意标记DETACHED_PROCESS,它表示 for console processes, the new process does not inherit its parent's console (the default), and the new process is not attached to any console.So the new process can call the AllocConsole function at a later time to create a console.由于在创建新的进程时使用了DETACHED_PROCESS,则新进程在进行C/C++运行时库初始化时自然无法将stdout,stdin,stderr与有效地控制台(Console)输入/输出句柄进行关联,从而使得当使用AllocConsole创建新的控制台(Console)之后,仍然不能使用printf这样的函数(高级流类API,因为printf类函数默认使用stdout进行输出,然而stdout此时却是无效的。),此时向控制台输出函数是可以有两种方法:
方法一:使用CreateFile and WriteConsole
1 HANDLE houtput; 2 if(INVALID_HANDLE_VALUE == (houtput = CreateFile(TEXT("CONOUT$"), GENERIC_WRITE, 3 FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL))) 4 { 5 _tprintf_s(TEXT("get console output handle fail.")); 6 return -1; 7 } 8 LPCTSTR lpBuffer = TEXT("write go to child.txt. "); 9 DWORD nums; 10 WriteConsole(houtput, lpBuffer, lstrlen(lpBuffer), &nums, NULL);
方法二:使用 Console and Port I/O
例如方法 _cprintf, _cscanf 等等,这类函数直接向当前Console写入值。实际上这些方法内部会使用方法一来完成真正的功能!
方法三:对 stdin, stdout, stderr 进行重定向,从而使其有效,让后就可使用printf类API了。
1 FILE *fwrite; 2 _tfreopen_s(&fwrite, TEXT("child.txt"), TEXT("w"), stdout); 3 _tprintf_s(TEXT("child process. "));
文件句柄和FILE*之间的转换
1 FILE* HANDLE2FILE(HANDLE hFile) 2 { 3 FILE *pfile = reinterpret_cast<FILE*>(malloc(sizeof(FILE))); 4 if(!pfile) 5 { 6 return NULL; 7 } 8 9 int descriptor = _open_osfhandle(reinterpret_cast<intptr_t>(hFile), _O_TEXT | _O_APPEND); 10 if(-1 == descriptor) 11 { 12 return NULL; 13 } 14 15 pfile = _tfdopen(descriptor, TEXT("w+")); 16 if(!pfile) 17 { 18 return NULL; 19 } 20 21 return pfile; 22 }