- Windows Processes and Threads
- Process Creation
- 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
- Example: Using 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"2 with a similar set of commands. The commands are as follows.
- jobbg uses the remaining part of the command line as the command 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. Using a new console avoids console contention with jobbg and other jobs. 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 operations such as suspending and resuming existing jobs.
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. You could place this information in the registry rather than in a temporary file (see Exercise 6–9).
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. Also, Exercise 6–8 identifies a defect caused by job id reuse and suggests a fix.
The full program in the Examples file 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 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 Programs 6-4, 6-5, and 6-6. Run 6-6 then demonstrates how to use the JobShell system.
Program 6-4. JobMgt: Creating New Job Information
/* Job management utility function. */ #include "Everything.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, fileSizeLow, fileSizeHigh; 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; /* Find file size: GetFileSizeEx is an alternative */ fileSizeLow = GetFileSize (hJobData, &fileSizeHigh); LockFileEx (hJobData, LOCKFILE_EXCLUSIVE_LOCK, 0, fileSizeLow + SJM_JOB, 0, ®ionStart); __try { /* Read records to find empty slot. */ /* See text comments and Exercise 6-8 regarding a potential defect (and fix) caused by process ID reuse. */ 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, fileSizeLow + SJM_JOB, 0, ®ionStart); CloseHandle (hJobData); } return jobNumber + 1; }
Program 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, fileSizeLow, fileSizeHigh; 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; /* Demonstration: GetFileSize instead of GetFileSizeEx */ fileSizeLow = GetFileSize (hJobData, &fileSizeHigh); LockFileEx (hJobData, LOCKFILE_EXCLUSIVE_LOCK, 0, fileSizeLow, fileSizeHigh, ®ionStart); __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, fileSizeLow, fileSizeHigh, ®ionStart); CloseHandle (hJobData); } return TRUE; }
Program 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, ®ionStart); ReadFile (hJobData, &jobRecord, SJM_JOB, &nXfer, NULL); UnlockFileEx (hJobData, 0, SJM_JOB, 0, ®ionStart); CloseHandle (hJobData); return jobRecord.ProcessId; }
Run 6-6 JobShell: Managing Multiple Processes
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.
There's also a subtle defect in this function; a code comment identifies it, and Exercise 6–8 suggests a fix.
Listing Background Jobs
Program 6-5 shows the DisplayJobs job management function.
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.
Run 6-6 shows the job shell managing several jobs using grep, grepMP, and sortBT (Chapter 5). Notes on Run 6-6 include:
- This run uses the same four 640MB files (l1.txt, etc.) as Run 6-1.
- You can quit and reenter JobShell and see the same jobs.
- A "Done" job is listed only once.
- The grep job uses the -c option, so the results appear in a separate console (not shown in the screenshot).
- JobShell and the grepMP job contend for the main console, so some output can overlap, although the problem does not occur in this example.
Job Objects
You can collect processes together into job objects where the processes can be controlled together, and you can specify resource limits for all the job object member processes and maintain accounting information.
The first step is to create an empty job object with CreateJobObject, which takes two arguments, a name and security attributes, and returns a job object handle. There is also an OpenJobObject function to use with a named object. CloseHandle destroys the job object.
AssignProcessToJobObject simply adds a process specified by a process handle to a job object; there are just two parameters. A process cannot be a member of more than one job, so AssignProcessToJobObject fails if the process associated with the handle is already a member of some job. A process that is added to a job inherits all the limits associated with the job and adds its accounting information to the job, such as the processor time used.
By default, a new child process created by a process in the job will also belong to the job unless the CREATE_BREAKAWAY_FROM_JOB flag is specified in the dwCreationFlags argument to CreateProcess.
Finally, you can specify control limits on the processes in a job using SetInformationJobObject.
BOOL SetInformationJobObject ( HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass, LPVOID lpJobObjectInformation, DWORD cbJobObjectInformationLength) |
- hJob is a handle for an existing job object.
- JobObjectInformationClass specifies the information class for the limits you wish to set. There are five values; JobObjectBasicLimitInformation is one value and is used to specify information such as the total and perprocess time limits, working set size limits,3 limits on the number of active processes, priority, and processor affinity (the processors of a multiprocessor computer that can be used by threads in the job processes).
- lpJobObjectInformation points to the actual information required by the preceding parameter. There is a different structure for each class.
- JOBOBJECT_BASIC_ACCOUNTING_INFORMATION allows you to get the total time (user, kernel, and elapsed) of the processes in a job.
- JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE will terminate all processes in the job object when you close the last handle referring to the object.
- The last parameter is the length of the preceding structure.
QueryJobInformationObject obtains the current limits. Other information classes impose limits on the user interface, I/O completion ports (see Chapter 14), security, and job termination.