Tutorial 14: Process

このチュートリアルでは、プロセスとは何かを説明し、プロセスの作成と終了の方法を説明する。
 ソース   リソーススクリプト   実行結果 

Preliminary:

プロセスとは何か?Win32APIレファレンスからこの定義を引用してみる。

プロセスというのは、そのプロセスだけが自由に使用できることの許された仮想メモリ空間、実行コード、データと、ファイルやパイプ、同期オブジェクトといったシステムリソースから成る実行アプリケーションのことである。

上の定義を見てわかるように、プロセスは「自分専用」のいくつかのオブジェクトがある。 メモリ空間、実行モジュールと実行モジュールが作成、オープンする実行モジュールといったものだ。 少なくとも、プロセスには実行モジュール、仮想メモリ領域と一つのスレッドがないといけない。
今度は、スレッドとは何?と思うだろう。スレッドとは実行されるキュー(待ち行列)である。 Windowsは、プロセスを生成するときに、1つのスレッドも同時に作成する。 たいていの場合、このスレッドからまず一番初めに実行される。 プロセスが2つ以上のスレッドが必要なら、明示的にスレッドを生成することができる。

Windowsがプロセスを作成するコマンドを受け取った場合、 そのプロセスに、固有のメモリ空間を割り当てる。 その後、そのプロセスのメインスレッドが作成される。
Win32では、CreateProcess関数をCALLすることにより、自分で作成するプログラムからでもプロセスを作成することができる。 CreateProcess関数は以下のようになっている。

CreateProcess proto lpApplicationName    :DWORD ,\
                    lpCommandLine        :DWORD ,\
                    lpProcessAttributes  :DWORD ,\
                    lpThreadAttributes   :DWORD ,\
                    bInheritHandles      :DWORD ,\
                    dwCreationFlags      :DWORD ,\
                    lpEnvironment        :DWORD ,\
                    lpCurrentDirectory   :DWORD ,\
                    lpStartupInfo        :DWORD ,\
                    lpProcessInformation :DWORD

ほとんどの引数は無視することができるので、引数の量に圧倒されることはない。

PROCESS_INFORMATION STRUCT
   hProcess         HANDLE ?           ; 子プロセスへのハンドル
   hThread          HANDLE ?           ; 子プロセスのメインスレッドへのハンドル
   dwProcessId      DWORD  ?           ; 子プロセスID
   dwThreadId       DWORD  ?           ; 子プロセスのメインスレッドのID
PROCESS_INFORMATION ENDS

プロセスハンドルとプロセスIDは別物で、プロセスIDというのはシステムで固有の値となっており、この値からプロセスを識別できるようになっている。 プロセスハンドルはAPI関数を使用するときに用いるもので、Windowsから返される値である。プロセスハンドルは唯一のものではないので、プロセスを識別できない。

CreateProcess関数をCALLし、新しいプロセスを作成したらすぐに制御を返してくる。 もし、その作成したプロセスがまだ動作しているかどうかをチェックしたければ、 GetExitCodeProcess関数を呼べばよい。この関数は以下のようなプロトタイプとなっている。

GetExitCodeProcess proto hProcess:DWORD, lpExitCode:DWORD

この関数が成功すれば引数のlpExitCodeに知りたい情報が入っている。 もしlpExitCodeがSTILL_ACTIVEなら、そのプロセスはまだ動作している。

強制的にプロセスを終了したければ、TerminateProcess関数をCALLする。この関数は以下のようになっている。

TerminateProcess proto hProcess:DWORD, uExitCode:DWORD

この関数で、指定したプロセスを終了させ、uExitCodeに終了コード記述できる。 察しのとおり、この関数で終了させるのは半ば強引な方法で、 プロセスに関係しているDLLに、プロセスの終了が通知できないため、あまりオススメできるものではない。

Example:

以下のサンプルはユーザが「create process」メニューアイテムを選択したときに、プロセスを新たに作成するものだ。 起動するプロセスは「msgbox.exe」という実行ファイルである。 もし作成したプロセスを終了したければ、「terminate process」メニューアイテムを選択すればよい。 その際、このプログラムはまず、新しいプロセスがすでに終了しているかどうかをチェックし、 もしまだ作成したプロセスが終了していなければ、TerminateProcess関数をCALLして、そのプロセスを終了させる。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_PROCESS equ 1
IDM_TERMINATE equ 2
IDM_EXIT equ 3

.data
ClassName db "Win32ASMProcessClass",0
AppName db "Win32 ASM Process Example",0
MenuName db "FirstMenu",0
processInfo PROCESS_INFORMATION <>
programname db "msgbox.exe",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HANDLE ?
ExitCode DWORD ?                   ; contains the process exitcode status from GetExitCodeProcess call.

.code
start:
       invoke GetModuleHandle, NULL
       mov   hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
   LOCAL wc:WNDCLASSEX
   LOCAL msg:MSG
   LOCAL hwnd:HWND
   mov  wc.cbSize,SIZEOF WNDCLASSEX
   mov  wc.style, CS_HREDRAW or CS_VREDRAW
   mov  wc.lpfnWndProc, OFFSET WndProc
   mov  wc.cbClsExtra,NULL
   mov  wc.cbWndExtra,NULL
   push hInst
   pop  wc.hInstance
   mov  wc.hbrBackground,COLOR_WINDOW+1
   mov  wc.lpszMenuName,OFFSET MenuName
   mov  wc.lpszClassName,OFFSET ClassName
   invoke LoadIcon,NULL,IDI_APPLICATION
   mov  wc.hIcon,eax
   mov  wc.hIconSm,eax
   invoke LoadCursor,NULL,IDC_ARROW
   mov  wc.hCursor,eax
   invoke RegisterClassEx, addr wc
   invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
          WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
          CW_USEDEFAULT,300,200,NULL,NULL,\
          hInst,NULL
   mov  hwnd,eax
   invoke ShowWindow, hwnd,SW_SHOWNORMAL
   invoke UpdateWindow, hwnd
   invoke GetMenu,hwnd
   mov hMenu,eax
   .WHILE TRUE
               invoke GetMessage, ADDR msg,NULL,0,0
               .BREAK .IF (!eax)
               invoke TranslateMessage, ADDR msg
               invoke DispatchMessage, ADDR msg
   .ENDW
   mov    eax,msg.wParam
   ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   LOCAL startInfo:STARTUPINFO
   .IF uMsg==WM_DESTROY
       invoke PostQuitMessage,NULL
   .ELSEIF uMsg==WM_INITMENUPOPUP
       invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
       .if eax==TRUE
           .if ExitCode==STILL_ACTIVE
               invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
               invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
           .else
               invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
               invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
           .endif
       .else
           invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
           invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
       .endif
   .ELSEIF uMsg==WM_COMMAND
       mov eax,wParam
       .if lParam==0
           .if ax==IDM_CREATE_PROCESS
               .if processInfo.hProcess!=0
                   invoke CloseHandle,processInfo.hProcess
                   mov processInfo.hProcess,0
               .endif
               invoke GetStartupInfo,ADDR startInfo
               invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
                                       NORMAL_PRIORITY_CLASS,\
                                       NULL,NULL,ADDR startInfo,ADDR processInfo
               invoke CloseHandle,processInfo.hThread
           .elseif ax==IDM_TERMINATE
               invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
               .if ExitCode==STILL_ACTIVE
                   invoke TerminateProcess,processInfo.hProcess,0
               .endif
               invoke CloseHandle,processInfo.hProcess
               mov processInfo.hProcess,0
           .else
               invoke DestroyWindow,hWnd
           .endif
       .endif
   .ELSE
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam
       ret
   .ENDIF
   xor   eax,eax
   ret
WndProc endp
end start

Analysis:

メインウィンドウを作成、将来使うことになるメニューハンドルを取得しておく。 そして、ユーザがメニューからコマンドを選択するのを待つ。 ユーザが「Process」メニューを選択したときに、WM_INITMENUPOPUPメッセージを受け取るので、 ポップアップメニューを表示する前に、 メニューアイテムを選択可能にしたりグレーアウトしたりといった処理をそこで行う。

.ELSEIF uMsg==WM_INITMENUPOPUP
    invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
    .if eax==TRUE
        .if ExitCode==STILL_ACTIVE
            invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
            invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
        .else
            invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
            invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
        .endif
    .else
        invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
        invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
    .endif

どうしてこのメッセージを処理するのだろうか?
それは、ユーザがポップアップメニューを目にする前に、ポップアップメニューの設定を行いたいからだ。 この例では、新しいプロセスが今動作中で無ければ、「start process」メニューアイテムが選択可能となっており、 「terminate process」メニューアイテムをグレーアウトにする。 作成したプロセスが動作中ならその逆となる。

CreateProcess関数でセットされるプロセスハンドルを引数にしてGetExitCodeProcess関数をCALLし、新しく作成したプロセスがまだ動作中かどうかチェックする。 GetExitCodeProcess関数がFALSEを返せばまだ新しいプロセスが作成されていないということなので、 「terminate process」をグレーアウトにし、一方、TRUEならプロセスが作成されていることがわかるが、さらにそのプロセスが稼動中かどうかをチェックしなければならない。 そのため、終了コードを検査しなければならず、STILL_ACTIVEなら、プロセスがまだ動作中ということなので、 「start process」メニューアイテムをグレーアウトにし、 いくつも同じプロセスを起動できないようにする。

.if ax==IDM_CREATE_PROCESS
    .if processInfo.hProcess!=0
        invoke CloseHandle,processInfo.hProcess
        mov processInfo.hProcess,0
    .endif
    invoke GetStartupInfo,ADDR startInfo
    invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
                            NORMAL_PRIORITY_CLASS,\
                            NULL,NULL,ADDR startInfo,ADDR processInfo
    invoke CloseHandle,processInfo.hThread

ユーザが「start process」メニューアイテムを選択したとき、 まず、PROCESS_INFORMATION構造体のメンバhProcessがすでに閉じられているかをチェックする。 これが一番初めなら、hProcessは常に0なので、.data セクションでPROCESS_INFORMATION構造体を定義する。 hProcessが0でなければ、作成したプロセスが終了したことを意味するが、 まだそのプロセスハンドルは閉じられていないので、このときに、プロセスハンドルを閉じる。

CreateProcess関数に渡すstartupinfo構造体に値をセットするためにGetStartupInfo関数をCALLする。 その後、新しいプロセスを作成するため、CreateProcess関数をCALLする。 この例ではCreateProcess関数の戻り値はチェックしていないことに気をつけなければならない。 本当は戻り値をチェックしなければならないのだが、かなり複雑になるため省略している。

CreateProcess関数をCALLした直後に、processInfo構造体のメンバhThread、つまり、メインスレッドハンドルを閉じているのは、 スレッドハンドルを参照する必要がないからだ。 スレッドハンドルを閉じたからといってプログラムが終了するわけではないので、 必要が無ければ、リソースの無駄使い、もしくはリークにもなりかねないので、閉じるようにしたほうがよい。

.elseif ax==IDM_TERMINATE
    invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
    .if ExitCode==STILL_ACTIVE
        invoke TerminateProcess,processInfo.hProcess,0
    .endif
    invoke CloseHandle,processInfo.hProcess
    mov processInfo.hProcess,0

ユーザが「terminate process」メニューアイテムを選択したとき、 GetExitCodeProcess関数をCALLして作成したプロセスがまだ動作中かどうかをチェックする。 動作中なら、TerminateProcess関数をCALLし終了させる。 そして、もう必要なくなったので、そのプロセスのハンドルも閉じる。


[戻る]