管道技术
管道技术是用在两个进程之间的通信,首先我们来看看进程之间的通信方式会有哪些
在很多时候会需要共享内核对象,
1、利用文件映射对象,可以在同一个机器上运行的两个不同进程之间共享数据块
2、借助邮件槽和命名管道,在网络中的不同计算机上运行进程可以相互发送数据块
3、互斥量、信号量和时间允许不同进程中的线程同步执行。
在操作系统中将句柄设计成为相对于某个进程的。避免进程中使用另一个进程中的内句柄进行改变。但是一个进程创建一个内核对象,这个内核对象的管理者是操作系统,其它的进程也可以访问。
也就是在子进程中可以访问父进程的内核对象。在CreateProcess函数中的bInheritHandles中指定为true,也就是允许子进程进行继承。在创建新的进程时,系统会为子进程分配一个句柄表,若是可继承的,那么会遍历父进程的句柄表,对每一个可继承的句柄,都会完整复制到进程的句柄表中,复制项在子进程与父进程中是完全一样的。也就是对一个内核对象进行标志的句柄值是完全一样的。当然会增加计数值。那么在销毁的时候,必须显示的在两个进程中进行销毁。
要控制子进程能继承内核对象的句柄,则可以调用SetHandleInformation函数来改变内核对象句柄的继承标志。
SetHandleInformation(
__in HANDLE hObject,
__in DWORD dwMask,
__in DWORD dwFlags
);
第一个参数指定的是一个有效的句柄,第二个参数告诉我们想更改哪个或哪些标志,#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
那么第三个参数指出希望把标识设为什么。若要打开一个内核对象句柄的继承。可以使用下面这个函数.要关闭这个标志,可以将第3个参数值设为0。 SetHandleInformation(hdl,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT);
在创建内核对象的时候我们几乎会涉及到_SECURITY_ATTRIBUTES这个结构体。安全描述符描述了谁拥有这个对象,哪些组和用户被允许访问或使用这个对象。
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
实际上只包含一个和安全有关的成员,即第二个成员变量。那么为了创建一个可继承的句柄,父进程必须分配并初始化一个SECURITY_ATTRIBUTES 的结构,并将这个结构的地址传给具体的Create函数。若在创建内核对象的时候将NULL作为这个结构的参数传入,那么返回的句柄是不可继承的,相应的在句柄表中的标志为0.
命令行的作用:指定一个完整的命令行,供CreateProcess用于创建新进程。在解析pazCommandLine字符串时。会检查字符串中的第一个标记,并会假定这个标记是我们想运行的可执行文件的名称。若可执行文件没有扩展名,就会默认是.exe扩展名。比如在C++运行启动例程会检查进程的命令行,将可执行文件名之后的第一个实参的地址传给WinMain的pazCmdLine参数。
可以使用GetCommandLine来获取一个指向进程完整命令行的指针。函数返回一个缓冲区指针。缓冲区中包括完整的命令行。可以将句柄值作为命令行参数传给子进程,子进程得到初始化代码将解析命令行。
一个指向NULL终止的字符串,用来指定可执行程序的名称,注意这里必须是包括完整的路径。与上面参数不同的是,命令行会自动加上扩展名,并会从几个路径中分别去找可执行文件,
每个进程都有一个与它关联的环境块,这是在进程地址空间内分配的一个内存。包括进程当前的系统,或者使用的语言,应用程序放置的位置等。其中包含的字符串与下面的类似:
=::=;;\...
VarName1=Value1\0
操作方法:使用GetEnvironmentStrings获取主调进程正在使用的环境字符串数据块的地址。通常子进程会继承一组环境变量,这些环境变量和父进程的环境变量相同,但是父进程可以控制哪些环境变量允许子进程继承。但是这种继承其实是一个副本,它的修改是不会影响到父进程的。我们要使子进程得到它想要的一个内核对象的句柄值,可以使用环境变量,变量名字为句柄名,变量值为句柄值。通过继承就可以获得这个句柄值。若参数为NULL,则新进程使用调用进程的环境,通常我们也是这样设置的。
若没有提供完整的路径名,Windows函数会在当前驱动器的当前目录查找文件和目录。若传入的路径名是当前驱动器之外的驱动器,系统会在进程的环境变量块中寻找与指定驱动器号关联的变量。一个父进程创建一个可以传递给子进程的环境块,子进程不会自动继承父进程的当前目录。所以要做到就必须在生成子进程之前,创建这些驱动器号环境变量,并将它们添加到环境块中。
使用函数GetFullPthName来获得它的当前目录。
CreateProcess(strFileName,NULL,NULL,NULL,true,0,NULL,NULL,&si,&pt)
CreateProcessA(
__in_opt LPCSTR lpApplicationName,
__inout_opt LPSTR lpCommandLine,
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in_opt LPVOID lpEnvironment,
__in_opt LPCSTR lpCurrentDirectory,
__in LPSTARTUPINFOA lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation
);
在创建进程中,我们可以看到这样的一些变量。
1) lpProcessAttributes与lpThreadAttributes,在创建一个新的进程时,系统必须创建一个进程内核对象和一个线程内核对象,这个线程的内核对象是进程的主线程。根据需要为进程对象和线程对象指定安全性。
2) dwCreationFlags标志影响新进程创建方式的标志,比如说父进程希望调试子进程以及子进程将来生成的所有进程。也就是说在任何一个子进程中发生特定的事件时,要通知父进程。
3)lpCurrentDirectory允许父进程设置子进程的当前驱动器和目录,若这个参数为NULL,则新进程的工作目录与生成新进程的应用程序一样。
4)lpStartupInfo用于指定新进程的主窗口将如何显示,
typedef struct _STARTUPINFOA {
DWORD cb;
LPSTR lpReserved;
LPSTR lpDesktop;
LPSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;
在这里我们关注几个变量,dwFlags包括一组标志,用于修改子进程的创建方式。告诉CreateProcess在这个结构体中其他成员是否包含有用的信息。
(1) hStdInput,hStdOutput,hStdError指定到控制台输入缓冲区的句柄和输出缓冲区的句柄。通常后面两个标志一个控制台窗口的缓冲区。这些字段用于重定向子进程的输入/输出。
(2) lpProcessInformation在创建进程函数返回之前会初始化这个结构的成员。
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
创建一个新的进程会创建一个进程内核对象和一个线程内核对象,在创建时,喜用会为每个对象指定一个初始的实用计数1,打开这个对象,那么对象的计数会变为2.看到里面有进程ID与线程ID,有的时候我就在疑惑,这个ID 的作用是什么。
在创建一个进程内核对象时,系统会为此对象分配一个独一无二的标志符,线程也是一样,这是由操作系统分配的,在创建进程返回之前,会将这些ID填充到PROCESS_INFORMATION 结构体中的dwProcessId和dwThreadId成员中,这个ID可以是我们容易识别系统中的进程和线程。主要用于任务管理器使用。进程对象释放,但是ID可能会依旧保存,不过可能指向的是另一个进程对象。可以使用GetCurrentProcessId来得到当前进程的ID。
句柄值只是作为进程句柄表中的索引来是同的,这些句柄是与当前这个句柄相关的,若是我们在其它的进程中使用这个值其实是使用那个进程句柄表中位于同一个索引的内核对象,那么在句柄表中有些什么呢?
索引 |
指向内核对象的内存块的指针 |
访问掩码 |
标志 |
1 |
Ox????? |
||
2 |
我们看到在索引对应的有内存块的指针,
1) 会终止进程中遗留的任何线程
2) 关闭所有内核对象
3) 进程的退出代码从STILL_ACTIVE变为传给ExitProcess函数的代码
4) 进程内核对象的状态变成已触发状态,当内核对象变成触发,正在等待的时间将变成可调度的。
5) 进程内核对象的使用技术减1
匿名管道:是一个未命名的、单向管道,通常在一个父进程与子进程之间传输数据,只能实现本地机器上两个进程间的通信,不能实现跨网络的通信。
CreatePipe(
__out_ecount_full(1) PHANDLE hReadPipe,
__out_ecount_full(1) PHANDLE hWritePipe,
__in_opt LPSECURITY_ATTRIBUTES lpPipeAttributes,
__in DWORD nSize
);
hReadPipe为out类型,用来返回管道的读写句柄,hWritePipe接收管道的写入句柄,
lpPipeAttributes检测返回的句柄是否能被子进程继承,但是我们是匿名的管道,在父子进程之间进行通信,子进程如想获得匿名管道的句柄们只能从父进程继承来,当一个子进程从父进程中继承匿名管道的句柄后,这两个进程就可以通过该句柄进行通信。所以我们设置的值为NULL。在SECURITY_ATTRIBUTES结构体中我们设置bInheritHandle的值为TRUE。
//因为子进程并不知道哪一个是管道的读写句柄,为了区分出,将子进程的特殊句柄设置为管道的读写句柄
//于是将标准输入和输出句柄分别设置为管道的读写句柄,那么在子进程中得到了标准输入和输出就相当于得到管道的读写句柄。
si.hStdError=GetStdHandle(STD_ERROR_HANDLE);//可以获得标准输入和输出或者一个标准错误输出句柄。
1、举个最简单的与cmd进行通信的程序。我们需要的是两个管道,一个是从父进程中向子进程中发送命令,一个是从子进程向父进程中返回结果。
2、注意事项:
1) 在字符串的最后要加上’\r\n’,否则会一直的等待输入,也许这个时候进程都已经结束了。
2) 注意输入字符的长度,在加了上述两个字符之后我们计算字符长度就不需要strlen(chInput)+1了。这样同样会让WriteFile处于一直等待的状态
3) 在创建进程中要指明标准输入输出的句柄为什么
4) 在创建了新进程之后,我们就可以关闭进程与主线程的句柄,这中方式只是让计数减1.并没有关闭进程。
//两个函数大致上还是相似的,还有些不足
//1.没有使用重用
//2.没有显示的关闭cmd进程
//3.在读取数据的时候一直处于等待,不知道有没有这样的一个函数,还是说我写的代码有问题
bool CTestCompileDlg::IsCE()//判断编译错误
{
#pragma region findFileAndCreatePipeAndCreateProcess
CString strFilePath="C:\\SDOJ";
CString strFileName="C:\\SDOJ\\OJtest.cpp";
CFileFind fileFind;
if (!fileFind.FindFile(strFilePath))
{
CreateDirectory(strFilePath,NULL);
}
if (!fileFind.FindFile(strFileName))
{
return true;
}
CFile oFile;
oFile.Open(strFileName,CFile::modeRead,NULL);
int nFileSize=oFile.GetLength();
char *chRead=new char[nFileSize];
ZeroMemory(chRead,nFileSize);
oFile.Read(chRead,nFileSize);//读取文件到chRead中
if (nFileSize==0)
{
m_ctrlRichEdit.SetWindowText(TEXT("读取文件失败"));
return true;
}
oFile.Close();
SECURITY_ATTRIBUTES sa;//安全描述
sa.nLength=sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle=TRUE;
sa.lpSecurityDescriptor=NULL;
HANDLE ChildIn_Read=NULL;//从子进程中读取文件
HANDLE ChildIn_Write=NULL;//向子进程中输入文件
//为子进程的标准输入创建管道
if (!CreatePipe(&ChildIn_Read,&ChildIn_Write,&sa,0))//创建管道
{
MessageBox(TEXT("创建管道失败"));
CloseHandle(ChildIn_Read);
ChildIn_Read=NULL;
CloseHandle(ChildIn_Write);
ChildIn_Write=NULL;
return true;
}
//保证标准输入的写句柄没有被继承
SetHandleInformation(ChildIn_Write,HANDLE_FLAG_INHERIT,0);
HANDLE ChildOut_Read=NULL;//子进程的输出句柄
HANDLE ChildOut_Write=NULL;//父进程读取子进程的句柄
//为子进程的标准输出创建管道
if(!CreatePipe(&ChildOut_Read,&ChildOut_Write,&sa,0))//创建管道
{
MessageBox("创建管道失败");
CloseHandle(ChildOut_Read);
ChildOut_Read=NULL;
CloseHandle(ChildOut_Write);
ChildOut_Write=NULL;
return true;
}
//保证标准输入的写句柄没有被继承
SetHandleInformation(ChildOut_Read,HANDLE_FLAG_INHERIT,0);
//创建进程
STARTUPINFO si={0};
PROCESS_INFORMATION pt={0};
ZeroMemory(&pt,sizeof(PROCESS_INFORMATION));
ZeroMemory(&si,sizeof(STARTUPINFO));
si.cb=sizeof(STARTUPINFO);
si.hStdInput=ChildIn_Read;//父进程写入子进程
si.hStdOutput=ChildOut_Write;//子进程输出
si.hStdError=ChildOut_Write;
//设置子进程接受StdIn以及StdOut的重定向
si.dwFlags=STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW;
si.wShowWindow=SW_HIDE;
#pragma endregion
DWORD dwWrite=NULL;
char linkCmd[200]="G++ -c C:\\SDOJ\\OJtest.cpp>C:\\SDOJ\\OJtest.o\r\n";
int length2=strlen(linkCmd);
if(!WriteFile(ChildIn_Write,linkCmd,strlen(linkCmd),&dwWrite,NULL))
{
MessageBox("写入管道失败");
return false;
}//向管道中写入
FlushFileBuffers(ChildIn_Write);
char exeCmd[200]="G++ C:\\SDOJ\\OJtest.cpp -o C:\\SDOJ\\OJtest.exe\r\n";
if(!WriteFile(ChildIn_Write,exeCmd,strlen(exeCmd),&dwWrite,NULL))
{
MessageBox("写入管道失败");
return false;
}
Sleep(2000);
FlushFileBuffers(ChildIn_Write);
int errorCode=GetLastError();
CloseHandle(ChildIn_Write);//关闭父进程的输入管道
ChildIn_Write=NULL;//将句柄置为空
CloseHandle(ChildOut_Write);//关闭子进程的输出句柄
ChildOut_Write=NULL;
CloseHandle(ChildIn_Read);
ChildIn_Read=NULL;
CloseHandle(ChildOut_Read);
ChildOut_Read=NULL;
return false;
}
//根据输入输出文件对答案和数据 bool CTestCompileDlg::IsWA(CString inputpath,CString outputpath,bool IsPath) { //获取输入值 CFile fOpen; fOpen.Open(inputpath,CFile::modeRead,NULL);//打开文件 long nFileSize=fOpen.GetLength(); char *input=new char[nFileSize+1];//放入数据的缓冲区 fOpen.Read(input,nFileSize);//读取输入的文件 input[nFileSize]='\0'; fOpen.Close(); //获取文件中的输出值 char *output=NULL; fOpen.Open(outputpath,CFile::modeRead,NULL);//打开模式为Read nFileSize=fOpen.GetLength(); output=new char[nFileSize+1]; fOpen.Read(output,nFileSize);//读取输出的文件 output[nFileSize]='\0'; fOpen.Close();//关闭文件 //判断刚写入的cpp文件是否存在,首先判断路径,然后判断文件 CFileFind m_FileFind; CString m_sFilePath = "C:\\SDOJ"; if(!m_FileFind.FindFile(m_sFilePath)) //路径不存在则创建该路径 { CreateDirectory(m_sFilePath,NULL); } CString m_sFileName="C:\\SDOJ\\OJtest.exe"; if (!m_FileFind.FindFile(m_sFileName)) { return true; } //创建一个新的进程运行编写好的cpp文件 SECURITY_ATTRIBUTES sa={0};//安全描述信息 sa.nLength=sizeof(SECURITY_ATTRIBUTES);//获取结构体的长度 sa.bInheritHandle=true;//可继承的,使得父子进程可以通信 sa.lpSecurityDescriptor=NULL;//安全描述为默认的安全描述符 HANDLE ChildIn_Read=NULL;//从子进程中读取文件 HANDLE ChildIn_Write=NULL;//向子进程中输入文件 //为子进程的标准输入创建管道 if (!CreatePipe(&ChildIn_Read,&ChildIn_Write,&sa,0))//创建管道 { MessageBox(TEXT("创建管道失败")); CloseHandle(ChildIn_Read); ChildIn_Read=NULL; CloseHandle(ChildIn_Write); ChildIn_Write=NULL; return true; } //保证标准输入的写句柄没有被继承 SetHandleInformation(ChildIn_Write,HANDLE_FLAG_INHERIT,0); HANDLE ChildOut_Read=NULL;//子进程的输出句柄 HANDLE ChildOut_Write=NULL;//父进程读取子进程的句柄 //为子进程的标准输出创建管道 if(!CreatePipe(&ChildOut_Read,&ChildOut_Write,&sa,0))//创建管道 { MessageBox("创建管道失败"); CloseHandle(ChildOut_Read); ChildOut_Read=NULL; CloseHandle(ChildOut_Write); ChildOut_Write=NULL; return true; } //保证标准输入的写句柄没有被继承 SetHandleInformation(ChildOut_Read,HANDLE_FLAG_INHERIT,0); STARTUPINFO si;//初始化信息 PROCESS_INFORMATION pt;//进程的描述信息 ZeroMemory(&pt,sizeof(PROCESS_INFORMATION)); ZeroMemory(&si,sizeof(STARTUPINFO)); si.dwFlags=STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW;//设置标准输入、输出以及标准错误句柄是有用的 si.wShowWindow = SW_HIDE; si.cb=sizeof(STARTUPINFO); si.hStdInput=ChildIn_Read;//将标准输入设置为管道的读句柄 si.hStdOutput=ChildOut_Write;//将标准输出设置为管道的写句柄 si.hStdError=ChildOut_Write; //因为子进程并不知道哪一个是管道的读写句柄,为了区分出,将子进程的特殊句柄设置为管道的读写句柄 //于是将标准输入和输出句柄分别设置为管道的读写句柄,那么在子进程中得到了标准输入和输出就相当于得到管道的读写句柄。 si.hStdError=GetStdHandle(STD_ERROR_HANDLE);//可以获得标准输入和输出或者一个标准错误输出句柄。 //获得的是父进程的标准错误句柄,也就是子进程的标准错误句柄设置为父进程的标准错误句柄。 CString strFilePath="C:\\SDOJ\\OJtest.exe"; if(!CreateProcess(strFilePath, NULL, NULL, NULL, true,0 , NULL, NULL, &si, &pt)) {//第五个参数设为TRUE,让父进程的每个可继承的打开句柄都能被子进程继承。第6个参数为创建标志, CloseHandle(ChildIn_Read);//在创建一个新进程时,系统会建立一个进程内核对象和一个线程内核对象 //那么内核对象都有一个使用计数,系统在这个时候会初始为1,在创建进程函数返回前,会打开进程 //对象和线程对象,此时技术为2.在Close时计数为减1,在进程终止运行,系统会再减1. CloseHandle(ChildIn_Write); CloseHandle(ChildOut_Read); CloseHandle(ChildOut_Write); ChildOut_Write=NULL; ChildOut_Read=NULL; ChildIn_Read=NULL; ChildIn_Write=NULL; MessageBox(TEXT("创建进程失败")); return true; } else { CloseHandle(pt.hProcess); CloseHandle(pt.hThread); } DWORD dwWrite;//保存实际写入的字节数 if (!WriteFile(ChildIn_Write,input,strlen(input),&dwWrite,NULL))//向管道中输入,这个函数可以完成对控制台、管道这类对象的读取操作。 { MessageBox("写入数据失败"); return true; } CloseHandle(ChildIn_Read); CloseHandle(ChildIn_Write); ChildIn_Read=NULL; ChildIn_Write=NULL; char chResult[100];//获得程序输出 memset(chResult,0,100); DWORD dwGet;//用来保存实际读取到的字节数, if(!ReadFile(ChildOut_Read,chResult,100,&dwGet,NULL)) { MessageBox("读取文件失败"); return true; } CloseHandle(ChildOut_Read); CloseHandle(ChildOut_Write); ChildOut_Write=NULL; ChildOut_Read=NULL; chResult[dwGet]='\0'; int length=strlen(chResult); if (strcmp(chResult,output)==0)//获取答案和标准答案相同,返回正确 { delete []input;//撤消堆中内容 delete []output;//清空申请的内存 return false; } delete []input;//撤消堆中内容 delete []output;//清空申请的内存 return true; }