- Windows Processes and Threads
- Process Creation
- Process Handle Counts
- Process Identities
- Duplicating Handles
- Exiting and Terminating a Process
- Waiting for a Process to Terminate
- Environment Blocks and Strings
- Example: Parallel Pattern Searching
- Processes in a Multiprocessor Environment
- Process Execution Times
- Example: Process Execution Times
- Generating Console Control Events
- Example: Simple Job Management
- Job Objects
- Summary
- Exercises
Example: Simple Job Management
UNIX shells provide commands to execute processes in the background and to obtain their current status. This section develops a simple "job shell" with a similar set of commands. The commands are as follows.
-
jobbg uses the remaining part of the command line as the command line for a new process, or job, but the jobbg command returns immediately rather than waiting for the new process to complete. The new process is optionally given its own console, or is detached, so that it has no console at all. This approach is similar to running a UNIX command with the & option at the end.
-
jobs lists the current active jobs, giving the job numbers and process IDs. This is similar to the UNIX command of the same name.
-
kill terminates a job. This implementation uses the TerminateProcess function, which, as previously stated, does not provide a clean shutdown. There is also an option to send a console control signal.
It is straightforward to create additional commands for suspending existing jobs or moving them to the foreground.
Because the shell, which maintains the job list, may terminate, the shell employs a user-specific shared file to contain the process IDs, the command, and related information. In this way, the shell can restart and the job list will still be intact. Furthermore, several shells can run concurrently. An exercise places this information in the registry, rather than in a temporary file.
Concurrency issues will arise. Several processes, running from separate command prompts, might perform job control simultaneously. The job management functions use file locking (Chapter 3) on the job list file so that a user can invoke job management from separate shells or processes.
The full program on the book's Web site has a number of additional features, not shown in the listings, such as the ability to take command input from a file. JobShell will be the basis for a more general "service shell" in Chapter 13 (Program 13-3). Windows NT services are background processes, usually servers, that can be controlled with start, stop, pause, and other commands.
Creating a Background Job
Program 6-3 is the job shell that prompts the user for one of three commands and then carries out the command. This program uses a collection of job management functions, which are shown in Program 6-4, 6-5, and 6-6.
Example 6-3. JobShell: Create, List, and Kill Background Jobs
/* Chapter 6. */ /* JobShell.c -- job management commands: jobbg -- Run a job in the background. jobs -- List all background jobs. kill -- Terminate a specified job of job family. There is an option to generate a console control signal. */ #include "EvryThng.h" #include "JobMgt.h" int _tmain (int argc, LPTSTR argv []) { BOOL Exit = FALSE; TCHAR Command [MAX_COMMAND_LINE + 10], *pc; DWORD i, LocArgc; /* Local argc. */ TCHAR argstr [MAX_ARG] [MAX_COMMAND_LINE]; LPTSTR pArgs [MAX_ARG]; for (i = 0; i < MAX_ARG; i++) pArgs [i] = argstr [i]; /* Prompt user, read command, and execute it. */ _tprintf (_T ("Windows Job Management\n")); while (!Exit) { _tprintf (_T ("%s"), _T ("JM$")); _fgetts (Command, MAX_COMMAND_LINE, stdin); pc = strchr (Command, '\n'); *pc = '\0'; /* Parse the input to obtain command line for new job. */ GetArgs (Command, &LocArgc, pArgs); /* See Appendix A. */ CharLower (argstr [0]); if (_tcscmp (argstr [0], _T ("jobbg")) == 0) { Jobbg (LocArgc, pArgs, Command); } else if (_tcscmp (argstr [0], _T ("jobs")) == 0) { Jobs (LocArgc, pArgs, Command); } else if (_tcscmp (argstr [0], _T ("kill")) == 0) { Kill (LocArgc, pArgs, Command); } else if (_tcscmp (argstr [0], _T ("quit")) == 0) { Exit = TRUE; } else _tprintf (_T ("Illegal command. Try again\n")); } return 0; } /* jobbg [options] command-line [Options are mutually exclusive] -c: Give the new process a console. -d: The new process is detached, with no console. If neither is set, the process shares console with jobbg. */ int Jobbg (int argc, LPTSTR argv [], LPTSTR Command) { DWORD fCreate; LONG JobNo; BOOL Flags [2]; STARTUPINFO StartUp; PROCESS_INFORMATION ProcessInfo; LPTSTR targv = SkipArg (Command); GetStartupInfo (&StartUp); Options (argc, argv, _T ("cd"), &Flags [0], &Flags [1], NULL); /* Skip over the option field as well, if it exists. */ if (argv [1] [0] == '-') targv = SkipArg (targv); fCreate = Flags [0] ? CREATE_NEW_CONSOLE : Flags [1] ? DETACHED_PROCESS : 0; /* Create job/thread suspended. Resume once job entered. */ CreateProcess (NULL, targv, NULL, NULL, TRUE, fCreate | CREATE_SUSPENDED | CREATE_NEW_PROCESS_GROUP, NULL, NULL, &StartUp, &ProcessInfo); /* Create a job number and enter the process ID and handle into the job "data base." */ JobNo = GetJobNumber (&ProcessInfo, targv); /* See "JobMgt.h" */ if (JobNo >= 0) ResumeThread (ProcessInfo.hThread); else { TerminateProcess (ProcessInfo.hProcess, 3); CloseHandle (ProcessInfo.hProcess); ReportError (_T ("Error: No room in job list."), 0, FALSE); return 5; } CloseHandle (ProcessInfo.hThread); CloseHandle (ProcessInfo.hProcess); _tprintf (_T (" [%d] %d\n"), JobNo, ProcessInfo.dwProcessId); return 0; } /* jobs: List all running or stopped jobs. */ int Jobs (int argc, LPTSTR argv [], LPTSTR Command) { if (!DisplayJobs ()) return 1; /* See job mgmt functions. */ return 0; } /* kill [options] JobNumber -b Generate a Ctrl-Break -c Generate a Ctrl-C Otherwise, terminate the process. */ int Kill (int argc, LPTSTR argv [], LPTSTR Command) { DWORD ProcessId, JobNumber, iJobNo; HANDLE hProcess; BOOL CntrlC, CntrlB, Killed; iJobNo = Options (argc, argv, _T ("bc"), &CntrlB, &CntrlC, NULL); /* Find the process ID associated with this job. */ JobNumber = _ttoi (argv [iJobNo]); ProcessId = FindProcessId (JobNumber); /* See job mgmt. */ hProcess = OpenProcess (PROCESS_ALL_ACCESS, FALSE, ProcessId); if (hProcess == NULL) { /* Process ID may not be in use. */ ReportError (_T ("Process already terminated.\n"), 0, FALSE); return 2; } if (CntrlB) GenerateConsoleCtrlEvent (CTRL_BREAK_EVENT, ProcessId); else if (CntrlC) GenerateConsoleCtrlEvent (CTRL_C_EVENT, ProcessId); else TerminateProcess (hProcess, JM_EXIT_CODE); WaitForSingleObject (hProcess, 5000); CloseHandle (hProcess); _tprintf (_T ("Job [%d] terminated or timed out\n"), JobNumber); return 0; }
Notice how the jobbg command creates the process in the suspended state and then calls the job management function, GetJobNumber (Program 6-4), to get a new job number and to register the job and its associated process. If the job cannot be registered for any reason, the job's process is terminated immediately. Normally, the job number is generated correctly, and the primary thread is resumed and allowed to run.
Getting a Job Number
The next three programs show three individual job management functions. These functions are all included in a single source file, JobMgt.c.
The first, Program 6-4, shows the GetJobNumber function. Notice the use of file locking with a completion handler to unlock the file. This technique protects against exceptions and inadvertent transfers around the unlock call. Such a transfer might be inserted accidentally during code maintenance even if the original program is correct. Also notice how the record past the end of the file is locked in the event that the file needs to be expanded with a new record.
Example 6-4. JobMgt: Creating New Job Information
/* Job management utility function. */ #include "EvryThng.h" #include "JobMgt.h" /* Listed in Appendix A. */ void GetJobMgtFileName (LPTSTR); LONG GetJobNumber (PROCESS_INFORMATION *pProcessInfo, LPCTSTR Command) /* Create a job number for the new process, and enter the new process information into the job database. */ { HANDLE hJobData, hProcess; JM_JOB JobRecord; DWORD JobNumber = 0, nXfer, ExitCode, FsLow, FsHigh; TCHAR JobMgtFileName [MAX_PATH]; OVERLAPPED RegionStart; if (!GetJobMgtFileName (JobMgtFileName)) return -1; /* Produces "\tmp\UserName.JobMgt" */ hJobData = CreateFile (JobMgtFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hJobData == INVALID_HANDLE_VALUE) return -1; /* Lock the entire file plus one possible new record for exclusive access. */ RegionStart.Offset = 0; RegionStart.OffsetHigh = 0; RegionStart.hEvent = (HANDLE)0; FsLow = GetFileSize (hJobData, &FsHigh); LockFileEx (hJobData, LOCKFILE_EXCLUSIVE_LOCK, 0, FsLow + SJM_JOB, 0, &RegionStart); __try { /* Read records to find empty slot. */ while (ReadFile (hJobData, &JobRecord, SJM_JOB, &nXfer, NULL) && (nXfer > 0)) { if (JobRecord.ProcessId == 0) break; hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, JobRecord.ProcessId); if (hProcess == NULL) break; if (GetExitCodeProcess (hProcess, &ExitCode) && (ExitCode != STILL_ACTIVE)) break; JobNumber++; } /* Either an empty slot has been found, or we are at end of file and need to create a new one. */ if (nXfer != 0) /* Not at end of file. Back up. */ SetFilePointer (hJobData, -(LONG)SJM_JOB, NULL, FILE_CURRENT); JobRecord.ProcessId = pProcessInfo->dwProcessId; _tcsnccpy (JobRecord.CommandLine, Command, MAX_PATH); WriteFile (hJobData, &JobRecord, SJM_JOB, &nXfer, NULL); } /* End try. */ __finally { UnlockFileEx (hJobData, 0, FsLow + SJM_JOB, 0, &RegionStart); CloseHandle (hJobData); } return JobNumber + 1; }
Listing Background Jobs
Program 6-5 shows the DisplayJobs job management function.
Example 6-5. JobMgt: Displaying Active Jobs
BOOL DisplayJobs (void) /* Scan the job database file, reporting job status. */ { HANDLE hJobData, hProcess; JM_JOB JobRecord; DWORD JobNumber = 0, nXfer, ExitCode, FsLow, FsHigh; TCHAR JobMgtFileName [MAX_PATH]; OVERLAPPED RegionStart; GetJobMgtFileName (JobMgtFileName); hJobData = CreateFile (JobMgtFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); RegionStart.Offset = 0; RegionStart.OffsetHigh = 0; RegionStart.hEvent = (HANDLE)0; FsLow = GetFileSize (hJobData, &FsHigh); LockFileEx (hJobData, LOCKFILE_EXCLUSIVE_LOCK, 0, FsLow, FsHigh, &RegionStart); __try { while (ReadFile (hJobData, &JobRecord, SJM_JOB, &nXfer, NULL) && (nXfer > 0)){ JobNumber++; if (JobRecord.ProcessId == 0) continue; hProcess = OpenProcess (PROCESS_ALL_ACCESS, FALSE, JobRecord.ProcessId); if (hProcess != NULL) GetExitCodeProcess (hProcess, &ExitCode); _tprintf (_T (" [%d] "), JobNumber); if (hProcess == NULL) _tprintf (_T (" Done")); else if (ExitCode != STILL_ACTIVE) _tprintf (_T ("+ Done")); else _tprintf (_T (" ")); _tprintf (_T (" %s\n"), JobRecord.CommandLine); /* Remove processes that are no longer in system. */ if (hProcess == NULL) { /* Back up one record. */ SetFilePointer (hJobData, -(LONG)nXfer, NULL, FILE_CURRENT); JobRecord.ProcessId = 0; WriteFile (hJobData, &JobRecord, SJM_JOB, &nXfer, NULL); } } /* End of while. */ } /* End of __try. */ __finally { UnlockFileEx (hJobData, 0, FsLow, FsHigh, &RegionStart); CloseHandle (hJobData); } return TRUE; }
Finding a Job in the Job List File
Program 6-6 shows the final job management function, FindProcessId, which obtains the process ID of a specified job number. The process ID, in turn, can be used by the calling program to obtain a handle and other process status information.
Example 6-6. JobMgt: Getting the Process ID from a Job Number
DWORD FindProcessId (DWORD JobNumber) /* Obtain the process ID of the specified job number. */ { HANDLE hJobData; JM_JOB JobRecord; DWORD nXfer; TCHAR JobMgtFileName [MAX_PATH]; OVERLAPPED RegionStart; /* Open the job management file. */ GetJobMgtFileName (JobMgtFileName); hJobData = CreateFile (JobMgtFileName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hJobData == INVALID_HANDLE_VALUE) return 0; /* Position to the entry for the specified job number. * The full program assures that JobNumber is in range. */ SetFilePointer (hJobData, SJM_JOB * (JobNumber - 1), NULL, FILE_BEGIN); /* Lock and read the record. */ RegionStart.Offset = SJM_JOB * (JobNumber - 1); RegionStart.OffsetHigh = 0; /* Assume a "short" file. */ RegionStart.hEvent = (HANDLE)0; LockFileEx (hJobData, 0, 0, SJM_JOB, 0, &RegionStart); ReadFile (hJobData, &JobRecord, SJM_JOB, &nXfer, NULL); UnlockFileEx (hJobData, 0, SJM_JOB, 0, &RegionStart); CloseHandle (hJobData); return JobRecord.ProcessId; }