IPC별 특성 


 

메일슬롯 

이름없는파이프 

이름있는파이프 

방향성 

단방향 ,브로드캐스팅

단방향 

양방향 

통신범위 

제한 없음 

부모 자식 프로세스 

제한 없음 





예제8-1


이름없는 파이프


#include <windows.h>

#include <tchar.h>

#include <stdio.h>


int _tmain(int argc, LPTSTR argv[])

{

HANDLE hReadPipe, hWritePipe; //pipe handle


TCHAR sendString[] = _T("anonymous pipe");

TCHAR recvString[100];


DWORD bytesWritten;

DWORD bytesRead;          


/* pipe 생성 */

CreatePipe(&hReadPipe, &hWritePipe, NULL, 0);

/* 이름없는 파이프는 단방향성이기에 함수를 호출하면 파이프의 쓰는 부분과 읽는 부분을 반환받는다. */


/* pipe의 한쪽 끝을 이용한 데이터 송신 */ 

WriteFile(hWritePipe, sendString, lstrlen(sendString)*sizeof(TCHAR), &bytesWritten,

                                                                                                                     NULL);

_tprintf( _T("string send: %s \n"), sendString);



/* pipe의 다른 한쪽 끝을 이용한 데이터 수신 */ 

ReadFile(hReadPipe, recvString, bytesWritten, &bytesRead, NULL);

recvString[bytesRead/sizeof(TCHAR)] = 0;

_tprintf( _T("string recv: %s \n"), recvString);



CloseHandle(hReadPipe);

CloseHandle(hWritePipe);


return 0;

}


예제를 보면 CreatePipe함수를 호출하는 프로세스가 파이프의 읽고 쓰는 핸들을 모두 가지고 있다. 

이름없는 파이프는 부모 자식 프로세스간 에만  통신이 가능하다고 위에 나왔다.

 부모 프로세스는 자식 프로세스에 핸들을 상속 할 수 있기에 자식프로세스에 핸들을 상속하여  통신이 가능하다.




이름있는 파이프 프로그래밍 모델





1. CreateNamedPipe 함수에 의해 생성

2. ConnectNamedPipe 함수에 의해 연결 대기 상태로 전환

3. Client의 CreateFile 함수 호출에 의한 파이프 오픈


이름 있는 파이프는 양방향 통신이다.

먼저 서버가 파이프를 생성 한다.

CreateNamePipe함수를 호출하면 서버는 파이프를 가지고 있는다. 하지만 말그대로 가지고만 있을 뿐 외부와의 연결은 되지않는 상태이다.

외부와의 연결을 위해

ConnectNamePipe 함수를 호출통하여 외부에 파이프를 노출시킨다.

외부에서 파이프에 연결은 CreateFile 함수를 이용한다.




예제 8-2

이름있는파이프 서버


#include <stdio.h>

#include <stdlib.h>

#include <windows.h> 


#define BUF_SIZE 1024


int CommToClient(HANDLE);


int _tmain(int argc, TCHAR* argv[]) 

{

LPTSTR pipeName = _T("\\\\.\\pipe\\simple_pipe"); 


HANDLE hPipe;

while(1)

{

  hPipe = CreateNamedPipe ( 

  pipeName,            // 파이프 이름

  PIPE_ACCESS_DUPLEX,       // 읽기,쓰기 모드 지정

  PIPE_TYPE_MESSAGE |

  PIPE_READMODE_MESSAGE | PIPE_WAIT,

  PIPE_UNLIMITED_INSTANCES, // 최대 인스턴스 개수.

  BUF_SIZE,                  // 출력버퍼 사이즈.

  BUF_SIZE,                  // 입력버퍼 사이즈 

  20000,                   // 클라이언트 타임-아웃  

  NULL                    // 디폴트 보안 속성

  );

  

  if (hPipe == INVALID_HANDLE_VALUE) 

  {

  _tprintf( _T("CreatePipe failed")); 

  return -1;

  }

  

  BOOL isSuccess; 

  isSuccess = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED); 

  

  if (isSuccess) 

  CommToClient(hPipe);

  else 

  CloseHandle(hPipe); 

}

return 1; 


int CommToClient(HANDLE hPipe)

TCHAR fileName[MAX_PATH];

TCHAR dataBuf[BUF_SIZE];


BOOL isSuccess;

DWORD fileNameSize;


isSuccess = ReadFile ( 

hPipe,        // 파이프 핸들 

fileName,    // read 버퍼 지정.

MAX_PATH * sizeof(TCHAR), // read 버퍼 사이즈 

&fileNameSize,  // 수신한 데이터 크기

NULL);       


if (!isSuccess || fileNameSize == 0) 

{

_tprintf( _T("Pipe read message error! \n") );

return -1; 

}


FILE * filePtr = _tfopen(fileName, _T("r") );


if(filePtr == NULL)

{

_tprintf( _T("File open fault! \n") );

return -1; 

}


DWORD bytesWritten = 0;

DWORD bytesRead = 0;


while( !feof(filePtr) )

{

bytesRead = fread(dataBuf, 1, BUF_SIZE, filePtr);


WriteFile ( 

hPipe, // 파이프 핸들

dataBuf, // 전송할 데이터 버퍼  

bytesRead, // 전송할 데이터 크기 

&bytesWritten, // 전송된 데이터 크기 

NULL);


       if (bytesRead != bytesWritten)

{

   _tprintf( _T("Pipe write message error! \n") );

   break; 

}

}


FlushFileBuffers(hPipe); 

DisconnectNamedPipe(hPipe); 

CloseHandle(hPipe); 

return 1;

}




예제 8-3

이름있는 파이프 클라이언트



#include <stdio.h>

#include <stdlib.h>

#include <windows.h>


#define BUF_SIZE 1024


int _tmain(int argc, TCHAR *argv[]) 

{

    HANDLE hPipe;

    TCHAR readDataBuf[BUF_SIZE + 1];

    LPTSTR pipeName = _T("\\\\.\\pipe\\simple_pipe"); 

    

    while(1)

    {

      hPipe = CreateFile( 

      pipeName,             // 파이프 이름 

      GENERIC_READ | GENERIC_WRITE, // 읽기, 쓰기 모드 동                         

                                                                                                               시 지정 

      0,

      NULL,

      OPEN_EXISTING,

      0,

      NULL

      );

     

      if (hPipe != INVALID_HANDLE_VALUE)

         break;

         

      if (GetLastError() != ERROR_PIPE_BUSY)

      {

         _tprintf( _T("Could not open pipe \n") );        

         return 0;

      }

     

      if (!WaitNamedPipe(pipeName, 20000))

      {

         _tprintf( _T("Could not open pipe \n") );

         return 0;

      }

    }

    

    DWORD pipeMode = PIPE_READMODE_MESSAGE|PIPE_WAIT; // 메시지 기반으로 모드 변경.

    BOOL isSuccess = SetNamedPipeHandleState (

                       hPipe,      // 파이프 핸들

                       &pipeMode,  // 변경할 모드 정보.  

                       NULL,     // 설정하지 않는다. 

                       NULL);    // 설정하지 않는다. 

    

    if (!isSuccess)

    {

        _tprintf( _T("SetNamedPipeHandleState failed") ); 

        return 0;

    }

    

    LPCTSTR fileName = _T("news.txt");

    DWORD bytesWritten = 0;

    

    isSuccess = WriteFile (

                    hPipe,                // 파이프 핸들

                    fileName,             // 전송할 메시지 

                    (lstrlen(fileName)+1) * sizeof(TCHAR), // 메시지 길이 

                    &bytesWritten,             // 전송된 바이트 수

                    NULL);

    

    if (!isSuccess) 

    {

      _tprintf( _T("WriteFile failed") ); 

      return 0;

    }

    

    DWORD bytesRead = 0;

    while(1) 

    {

        isSuccess = ReadFile(

                        hPipe, // 파이프 핸들

                        readDataBuf, // 데이터 수신할 버퍼

                        BUF_SIZE * sizeof(TCHAR),  // 버퍼 사이즈

                        &bytesRead, // 수신한 바이트 수

                        NULL);      

        if (! isSuccess && GetLastError() != ERROR_MORE_DATA) 

            break; 

        

        readDataBuf[bytesRead] = 0; 

        _tprintf( _T("%s \n"), readDataBuf ); 

    }

    

    CloseHandle(hPipe); 

    return 0; 

}






핸들 테이블의 상속


BOOL CreateProcess(

LPCTSTR lpApplicationName , 

LPTSTR lpCommandLine ,                                                                                    

LPSECURITY_ATTRIBUTES lpProcessAttributes ,

LPSECURITY_ATTRIBUTES lpTreadAttributes ,

BOOL binheritHandles , // 핸들테이블의 상속여부

DWORD dwCreationFlags ,

LPVOID lpEnvironment ,

LPCTSTR lpCurrentDirectory ,

LPSTARTUPINFO lpstrartupInfo ,

LPPROCESS_INFORMATION lpProcessInformation

);


핸들테이블은 5장 과 6장에서 각 리소스에 독립적이라 하였는데 핸들테이블은 조건에 따라 상속도 가능하다. 

상속될때 핸들같이 동일하게 상속된다.

핸들테이블에 핸들 정보에 상속가능여부가 있는데 이 값이 가능상태여야 상속이가능하다.

ex)


 핸들

주소 

상속여부 

127 

0x1200 

168 

0x1680 

 ......

 ......

 ......

이런 식으로 상속 가능여부가 핸들값마다 저장이 되어진다.



CreateProcess함수를 살펴보면 5번째 인자에 BOOL binheritHandles 가있는데 이 인자값을 true 을 주면 부모프로세스의 핸들테이블에 상속가능한 핸들들이 상속된다.



그러하다면 어떻게하면 핸들값의 상속여부를 가능하게 할 수 있을 것인가.

 

ex)


SECURITY_ATTRIBUTES sa;

sa.nLength=sizeof(sa);

sa.lpSecurityDescriptor=NULL;

sa.bInheritHandle=TRUE;  // 이런식으로 하면 상속가능하게 할 수 있다.


.................


CreateMailslot(......... &sa); //4번째 전달인자이다.


대부분의 리소스 생성과정에서 보면 SECURITY_ATTRIBUTES 즉 보안관리자 설정을 위한 구조체가 사용된다.

이보안설정을 하는과정에서 앞의 강의에서 NULL 설정을 하였다.

보안 설정을 하지 않을 경우 상속가능여부는 가능하지 않게 된다.


메일 슬롯 뿐아리라 대부분 다른 리소스를 생성할 경우에도 보안관리구조체의 포인터를 전달하면 동일하게 설정 할 수 있다.



핸들의 상속과 UC




핸들 값을 상속하게 되면 당연하게도 UC값이 증가한다.








Pseudo 핸들과 핸들의 중복


6장에 나왓듯이 프로세스가 자기자신의 핸들값은 핸들테이블에 저장시켜서 사용하는것이 아니라 GetCurrentProcess함수를 이용하여 특정 상수를 이용한다라고 하였는데 이를 Pseudo 핸들 이라한다.즉 가짜핸들이다.


하지만 이런 일이 일어난다고 해보자.


A프로세스가 B라는 자식프로세스에게 A프로세스의 핸들을 상속하고싶다.

하지만 A프로세스의 핸들테이블에는 A의 핸들정보가 존재하지않는다.

그래서 A프로세스의 핸들테이블에 자기자신의 핸들정보를 저장하여야 한다.



DuplicateHandle 함수가 있다


대략 하는일은 DulpicateHandle(A 핸들, 256,B핸들,&val,.......)  

                                                                        HANDLE val=364

이런식으로 함수를 호출하면 밑에 처럼 A프로세스의 핸들테이블에 있는 256 핸들이    B프로세스의 핸들테이블에 364 값으로 복사가 된다. 

여기서 다시한번 핸들테이블은 각 리소스에 독립적이라는 것을 인식하자.


프로세스 A     

 프로세스B

 

핸들 

주소 

 

핸들 

주소 

... 

... 

 

..0

... 

256 

0x2400 

 ------------>

364 

0x2400 

 ...

... 

 복사

... 

 ...


이것으로 이제 Pseudo 핸들을 핸들테이블에 저장해보자.



DuplicateHandle(

프로세스A핸들,

256,

프로세스A핸들,

&val,

.....

);


 핸들 

주소 

 

 

... 

...   

 256

0x2400 

 ---------------->

 Usage count : 2

 ...

 ...  

 284

  0x2400  ----------------> 

 ...

...   


한테이블에서 핸들을 복사 할 수도 있다.

이러면 같은 주소를 가리키는 핸들이 하나 더 생긴다. 

※물론  UC도 늘어남으로 나중에 핸들을 닫을때 두번 닫아야 한다.


이제 자신의 핸들값을 상속 할 수있게 Pseudo 핸들을 핸들테이블에 저장해보자.


DuplicateHandle(

GetCurrentProcess(),

GetCurrentProcess(),

GetCurrentProcess(),

&hProcess,

0,

TRUE,

DUPLICATE_SAME_ACCESS

);


앞에 3인자에 모두 GetCurrentProcess함수를 넣으면 함수를 호출한 프로세스의 핸들이 반환되니까 호출한 프로세스의 핸들테이블에 호출한프로세스의 핸들값이 저장이 되는것이다. 





 Signaled vs Non-Signaled



커널 오브젝트의 상태


이곳은 프로세스 기준이다.

커널오브젝트의 구성멤버 변수중에 커널오브젝의 상태정보 변수가있는데 이 변수는

프로세스가 실행중이면 프로세스의 커널오브젝트가 Non-signaled 상태 이고

                실행 종료되면  프로세스의 커널오브젝트가 signaled 상태가 된다.  


프로세스가 실행되면 그 프로세스의 커널 오브젝트는 바로  Non-signaled 가 된다.


커널 오브젝트의 상태가 필요한 이유는 프로세스의 핸들을 가지고 있는 무언가가 프로세스의 실행 여부를 판단하기 위해서이다.


상태관찰 시나리오


부모프로세스가 자식 프로세스 생성 - >자식프로세스의 핸들값반환 


WaitForSingleObject(자식프로세스핸들인자){

Non-signaled 면 멈춰있는다.

signaled 상태면 동작한다.

}



IPC(interprocessor communication)


하나의 프로그램에서 여러게의 프로세스를 형성 할 수있다.


프로세스간 통신

프로세스간 데이터 송 수신→메모리 공유 


A에서 B로 주고 받는다는 느낌보다는 메모리 공유 기법에서 이루어진다.

process A ------[memory]--------process B

각 프로세스의 메모리영역이 분리 되어있고 그 범위를 벗어날 수 없기때문에 프로세스들은 직접 서로 데이터를 주고 받을 수 없다. 

다른 프로세스의 메모리에 접근은 안전성을 위해서 막혀있다. 

[출일슬롯 방식의 IPC|작성자 F



메일슬롯(Mail Slot)


Receiver process<----- Receiver's Mail slot <------- Sender process


데이터를 받기 위해서 프로세스는 접근가능한 공간이 필요하다. 그렇기에 OS는 메일슬롯을 생성하고 받는프로세서는 함수호출을 통한 간접적 방법으로 이 영역에 접근이 가능하다. 그리고 이 영역에 주소를 할당하여 데이터를 전송하는 프로세스는 이 주소를 통하여 데이터를 전송한다.

우체통에 편지를 보내서 받는 개념이다. 

그렇게 때문에 양방향성의 통신이 아니라 단방향 통신이다.



Process A <--------  A Mail slot    <------------

Process B <--------  A Mail slot    <------------            Sender Process

Process C <--------  A Mail slot    <------------


현실에서는 우체통 마다 다른 주소를 같지만 윈도우즈에서는 메일슬롯의 주소가 같을 수 도 있고 또한 데이터를 보내는 프로세스가 한번의 보내기를 통하여 각각의 프로세스에 모두 전달 할 수 있다. 




Receiver 가 우체통을 생성하면 Sender 는 우체통에 접근하기위하여 연결을 하기 위해 어떻한 함수를 호출하는데 이런 연결과정을 거친후에 데이터를 전송 할 수 있다.

위에서 한번 나왔듯이 Receiver에서의 데이터 전송은 가능하지 않다.


잘 보면 여기서 사용하는 함수들이 파일 관련함수들이다. 

 A 가 파일을 생성하여 데이터 작성 --> B가 파일을 읽어서 데이터 받음

이런식의 파일을 이용한 단순하고 불안정한 통신이 가능한데 

실제로 메일슬롯 방식이 이러한 방식을 기반으로 만들어졌기 때문에  파일관련함수를 사용한다.



예제



메일슬롯 Receiver



#include <windows.h>

#include <tchar.h>

#include <stdio.h>


#define SLOT_NAME    _T("\\\\.\\mailslot\\mailbox")//이곳에 컴퓨터 이름이 들어간다.

/*이 주소를 이용하여 분리된 컴퓨터간에서 프로세스간 통신가능 하지만 실제로는 거의 이용되지 않는다*/

int _tmain(int argc, LPTSTR argv[])

{

HANDLE hMailSlot;  //mailslot 핸들

TCHAR messageBox[50];

  DWORD bytesRead;  // number of bytes read


/* mailslot 생성 */

hMailSlot=CreateMailslot(

SLOT_NAME, 

0, //메일슬롯 버퍼크기

MAILSLOT_WAIT_FOREVER,

NULL);

if(hMailSlot==INVALID_HANDLE_VALUE)

{

_fputts(_T("Unable to create mailslot!\n"), stdout);

return 1;

}


/* Message 수신 */

_fputts(_T("******** Message ********\n"), stdout);

while(1)

{

if(!ReadFile(hMailSlot, messageBox, sizeof(TCHAR)*50, &bytesRead, NULL))

{

_fputts(_T("Unable to read!"), stdout);

CloseHandle(hMailSlot);

return 1;

}


if(!_tcsncmp(messageBox, _T("exit"), 4))

{

_fputts(_T("Good Bye!"), stdout);

break;

}


messageBox[bytesRead/sizeof(TCHAR)]=0; //NULL 문자 삽입.

_fputts(messageBox, stdout);

}


CloseHandle(hMailSlot);

return 0;

}


-----------------------------------------------------------------------------------



 메일슬롯 Sender


#include <windows.h>

#include <tchar.h>

#include <stdio.h>


#define SLOT_NAME _T("\\\\.\\mailslot\\mailbox")


int _tmain(int argc, LPTSTR argv[])

{

HANDLE hMailSlot;  //mailslot 핸들

TCHAR message[50];

    DWORD bytesWritten;  // number of bytes write


hMailSlot=CreateFile(SLOT_NAME, GENERIC_WRITE, FILE_SHARE_READ, NULL,

OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if(hMailSlot==INVALID_HANDLE_VALUE)

{

_fputts(_T("Unable to create mailslot!\n"), stdout);

return 1;

}


while(1)

{

_fputts(_T("MY CMD>"), stdout);

_fgetts(message, sizeof(message)/sizeof(TCHAR), stdin);


if(!WriteFile(hMailSlot, message, _tcslen(message)*sizeof(TCHAR), &bytesWritten, NULL))

{

_fputts(_T("Unable to write!"), stdout);

CloseHandle(hMailSlot);

return 1;

}


if(!_tcscmp(message, _T("exit")))

{

_fputts(_T("Good Bye!"), stdout);

break;

}


}


CloseHandle(hMailSlot);

return 0;

}










커널 오브젝트와 핸들의 관계



프로세스가 생성될때 커널 오브젝트가 생성된다. 또한 그 프로세스의 핸들 테이블도 생성된다. 

핸들 테이블에 핸들 값이 있고 그 핸들값이 가리키는 대상 즉 커널 오브젝트가 저장된다. 

※핸들 테이블은 각 프로세스 영역에서만 의미를 지니기 때문에 가리키는대상이 같아도 핸들값이 다를 수 있다.


커널 오브젝트에는 그 커널오브젝트에 접근가능한 사용자의 수를 카운트하는 값이 존재한다. 만약 프로세스A가 생성된다면 그 값은 2가될겄이다. 하나는 자기자신일 것이도 다른 하나는 프로세스A를 실행시킨 그 무엇(마우스로 실행시키면 탐색기가 될수도있고 명령프롬프트가 될 수도 있다.)이 될 것이다.

자식 프로세스는 실행될때 부모 프로세스로 자신의 핸들값을 집어던진다.


프로세스가 소멸 될지라도 커널 오브젝트는 사라지지 않는다. 

다른 프로세스가 그 커널 프로세스를 참조 할 수도 있기때문이다.

하지만 이미 그 프로세스가 소멸 했는데 다른 프로세스가 참조한다해도 이미 할 수있는 일은 아무것도 없지않을까 ?

여기서 잠깐 C언어 프로그래밍을 보면 return 0,1,-1 또는 exit(-1),exit(1) 이런 식으로 종료되는 경우가 많다. 어차피 뭘 넣던 종료는 된다.

하지만 

뒤에 오는 값은 종료되는 프로세스의 부모프로세스에게 자신이 정상 종료인지 비정상 종료인지 알려주는 것이다.

부모 프로세스와 자식 프로세스가 완전 별도의 프로세스 일 수도 있지만 자식 프로세스의 결과에따라 부모 프로세스의 실행을 결정 할 수도 있기에 자식 프로세스가 정상적 종료인지 아닌지 궁금해 할 수도 있다.


프로세스가 종료될때 종료코드값를 리턴하는데 어디로 하느냐 바로 커널 오브젝트이다.  그래서 프로세스가 종료되어도 커널 오브젝트는 남아있는다.

그 커널 오브젝에 아무도 관심이 없어진 상태가 된야만 운영체제가 그 커널 오브젝트를 소멸 시킨다.



파일에 접근 할 때 ANSI 함수를 사용 하거나 시스템 함수를 사용 할 수 있다.

ANSI 함수를 사용하면 파일의 커널오브젝트가 생성되지 않는다고 알고있는 사람도 있다고하는데 ANSI 함수는 각 OS에 시스템 함수를 호출하는 방법으로 사용된다. 

즉 파일이 생성되면 반드시 커널 오브젝트는 생성된다.


프로세스가 자기 자신의 커널 오브젝트에 접근 할때는 핸들 테이블에 등록되어있는 값을 사용하는것이 아니라 자기자신을 의미하는 상수값을 반환받는다.



예제


#include <stdio.h>

#include <windows.h>

#include <tchar.h>


int _tmain(int argc, TCHAR* argv[])

{

STARTUPINFO si={0,};

PROCESS_INFORMATION pi;

si.cb=sizeof(si);


TCHAR command[]=_T("Operation2.exe");


CreateProcess(NULL,     // 프로세스 생성.

     command,

 NULL,

 NULL, 

 TRUE, 

 0, 

 NULL, 

 NULL, 

 &si, 

 &pi

);  //CreateProcess


while(1)

{

for(DWORD i=0; i<10000; i++)

for(DWORD i=0; i<10000; i++); //Busy Waiting!!

_fputts(_T("Operation1.exe \n"), stdout); 

timing+=1;

if(timing==2)

 SetPriorityClass(pi.hProcess,

 NORMAL_PRIORITY_CLASS);   //우선순위를 다시 낮춘다.


}


return 0;

}


--------------------------------------------------------------------------------

Operation2.exe


#include <stdio.h>

#include <windows.h>

#include <tchar.h>


int _tmain(int argc, TCHAR* argv[])

{

SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);


while(1)

{

for(DWORD i=0; i<10000; i++)

for(DWORD i=0; i<10000; i++); //Busy Waiting!!


_fputts(_T("Operation2.exe \n"), stdout); 

}


return 0;

}


저번 예제와 거의 같다. 달라진 점은 프로세스를 실행시키면 자식프로세스인 operation.exe는 자신의 우선순위를 높인다. 그후에 부모프로세스가 우선순위를 다시 일반적 우선순위로 바꾼다. 

프로세스를 실행시킬때 PROCESS_INFORMATION pi; 구조체를 전달해주는데 

pi 구조체의 pi.hProcess를 통해 해당 프로세스를 통해 접근하게 된다.





커널 오브젝트(Kernel Object)에 대한 이해


커널오브젝트(Kernel Object)

커널에 의해 관리되는 리소스 정보를 담고 잇는 데이터 블록



         Kernel Object                                                 Resource

 

파이프 커널 오브젝트


프로세스 커널 오브젝트


쓰레드 커널 오브젝트


<---------------->





<---------------->





<---------------->

 파이프 


프로세스


쓰레드




커널은 쉽게 말해서 OS의 핵심 이다.

파일의 생성,접근,삭제등을 할때 우리가집접하는 것이 아니라 우리가 OS에 명령을 내리면 OS가 이러저러한 잡업을 해준다.

이런 리소스를 컨트롤하기 위해서 필요한 정보를 담고있는 것이 커널오브젝트이다.


프로세스 기반 커널 오브젝트



CreateProcess 함수를 호출한다고 한다면 OS에 요청을 하면 OS는 프로세스만 생성하는 것이아니라 프로세스의 정보와 프로세스의관리에 필요한 정보들로 이루어진 커널 오브젝트를 생성한다.

OS에는 커널 오브젝트를 관리하기위한 구조체가 존재한다.




커널 오브젝트와 핸들의 관계


         Handle                                    Kernel Object                         Resource

 

파이프 핸들


프로세스 핸들


쓰레드 핸들




<----->





<----->




 

<----->


 

 

파이프 커널 오브젝트


프로세스 커널 오브젝트


쓰레드 커널 오브젝트





<--->





<--->




 

<--->


  

 

파이프


프로세스


쓰레드



커널 오브젝트에는 여러가지 정보가 담겨있지만 커널 오브젝트에는 직접접근이 허용되지 않는다.

그렇기에 커널 오브젝트에 정보를 바꿀 수 있게 하기위해 관련된 시스템 함수를 제공한다.

이러한 함수를 사용하기위해서 커널 오브젝트를 지정해야하는데 직접접근 할 수 있는 어떻한 정보도 얻지못한다  이러한 관계로 

OS는 커널 오브젝트를 만들때 각각 고유한 번호를 매긴다 이를 핸들(Handle)이라한다. 핸들은 우리가 반환받을 수있다.


단순하게 보면

커널 오브젝트는 구조체 변수이고 핸들은 숫자 일 뿐이다.

핸들을 얻는 방법은 리소스 별로 각각다르기 때문에 일일이 알아 두어야 한다.


※커널 오브젝트마다 고유 핸들 값이있는것은아니다.

자세한 내용은 다음장에 계속...





밑의 예제는 커널 오브젝트에 접근하여 프로세스의 우선순위를 변경하는 예제이다.


예제6-1]


#include <stdio.h>

#include <windows.h>

#include <tchar.h>


int _tmain(int argc, TCHAR* argv[])

{

STARTUPINFO si={0,};

PROCESS_INFORMATION pi;

si.cb=sizeof(si);


TCHAR command[]=_T("Operation2.exe");


CreateProcess(NULL,     // 프로세스 생성.

     command,

 NULL,

 NULL, 

 TRUE, 

 0, 

 NULL, 

 NULL, 

 &si, 

 &pi

);  //CreateProcess


while(1)

{

for(DWORD i=0; i<10000; i++)

for(DWORD i=0; i<10000; i++); //Busy Waiting!!

_fputts(_T("Operation1.exe \n"), stdout); 

}


return 0;

}


예제6-2]


#include <stdio.h>

#include <windows.h>

#include <tchar.h>


int _tmain(int argc, TCHAR* argv[])

{

SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);

            //GetCurrentProcess함수는 프로세스자신의 핸들을 얻어오는 함수이다.


while(1)

{

for(DWORD i=0; i<10000; i++)

for(DWORD i=0; i<10000; i++); //Busy Waiting!!


_fputts(_T("Operation2.exe \n"), stdout); 

}


return 0;

}


6-1과 6-2모두 단순 출력 프로그램이다.

예제6-1은 예제 6-2를 생성시키는데  6-2는 자신의 우선순위를 올리는 함수를 호출한다.

Busy Waiting은 출력결과가 너무빠르기때문에 출력을 늦추기위함인데 

sleep함수도 있긴하지만 sleep함수로 프로세스를 늦추면 Running상태에서 Blocked상태로 된다. 

하지만 Busy Waiting을 시키면 프로세스가 Blocked상태가아니라 스케줄러에 의해서 

Ready 상태로 된다.

예제가 우선순위에 의한 프로세스의 실행량을 보기 위함이기 때문에 sleep함수보다

Busy Waiting을 사용해야한다.


실행결과 

Operation2.exe

OpOperation2.exe

Operation2.exe

Operation2.exe

Operation2.exe

Operation2.exe

Opraeration2.exe

Operation2.exe

Operation2.exe

Operation2.exe

Operation2.exe

Opertion1.eation2.exe

Operation2.exe

중략.....................

결과를 보면 예제6-1보다 확실히 예제6-2가 많이 실행된것을 알 수있다.
또한 함수호출이 완료되기 이전에도 우선순위가 넘어갈 수 있다.

우선순위의 기준은 함수 단위가아닌 명령어 단위로 이루어 지기때문이다.




프로그래밍을 통한 프로세스의 생성


프로세스 생성 함수



BOOL CreateProcess(

LPCTSTR lpApplicationName ,   //실행파일 이름 지정

LPTSTR lpCommandLine ,          /*매개변수 전달  (실행파일이름과 매개변수                            

                                                     정보를 묶어서 하나로 이곳에 표현 가능)*/

LPSECURITY_ATTRIBUTES lpProcessAttributes ,

LPSECURITY_ATTRIBUTES lpTreadAttributes ,

BOOL binheritHandles ,

DWORD dwCreationFlags ,

LPVOID lpEnvironment ,

LPCTSTR lpCurrentDirectory ,

LPSTARTUPINFO lpstrartupInfo ,

LPPROCESS_INFORMATION lpProcessInformation

);




LPSTARTUPINFO 

 정보전달

CreateProess 

정보반환

  LPPROCESS_INFORMATION 


※CreateProcess함수를 호출하기 위해서는

 LPSTARTUPINFO, LPPROCESS_INFORMATION 구조체를 선언해야한다.


LPSTARTUPINFO

이구조체를 통하여 생성하고자하는 프로세스 특성정보를 설정하여 CreateProcess에 인자로 전달한다.

 

LPPROCESS_INFORMATION 

CreateProcess함수가 생성된 프로세스정보를 LPPROCESS_INFORMATION 에 반환한다.




LPSTARTUPINFO


typedef struct _STARTUPINFO{

DWORD cb;                //구조체 변수의 크기

LPTSTR lpReserved;

LPTSTR lpDesktop;

LPTSTR lpTitle;          //콘솔 윈도우의 타이틀 바 제목

DWORD dwX;            //프로세스 윈도우의 x좌표

DWORD dwY;            //프로세스 윈도우의 y좌표  

DWORD dwXSize;     //프로세스 윈도우의 가로 길이

DWORD dwYSize;     //프로세스 윈도우의 세로 길이

DWORD dwXCountChars;

DWORD dwYCountChars;

DWORD dwFileAttribute;

DWORD dwFlags;    //설정된 멤버의 정보

WORD wShowWindow;

WORD cbReserved2;

LPBYTE lpReserved2;

HANDLE hStdInput;

HANDLE hStdOutput;

HANDLE hStdError;

} STARTUPINFO, *LPSTARTUPINFO;


LPPROCESS_INFORMATION


typedef struct _PROCESS_INFORMATION

{

HANDLE hProcess;        //프로세스의 핸들

HANDLE hThread;         //쓰레드 핸들

DWORD dwprocessId;    //프로세스의 ID

DWORD dwThreadId;      //쓰레드ID

} PROCESS_INFORMATION;

DWORD cb; 를 보면 구조체의 크기인데 왜 구조체가 정해져잇는데 구조체의 크기를 인자로 전달하는 걸까?

CreateProcess 함수를 보면 9번째 인자는 LPSTARTUPINFO로 확정되어있다.

그런데 나중에 다른인자를 올수있게 라이브러리를 개편한다면 전과의 호환성을 위해 구조체를 구별 할 수있게 전달하는 것이다.



예제 5-1]



AdderProcess 자식프로세스 단순덧셈프로세스

CreateProcess 부모프로세스 AdderProcess를 실행시키는 예제이다.


표준 검색경로

1 실행중인 프로세스의 실행파일이 존재하는 디렉터리

2 실행중인 프로세스의 현재 디렉터리(Current Directory)

3 Windows의 시스템디렉터리 (System Directory)

4 Windows 디렉터리 (Windows Directory)

5 환경변수 PATH에 의해 지정되어 있는 디렉터리




실행결과

 







프로세스와 스케줄러의 이해


프로세스의 이해

 

프로세스란?

메인 메모리로 이동하여 실행중인 프로그램 → 일반적인 정의

보통 프로세스라 하면 실행중인 프로그램 이다. 라고 표현을 한다.

틀린 표현은 아니다. 

그러나 메인메모리가 256메가 인데 실행파일의 크기가 1기가 라고 하면 과연 메모리에 올라가있는 코드만 프로세스인가? 

틀린건 아니지만 너무 애매한 표현이다.



프로세스의 구체적인 이해


프로세스의 범위

메모리 구조 + 레지스터 Set

프로세스 별 독립적인 대상은 프로세스의 범주에 포함시킬 수 있다.




 ↑

 Code 영역

명령어

(Instruction Code)

   

 ↑

Data 영역

 전역변수

static 변수

   

Heap 영역



Stack 영역

 프로그래머 할당

   

 



   

 지역변수

전달인자 정보

   
       

프로세스가 생성되면 위와같은 메모리가 할당 된다. 

프로그램이 실행된다라는 것은 레지스터에 프로그램 실행정보로 가득차게되는데

이런 레지스터 set과 메모리 구조 모두 프로세스 범위에 포함된다.


'프로세스를 구성하는 범주는 프로세스 별로 독립적으로 할당받는 리소스다.'

라고 할 수 있다.



프로세스 스케줄러


프로세스 스케줄러 기능

둘 이상의 프로세스가 적절히 실행되도록 컨트롤


스케줄링 방법

스케줄링 알고리즘에 따라 다양함.



스케줄러 라는 장치는 Windows 에서 지원해주는 소프트웨어적인 장치이다.

프로세스 스케줄러는 말그데로 프로세스를 스케줄링 해주는 것이다.

CPU는 하나인데 프로세스는 여러게를 실행 시키려하면 CPU는 하나의 프로세스만 실행시킬수 있기때문에 스케줄러가 프로세스의 순서를 정해주고  순서를 정하는 정책을 결정하고 관리해주는 것이다.


§스케줄러는 소프트웨어이기 때문에 스케줄러가 동작하는 순간에도 프로세스들은 동작하지 못한다. 넓은 관전에서 보면 스케줄러도 프로세스에 포함시킬 수 있다.






프로세스의 상태




프로세스는 Ready , Running, Blocked 같은 상태정보를 지닌다.

Running 상태는 CPU에 의해서 실행중인 상태로 CPU가 하나라면 Running 상태의 프로세스는 하나일 수 밖에 없다.

Blocked는 실행중이 아닌 상태.

Ready 상태는  Running 하고 싶은 상태로 C프로세스가  Running 상태이고

A와 B가 Ready 상태일때 C가 Blocked 상태로 간다고 하면 A와 B중 누가 Running

상태로 가느냐를 정해주는 것이 바로 스케줄러가 하는것이다.   

즉 Ready 상태에 있다는 것은 스케줄러가 선택해주기를 기다리는 상태이다.

그림에서 볼 수있듯이 모든 프로세스는 실행시키면 Ready 상태가 된다.

연산중에는 CPU에 의존적인 연산이 있고 그렇지 않은 연산있다.

그중에 I/O연산은 유독 CPU에 의존적이지 않기때문에 사칙연산과같은 기본적인 연산과 병행해 질수있다. 

만약에 Running 상태에 있는 C라는 프로세스가 I/O 연산시작 했을때 I/O 연산이 끝날때 까지 C는 쉬고있고 대신 다른 프로세스가  Running 상태가 되어도 된다.

그럼 C는 Ready 상태가 되야될까? Blocked 상태가 되야할까?

 Ready 상태가 된다고 생각해보면 C는 다시  Running 상태가 될것이고 I/O연산이 끝나지 않았다면 다시  Ready 상태가 되는 반복을 하기때문에 Blocked 상태가 된다.

그리고 다시 I/O연산이 끝났다면 Ready 상태로 돌아가게 된다.

 Running 상태에서 바로 Ready 상태가 되는것은 다른 프로세스들고 실행을 시키여하기때문에 연산이 끝나지않았어도 Ready 상태로 내리는 경우이다.


1. 모든 프로세스는 실행시키면 Ready 상태가 된다.

2. Ready 상태이면 스케줄러가   Running 상태로 만들어준다.

3. Running 상태에서 다른 프로세스를 실행시키기위해 어쩔수 없이 양보하는 경우

    Ready 상태로 내려간다.

4. 일반적으로 I/O 연산을 하고있는 프로세스들은 다른프로세스들에게 양보하기위        

   해서 Blocked 상태로 간다.

5. I/O 연산이 끝나면 프로세스를 다시 진행시키기위해  Ready 상태가 된다.

6. 프로세스 종료.




컨텍스트 스위칭(context switching)




ALU가 연산을 하기위해서 레지스터에 의존적이다.

레지스터는 Running 상태의 프로세스 정보로 가득차있다.

이때 Ready상태의 프로세스를 실행 시키려 할때 프로세스를 실행시킬 수 있는 

첫 번째 조건은 지금 레지스터레 채워져있는 데이터를 다른 저장영역에 저장시키고

 Ready상태의 프로세스를 위한 데이터를 레지스터에 복원해야한다.

이런 작업을 컨텍스트 스위칭(context switching)이라 한다.













Direct 모드와 Indirect 모드


Direct 모드                              Indirect 모드          

  LOAD r1, 0x10 실행시                LOAD r1, [0x10] 실행시              

 r0

 

 r0

 
 r1

 ←10     0x10번지

 r1

 0x30                  ↓                 0x10번지

 r2   r2

   ↖                   ↓

 r3

  r3

       ↖   10                            0x30번지

 r4     ir

 

 r4     ir

 
 r5     sp  

 r5     sp

 
 r6     lr  

 r6     lr

 

 r7     pc

 

 r7     pc

 


Direct 모드는 말그데로 메모리에 있는 값을 레지스트로 가져와라 이고

Indirect 모드는  메모리에 있는 값을 다시 메모리로 참조에서 값을 가져오는것이다.  0x10[0x30] → 0x30[10] → r1

※  LOAD r1, [0x10] 여기서 []는 Indirect 모드 라는 표시로 아무거나 써도 상관은없다.



Direct 모드의 문제점


 

 <-  LOAD  ->

 <destination>

<-                    source                   ->

 0

 0

 1

1

0

 0

1

 1

 0

 0

 0

1

 1

 1

   

int a = 10;      //0x0010 번지할당

int b = 20;     //0x0100 번지 할당

int c = 0;       //0x0020 번지 할당

c = a + b;

                                                                                      

LOAD r1, 0x0010 (구성가능)

LOAD r2, 0x0100 (구성불가)


8비트로 표현가능한 수는 0~255이다.

LOAD 명령어를 살펴보면 source 부분은 8비트 밖에 되지않는다.

0x0010같은 경우는 8비트로 표현이 가능하지만 0x0100은 딱봐다 8비트로는 표현이가능하지않는다. 

그럼 명령어로 구성 할 수없는 메모리 주소는 어떻게 사용할 것이가??



문제해결


int a = 10;      //0x0010 번지할당

int b = 20;     //0x0100 번지 할당

int c = 0;       //0x0020 번지 할당

c = a + b ;

명령어로 표현해보면 메모리 주소값문제로 Direct 모드로 표현 할 수없다는 것을 알고있다.

그래서 Indirect 로 풀어보면 


LOAD r1 , 0x0010


MUL r0, 4, 4

MUL r2, 4, 4
MUL r3, r0, r2


STORE r3, 0x0030

LOAD  r2, [0x0030]


ADD r3, r1, r2

 일단 r1에 a값을 넣는것은 그냥 LOAD명령어를 넣으면 된다 

하지만 b값은 메모리주소 0x0100 에 할당되어있어서 명령어로 표현이 불가능하다 그래서 MUL 명령어를 이용해보자.

r0에 4 곱하기 4 연산값을 넣고 r2에도 반복하자.

그리고 r3에 r0와 r2의 값을 곱한값을 넣으면 16*16 즉 256 , 16진수로 0100 이다. r3에보면 0x100이 들어가 있는상태이다.

이제 r3에 있는 값을 0x0030 번지에 값을 넣고 Indirect로 0x0030을 불러와서 r2에 넣으면 r2에 원하던 값을 넣을 수 있게 된다 .


     레지스터                                 메모리

 r0         16

 

 r1         10

   10                                 0x0010(a)

 r2         16

   0                                  0x0020(c)

 r3     265(0x0100)

   
 r4   ir

  0x0100                               0x0030

 r5   sp

  20                                      0x0100

 r6   lr  
 r7   pc  


그림으로 만들어 보면 이런 모양일 것이다.




LOAD & STORE 명령어 디자인


LOAD & STORE 명령어의 필요성


명령어의 제한

사칙연산의 피 연산자는 숫자 또는 레지스터.

연산결과는 레지스터에 저장.

레지스터를 통해서 모든 연산을 진행 해야 한다.




사칙연산 명령어 구성

 <-예약->

 <-  연산자  ->

 <-  저장소  ->

<-    피연산자1    ->

<-     피연산자2   ->

                

                      ADD                  r2                        r1                            7


저장소 : 레지스터

피연사자1,2:레지스터 or 숫자


챕터①에서 명령어의 구성을 살펴보면  연산결과를 레지스터에만 저장 할 수있도록 제한이 되었다. CPU로 오가는 데이터는 모두 레지스터를 통하기 때문에 일단 레지스터에 저장을 해야한다. 

그럼 메모리를 목표로 할때 레지스터로 통해 나간다 치고 

명령어에 메모리주소를 넣으면 좋지 않을까? 

복잡한 명령어의 CISK에서는 가능하지만 RISK에서는 성능향상을 위해 어쩔수없다.

 


문제


 int a=10; //0x10 번지 할당

int b=20; //0x20 번지 할당

int c=0;  //0x3 0번지 할등


c=a+b;



그러면 !!

왼쪽과같이 메모리에 있는 연산자를 연산하려고한다. 하지만 연산자구성을 보면 피연산자는 레지스터 또는 숫자만 가능하도록 제한하였다.

그래서 메모리에 있는 값을 레지스터로 불러들이고 반대로 메모리에 연산결과를 저장하는 것을 위해

LOAD & STORE 명령어 가 필요한 것이다.








LOAD & STORE 명령어 디자인


LOAD 명령어


 <-예약->

 <-  LOAD  ->

 <destination>

<-                    source                   ->

  

 1

           

                                                                                          

destination : 데이터를 저장할 레지스터 정보        LOAD r1, 0x20

source : 데이터를 읽어올 메모리의 주소 정보



STORE 명령어


 <-예약->

 <-  STORE  ->

 <destination>

<-                    source                   ->

  

 1

1

           

 

source : 데이터를 읽어올 레지스터 정보             STORE r1, ox20

destination : 데이터를 저장할 메모리의 주소 정보


※명령어 2진코드 110, 111은 사칙연산에 겹치지않게 만든것이다.


문제해결

int a=10; //0x10 번지 할당

int b=20; //0x20 번지 할당

int c=0;  //0x3 0번지 할등

c=a+b;

LOAD r1 , 0x10

LOAD r2 , 0x20

ADD r3, r1, r2

STORE r3, 0x30

 r0

 메모리

 r1

←a    10                                           0x10

 r2

 

 r3        

↖b    20                                           0x20

 r4       ir

 

 r5      sp

 
 r6      lr   c    30                                           0x30
  r7     pc 

ㄴㅇㅁㄴㅇㅁㄴㅇㅁㄴㅇ


 c=a+b를 이러한 명령어 조합으로 만들어 볼수있다. 

메모리에 있는 값을 LOAD명령어로 불러들여온후 ADD 명령어로 연산을 하면 사칙연산에서의 피연사자 문제를 해결 할 수있다.




+ Recent posts