0% found this document useful (0 votes)
2 views24 pages

06

Chapter 6 discusses process and thread abstractions in operating systems, emphasizing the isolation of virtual address spaces and the resources owned by processes. It compares Windows and UNIX process/thread models, highlights the importance of concurrency and security, and provides examples of process creation APIs. The chapter concludes with best practices for handle management and process termination methods.

Uploaded by

Hoang
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views24 pages

06

Chapter 6 discusses process and thread abstractions in operating systems, emphasizing the isolation of virtual address spaces and the resources owned by processes. It compares Windows and UNIX process/thread models, highlights the importance of concurrency and security, and provides examples of process creation APIs. The chapter concludes with best practices for handle management and process termination methods.

Uploaded by

Hoang
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 24

Chap 6

1. The Process Abstraction

 Isolation of Virtual Address Space


o Author’s point: A process owns its own virtual address space, protecting its code
and data from other processes.
o Clarification: In modern systems this isolation is enforced by the MMU;
processes can still share memory explicitly (e.g., memory-mapped files, shared
memory segments), but each process typically maps that shared region to its
own virtual addresses.
 Resources Owned by a Process
The author lists the main OS-managed entities per process:
1. Thread(s): One or more threads of execution.
2. Virtual address space: Distinct mappings, even if backing physical pages may be
shared.
3. Code segments: Both the EXE’s code and any loaded DLLs/shared libraries.
4. Data segments: Global/static variables.
5. Environment strings: Environment variables (e.g. PATH).
6. Heaps: The process heap (on Windows via HeapAlloc/HeapCreate).
7. Open handles/resources: File handles, synchronization objects, additional heaps, etc.

Note: In current Windows versions, each process also has other resources like GDI objects,
kernel objects (tokens, jobs), and security descriptors that weren’t enumerated here.

2. The Thread as the Unit of Execution

 Author’s point: Threads are what the scheduler dispatches to CPUs. Threads within
one process share that process’s resources.
 Key thread-specific elements:
1. Stack: For local variables, call frames, exception handling.
2. Thread-Local Storage (TLS): Allows per-thread globals—on Windows via TLS
APIs; on POSIX via pthread_key_t.
3. Start argument: A single pointer or integer passed to the thread’s entry
function.
4. Kernel context: The saved register state when the thread is preempted.
 Multiprocessor Scheduling:
o Windows schedules threads across all available CPUs (SMP).

1
o On UNIX/Linux, the kernel’s scheduler does likewise; many implementations
allow thread-to-CPU affinity.

3. Windows vs. UNIX Processes and Threads

 Author’s comparison:
o A UNIX process ≃ Windows process.
o UNIX “threads” via POSIX Pthreads roughly mirror Windows threads, though
Windows offers a richer, highly granular API.
 Modern corrections:
o On Linux and modern UNIX-like OSes, lightweight processes (LWP) are often the
kernel-level threads that Pthreads maps onto.
o Windows’ thread API (e.g. CreateThread, SetThreadPriority, synchronization via
events/mutexes/semaphores) is broader than POSIX, but both offer the classic
primitives (mutex, condition variable, semaphore).

4. Why Processes and Threads Matter

 Concurrency and Parallelism:


o By spawning multiple processes or threads, applications can handle I/O,
computation, and network tasks concurrently.
o On multicore systems, true parallelism (multiple threads simultaneously
executing on different cores) can improve throughput.
 Security and Stability:
o Process-level isolation means a crash or corruption in one process usually
doesn’t take down others.
o Threads share memory, so a buffer overflow in one thread can corrupt another’s
stack or data—hence threads require careful synchronization.

5. Typical Process-Creation APIs (Modernized Examples)

Although the excerpt didn’t include code, here’s a corrected, C11-style sketch for spawning a
child process on Windows and on UNIX.

<details> <summary>Windows (using <code>CreateProcess</code>)</summary>


#include <windows.h>
2
#include <stdio.h>

int main(void) {
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;

BOOL ok = CreateProcessA(
"C:\\Windows\\System32\\notepad.exe", // Application name
NULL, // Command line (can be modified)
NULL, // Process security attributes
NULL, // Thread security attributes
FALSE, // Inherit handles?
0, // Creation flags
NULL, // Environment (inherit)
NULL, // Current directory
&si, // Startup info
&pi // Process info (output)
);

if (!ok) {
fprintf(stderr, "CreateProcess failed: %lu\n", GetLastError());
return 1;
}

// Optionally wait for the process to exit:


WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}

</details> <details> <summary>POSIX (using <code>fork</code> +


<code>execvp</code>)</summary>

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int main(void) {

3
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) {
// Child process: replace image with 'ls -l /'
char *argv[] = { "ls", "-l", "/", NULL };
execvp(argv[0], argv);
// Only reached on error:
perror("execvp");
exit(EXIT_FAILURE);
} else {
// Parent process: wait for child
int status;
waitpid(pid, &status, 0);
printf("Child exited with status %d\n", WEXITSTATUS(status));
}
return EXIT_SUCCESS;
}
</details>

In Your Own Work


 Use High-Level Wrappers When Available:
o In C++, consider std::thread (C++11) rather than raw OS APIs.
o In .NET or Java, the runtime provides even higher abstractions.
 Synchronize Carefully:
o Threads share data—always pair shared-data access with mutexes or lock-free
patterns.
o Processes isolate memory, but inter-process communication (pipes, sockets,
shared memory) still requires coordination.

Bottom Line:
The author rightly emphasizes the importance of isolating resources at the process level and
using threads for concurrent activities. Modern practice builds on these foundations but
often leans on higher-level abstractions (e.g. std::thread, thread pools) and more
standardized synchronization primitives (C11 atomics, POSIX/Rust channels, etc.).
Understanding these basics, however, remains essential whenever you need fine-grained
control or high performance.

-------------------------
4
6.2 Process Creation on Windows

1. The Core API: CreateProcess

 Author’s point:
o CreateProcess is the fundamental Windows call to spawn a new process (with
one primary thread).
o It returns both a process handle and a thread handle (via a
PROCESS_INFORMATION struct), not a single handle.
 Parameter breakdown & modern notes:
1. lpApplicationName vs. lpCommandLine
 If you supply lpApplicationName (wide or ANSI), it must be an exact path
(no search).
 Otherwise, Windows parses the first token of lpCommandLine and
searches in this order:
1. Parent’s executable directory
2. Current working directory
3. System directory (GetSystemDirectory)
4. Windows directory (GetWindowsDirectory)
5. Directories in PATH

Clarification: In modern code, it’s often simpler to pass a full path in lpApplicationName and
build lpCommandLine separately, to avoid quoting/parsing pitfalls.

2. Security attributes (lpProcessAttributes, lpThreadAttributes)


 NULL ⇒ default security descriptor, non-inheritable handle.
 To allow handle inheritance, supply a SECURITY_ATTRIBUTES with
bInheritHandle = TRUE.
3. bInheritHandles
 Acts as a “master switch.”
 If FALSE, even inheritable handles won’t be passed to the child.
4. dwCreationFlags
 CREATE_SUSPENDED: child’s primary thread won’t run until you call
ResumeThread.
 CREATE_NEW_CONSOLE vs. DETACHED_PROCESS: choose exactly one or
neither.
 CREATE_UNICODE_ENVIRONMENT: child’s env block is wide-char.
 CREATE_NEW_PROCESS_GROUP: groups for console signal broadcasting
(limited compared to UNIX).
 Priority class flags, e.g. NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS.
5. lpEnvironment
5
 NULL ⇒ inherit parent’s environment block.
 Otherwise point to a double-null-terminated block of VAR=VALUE\0
strings.
6. lpCurrentDirectory
 NULL ⇒ inherit parent’s CWD.
 Otherwise specify where the child will start.
7. lpStartupInfo
 Controls window appearance, standard I/O redirection, desktop, etc.
 To redirect std handles, zero out the struct, set dwFlags |=
STARTF_USESTDHANDLES, assign hStdInput/Output/Error, and ensure
those handles are inheritable.
8. lpProcessInformation

CopyEdit

typedef struct _PROCESS_INFORMATION {

HANDLE hProcess;

HANDLE hThread;

DWORD dwProcessId;

DWORD dwThreadId;

} PROCESS_INFORMATION;

 Always call CloseHandle on both hProcess and hThread when done.

2. Handles vs. IDs

 Author’s point:
o IDs (DWORD) are globally unique while the object exists, but have no rights
associated.
o Handles carry access rights and are required for handle-based APIs (e.g.,
WaitForSingleObject, TerminateProcess).
o A single object may have multiple handles with different access permissions.

3. No fork in Windows

6
 Author’s point:
o Windows lacks a direct fork; instead CreateProcess is analogous to UNIX’s fork +
exec.
o fork itself is ill-suited to multithreaded parents (it would have to replicate all
threads and synchronization state).
o Windows process groups are not the same as UNIX groups, but support console
control propagation.

6.3 Process Identities and OpenProcess

1. Getting Your Own Handle/ID

 HANDLE GetCurrentProcess(void)
o Returns a pseudo-handle (value -1), which you can use like a real handle but do
not close.
 DWORD GetCurrentProcessId(void)

2. Opening Another Process

CopyEdit

HANDLE OpenProcess(

DWORD dwDesiredAccess, // e.g., PROCESS_QUERY_INFORMATION | SYNCHRONIZE

BOOL bInheritHandle,

DWORD dwProcessId

);

 dwDesiredAccess flags of note:


o SYNCHRONIZE: allows WaitFor… on the process handle.
o PROCESS_TERMINATE: permits TerminateProcess.
o PROCESS_QUERY_INFORMATION: for GetExitCodeProcess, GetPriorityClass, etc.
o PROCESS_ALL_ACCESS: (use sparingly; requests every right).
 Modern best practice:
o Only request the minimal rights needed (principle of least privilege).

7
o Always check for NULL (failure) and call GetLastError() for diagnostics.

3. Retrieving Executable Path

 DWORD GetModuleFileNameA(NULL, buffer, buflen)


o With hModule = NULL, returns the path of the current EXE.
o With a valid HMODULE, returns the path for that DLL.

Code Snippet (Modern, C11-compatible)

Here’s a leaner, typo-free C11 sketch for launching a child process with I/O redirection:

CopyEdit
#include <windows.h>
#include <stdio.h>

int main(void) {
STARTUPINFO si = { .cb = sizeof(si), 0 };
PROCESS_INFORMATION pi = { 0 };
SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE };

// Create inheritable stdout.


HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetHandleInformation(hOut, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = hOut;
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);

BOOL ok = CreateProcessA(
"C:\\Windows\\System32\\ipconfig.exe", // app name
NULL, // command line
NULL, // proc sec attr
NULL, // thread sec attr
TRUE, // inherit handles
0, // creation flags

8
NULL, // env
NULL, // cwd
&si, &pi
);
if (!ok) {
fprintf(stderr, "Error %lu\n", GetLastError());
return 1;
}

// Wait for child to finish.


WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}

Note: Always call CloseHandle on any inheritable handle you no longer need.

Key Takeaways

1. CreateProcess is Windows’ one-stop for spawning a fresh process (no fork).


2. Understand each parameter—especially handle inheritance and startup info—so you
can redirect I/O or configure console behavior.
3. Differentiate IDs (for lightweight queries) vs. handles (for security-aware operations
and waiting).
4. Employ minimal access rights with OpenProcess and always clean up handles.

This foundation will support everything from simple child-process launches to sophisticated
orchestration of console apps, services, or GUI programs.

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

9
1. Duplicating Handles (Section 6.4)

 Why duplicate?
A child may need different access rights (or inheritability) than the original handle
provides. Likewise, you sometimes need a real handle (not the pseudohandle from
GetCurrentProcess) that another process can use.
 Core API

BOOL WINAPI DuplicateHandle(


HANDLE hSourceProcessHandle, // process owning the original
HANDLE hSourceHandle, // the handle to copy
HANDLE hTargetProcessHandle, // process that will receive the new handle
LPHANDLE lpTargetHandle, // out: receives the duplicate
DWORD dwDesiredAccess, // desired rights for new handle
BOOL bInheritHandle, // TRUE if child can inherit
DWORD dwOptions // flags (e.g., same access, close source)
);

o If you pass DUPLICATE_SAME_ACCESS in dwOptions, dwDesiredAccess is ignored


and the new handle gets exactly the same rights.
o With DUPLICATE_CLOSE_SOURCE, the original handle is closed; this keeps the
underlying object’s reference count unchanged.
 Under the hood
The kernel tracks a reference count per object (file, event, etc.). Each distinct handle—
whether inherited or duplicated—bumps that count. Only when all handles close does
the object finally get released.
 Best practice
Duplicate only what you need; avoid leaking handles in other processes. If you really
need cross-process notification of a new handle, you’ll also need to send its value (e.g.,
via IPC).

2. Exiting and Terminating a Process (Section 6.5)

 Graceful exit

VOID ExitProcess(UINT uExitCode);

10
o Terminates the calling process and all its threads immediately. DLLs still receive
their DLL_PROCESS_DETACH notifications, but structured-exception handlers
(__finally/__except) in your code will not run.
 Querying exit status

BOOL GetExitCodeProcess(
HANDLE hProcess, // must have PROCESS_QUERY_INFORMATION
LPDWORD lpExitCode // out: exit code or STILL_ACTIVE
);

A return of STILL_ACTIVE (value 259) means the process is still running.

 Forcing termination

BOOL TerminateProcess(
HANDLE hProcess,
UINT uExitCode
);

o Dangerous! The target process gets no chance to clean up—no SEH cleanup, no
C-runtime on-exit handlers, not even DLL detach calls.
o The author warns especially against using this on processes that share
resources (mutexes, semaphores).
 Cooperative shutdown
Rather than killing, consider sending a message or event and letting the target call
ExitProcess itself—this preserves orderly cleanup.
 UNIX comparison
o exit() in C maps roughly to ExitProcess.
o To kill another process, UNIX uses signals (e.g. kill(pid, SIGKILL)).
o There’s no direct equivalent of Windows process handles or
GetExitCodeProcess.

3. Waiting for Process Termination (Section 6.6)

 Unified waiting model


Windows lets you wait on any kernel object (processes, threads, mutexes, etc.) using:

c
11
CopyEdit
DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds);
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE *lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
);

o A process handle becomes signaled when that process ends.


o WAIT_OBJECT_0 + i (0 ≤ i < nCount) tells you which handle was signaled.
o WAIT_TIMEOUT indicates the timeout expired (you can poll by passing 0).
o INFINITE means “block until signaled.”
 Flexibility
You can wait for any one process to finish, all of them, or combine process-termination
events with other sync objects.
 UNIX contrast
o wait()/waitpid() only handle child processes, and have no built-in timeout or
multi-wait feature.
o They return the exit status directly, so there’s no separate “get exit code” call.

4. Environment Blocks and Strings (Section 6.7)

 Structure
The environment is a single block of consecutive, null-terminated NAME=VALUE
strings, ending in an extra \0. Internally it must be sorted alphabetically
(case-insensitive) to let the system look up variables efficiently.
 APIs

DWORD GetEnvironmentVariable(
LPCTSTR lpName,
LPTSTR lpValue,
DWORD cchValue
);
BOOL SetEnvironmentVariable(
LPCTSTR lpName,
LPCTSTR lpValue // if NULL, the variable is deleted
);

12
oIf your buffer is too small, GetEnvironmentVariable returns the size required.
o Passing NULL for the lpEnvironment parameter of CreateProcess inherits the
caller’s entire block.
 Best practice
Always check the return value and, if needed, reallocate your buffer. Remember that
neither setenv() nor putenv() on UNIX automatically sorts; that’s purely Windows’
internal requirement.

5. Process Security (brief note)

 By default, CreateProcess grants PROCESS_ALL_ACCESS to the new process handle, but


you can—and often should—restrict rights (e.g., remove PROCESS_TERMINATE) via a
customized SECURITY_ATTRIBUTES structure. This reduces the risk of accidental or
malicious termination by other processes.

My take:

 The author provides a clear, systematic tour of Windows process handling, contrasting
it with UNIX.
 In modern C (C11 and later), you’d still use these same Win32 APIs, but wrap them in
safer RAII-style classes (e.g., smart handles that call CloseHandle) and always check
return codes.
 Always prefer cooperative shutdown (events/messages) over TerminateProcess;
handle cleanup deterministically via C++ destructors or at least __try/__finally blocks.
 For environments, treat the block as immutable once your process starts; avoid
reshuffling large blocks at runtime—set only what you need.

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

13
6.8 Example: Parallel Pattern Searching (grepMP)

 Overview
The author builds a “master” program, grepMP, which forks one child process per
input file to run the standard grep-style search. Each child writes its matches to a
distinct temporary file via redirected STDOUT.
 Key Steps
1. Make handles inheritable:

CopyEdit

SECURITY_ATTRIBUTES sa = { .nLength = sizeof(sa),

.lpSecurityDescriptor = NULL,

.bInheritHandle = TRUE };

HANDLE hTemp = CreateFile(tempName, GENERIC_WRITE,

FILE_SHARE_READ | FILE_SHARE_WRITE,

&sa, CREATE_ALWAYS,

FILE_ATTRIBUTE_NORMAL, NULL);

2. Spawn child:
Populate a STARTUPINFO with hStdOutput = hTemp, then call
CreateProcess(NULL, cmd, …, TRUE, …).
3. Collect handles:
Store each child’s hProcess in an array.
4. Wait for all:
Loop through in batches of up to MAXIMUM_WAIT_OBJECTS using
WaitForMultipleObjects(…, TRUE, INFINITE).
5. Report results:
For each process whose exit code is zero (pattern found), spawn a cat (or print
directly) to dump the temp file, then delete it.
 Corrections & Modern Tips
o Always check return values of CreateFile, CreateProcess, and wait calls.
o Prefer GetTempFileNameA/GetTempFileNameW carefully: buffer sizes must
include the terminating NUL.
14
o Free resources promptly; consider wrapping handles in RAII wrappers (or use
C11 _cleanup_ tricks).

6.9 Processes in a Multiprocessor Environment

 Automatic Parallelism
The author notes that, with no extra coding, Windows will schedule each child’s thread
on a different CPU core—so grepMP on a 4-core box runs ~4× faster than sequential
grep (15 s vs. 77 s in his tests).
 Non-Linear Scaling
Overhead (process creation, I/O serialization) means you won’t get a perfect N-fold
speedup. But in embarrassingly parallel tasks (independent per-file searches), the
gains are worthwhile.
 Affinity Control
If you need to reserve cores for other work, you can set a process’s processor affinity
mask (SetProcessAffinityMask) so that only specific CPUs are used.
 Threads vs. Processes
The author foreshadows that you could also spawn multiple threads in one process;
Chapter 7 covers thread-level considerations. Threads share memory but still benefit
from multi-core scheduling.

6.10 Process Execution Times

 What You Can Measure


Using

CopyEdit

BOOL GetProcessTimes(

HANDLE hProcess,

LPFILETIME lpCreationTime,

LPFILETIME lpExitTime,

15
LPFILETIME lpKernelTime,

LPFILETIME lpUserTime

);

you can obtain:

o Creation time (when the process started)


o Exit time (when it ended; for a running process, equal to the creation time)
o Kernel time (time spent in kernel mode)
o User time (time spent in user mode)
 Elapsed (“Real”) Time
Subtract creation from exit via a LARGE_INTEGER union, then convert FILETIME →
SYSTEMTIME for human-readable output.
 Use Cases
Beyond measuring your own parent process, you can also profile arbitrary child
processes—though note that GetProcessTimes on your parent won’t include CPU
consumed by its children. For aggregate timing across many processes, the author
later suggests job objects (Chapter 15).

6.11 Example: timep (Process Times)

 Purpose
A Windows analogue of UNIX time, printing:
o Real (elapsed) time
o User time
o System (kernel) time
 Flow
1. Capture start wall-clock with GetSystemTime.
2. CreateProcess on the target command line.
3. WaitForSingleObject to block until it finishes.
4. Call GetProcessTimes for kernel/user; compute elapsed via the start/end times.
5. Print each with millisecond precision.
 Modern Refinements
o Use GetTickCount64 or QueryPerformanceCounter for higher-precision
wall-clock timing.
o For robust parsing of the full command line, prefer CommandLineToArgvW in
Unicode builds.

16
6.12 Generating Console Control Events

 Motivation
Abrupt termination (via TerminateProcess) skips cleanup. Instead, you can request
that a console-based child handle a clean shutdown by sending it a control event.
 Process Groups
If you start a process with CREATE_NEW_PROCESS_GROUP, that process becomes the
leader of a new group. All its descendants without another such flag join the same
group.
 Raising the Event

CopyEdit
BOOL GenerateConsoleCtrlEvent(
DWORD dwCtrlEvent, // CTRL_C_EVENT or CTRL_BREAK_EVENT
DWORD dwProcessGroupId // the leader process’s PID
);

All attached console processes in that group will receive the event, allowing them to run their
SetConsoleCtrlHandler handlers and exit gracefully.

 Limitations
o The caller and target must share the same console.
o It only works for console apps. GUI processes aren’t affected.
o Handlers run in a separate thread context; you must guard shared data
appropriately.

In my view, this continuation solidifies how Windows processes can be orchestrated for
parallel work, profiled, and shut down cleanly. For production code, wrap raw handles in safe
abstractions, check every API return, and prefer cooperative shutdown (console events or
custom IPC) over forced kills.

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

17
Here’s a concise synthesis of the “Simple Job Management” example, highlighting the key
ideas, correcting typos, and updating to a more modern C11 style where appropriate.

1. What the Author Is Building

 A minimal “job shell” that mimics UNIX shell job control on Windows, with three
commands:
1. jobbg – launch a process in the background (optionally in its own console or
completely detached).
2. jobs – list all active (or stopped) background jobs, showing job numbers, PIDs,
and status.
3. kill – terminate or signal a background job (via TerminateProcess or console
control events).
 Persistent job list: Jobs are recorded in a user-specific file (%TMP%\UserName.JobMgt)
so that multiple shells share the same list and jobs survive a shell restart.
 Concurrency safety: All accesses to the job-list file are protected by Win32 file locking
(LockFileEx / UnlockFileEx), preventing interleaved updates from multiple interactive
shells.

Note from the author: you could instead use the registry for storing job metadata (Exercise
6-9), and you can extend this basic framework with suspend/resume commands.

2. High-Level Flow of the JobShell Program

1. Main loop (JobShell.c)


o Prompt with JM$, read a line from stdin.
o Tokenize with GetArgs(), normalize to lowercase.
o Dispatch to one of four handlers:
 Jobbg(...)
 Jobs(...)
 Kill(...)
 quit → exit the loop.
o On unrecognized input, print an error.
2. Launching a background job (Jobbg)
o Parse options -c (new console) or -d (no console).
o Build CREATE_PROCESS flags:

18
c

CopyEdit

DWORD creationFlags = CREATE_SUSPENDED | CREATE_NEW_PROCESS_GROUP

| (giveConsole ? CREATE_NEW_CONSOLE : 0)

| (detachConsole ? DETACHED_PROCESS : 0);

o Call CreateProcess(…, creationFlags, …, &pi).


o Register the new process in the job database via GetJobNumber(&pi, cmdLine).
 On success: ResumeThread(pi.hThread), close handles, print [jobNo] PID.
 On failure: TerminateProcess(pi.hProcess), report error.
3. Assigning a Job Number (GetJobNumber)
o Opens (or creates) the shared job file with GENERIC_READ|WRITE and share
flags.
o Locks the entire file plus one record’s worth of bytes to guard extension.
o Scans existing records:
 Looks for an entry with ProcessId == 0 (free slot), or an exited PID (using
GetExitCodeProcess).
o If at end-of-file, a new record is appended; otherwise the empty slot is reused.
o Writes the new record (ProcessId, truncated commandLine), then unlocks and
closes.

<details> <summary>⚠️ Author’s noted defect</summary> If a PID is reused quickly by the


OS, the scan may mistake a *new* process for an old one. Exercise 6-8 suggests adding a
“birth timestamp” to each record to disambiguate. </details>

4. Listing Jobs (DisplayJobs)


o Opens and locks the file (read/write share).
o Iterates over each record, increments a counter for job numbers.
o For each nonzero ProcessId:
 Attempts OpenProcess; if it fails → status = “Done” and clears the record.
 Otherwise queries GetExitCodeProcess:
 STILL_ACTIVE → prints [n] <command>
 exited → prints [n] + Done <command> and marks record free.
5. Killing a Job (Kill)
o Parses -b (Ctrl-Break) or -c (Ctrl-C) flags.
o Looks up PID via FindProcessId(jobNumber).
o Opens a handle with PROCESS_TERMINATE.

19
o If a console event flag is set, calls GenerateConsoleCtrlEvent().
o Else calls TerminateProcess(hProcess, code).
o Waits up to 5 s for termination, then closes handle and reports.
6. Finding a Job (FindProcessId)
o Seeks to the byte offset (jobNumber – 1)*sizeof(JM_JOB) in the file.
o Locks that single record, reads it, unlocks, closes.
o Returns the stored ProcessId.

3. Corrections, Modernizations & Best Practices

 Typo fixes & C11 style


o Always terminate statements with semicolons (for (i = 0; i < MAX_ARG; i++) …).
o Use <stdbool.h> for bool instead of BOOL.
o Use snprintf or strncpy_s instead of _tcsnccpy.
o Replace older _T() macros with plain UTF-8 strings and CreateProcessA/W as
needed.
 Graceful shutdown
o The author uses TerminateProcess, which is forceful. For a cleaner exit, one
might send WM_CLOSE to the main window, or implement a custom IPC
shutdown protocol.
 Race conditions
o Even with file locking, there’s a small window between identifying a free slot and
writing the record. On high-load servers, consider using a transactional
database or memory-mapped file with atomic transactions.
 Resource cleanup
o Always check return values of ResumeThread, WriteFile, etc., and handle errors.
o Use structured exception handling (SEH) or C11’s atexit to ensure locks get
released even on crashes.

Example: Updated main() Skeleton in C11


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <windows.h>
#include "JobMgt.h"

20
int main(void) {
bool exitFlag = false;
char line[1024];
char* args[64];
int argc;

printf("Windows Job Management\n");


while (!exitFlag) {
fputs("JM$ ", stdout);
if (!fgets(line, sizeof(line), stdin)) break;
line[strcspn(line, "\r\n")] = '\0'; // trim newline

argc = GetArgs(line, args); // parse into tokens


if (argc == 0) continue;

for (int i = 0; args[0][i]; i++)


args[0][i] = tolower((unsigned char)args[0][i]);

if (strcmp(args[0], "jobbg") == 0) {
Jobbg(argc, args, line);
} else if (strcmp(args[0], "jobs") == 0) {
Jobs(argc, args, line);
} else if (strcmp(args[0], "kill") == 0) {
Kill(argc, args, line);
} else if (strcmp(args[0], "quit") == 0) {
exitFlag = true;
} else {
printf("Illegal command. Try again.\n");
}
}
return 0;
}

This version remedies missing punctuation, adopts C11 types, and uses safer string handling.

In summary, the author demonstrates how to build a lightweight job-control shell on


Windows by:

1. Spawning processes suspended and registering them in a shared, locked file.


21
2. Resuming, listing, and signaling or killing background jobs on demand.
3. Ensuring persistence across shell restarts and concurrency across multiple shells.

By correcting minor typos, adopting C11 conventions, and adding more robust shutdown
logic, you can make this example both more maintainable and more production-ready.

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

1. What the Author Introduces

 Windows Job Objects let you group one or more processes into a single “job,” so you
can:
1. Control them together (e.g. terminate all at once).
2. Enforce resource limits across the group (CPU time, working‐set size, process
count, etc.).
3. Collect accounting data (total user & kernel time, number of processes, etc.).
 This mechanism augments the earlier “job shell”–based approach, which managed
each process individually via a shared file.

2. Core APIs & Workflow

1. Creating or Opening a Job Object


o CreateJobObject(securityAttrs, name) → returns a handle.
o OpenJobObject(...) can grab an existing named job.
o When you’re done, CloseHandle(hJob) both releases your handle and—if the
KILL_ON_JOB_CLOSE limit is set—will terminate all member processes.
2. Assigning Processes
o AssignProcessToJobObject(hJob, hProcess) adds a running process to the job.
o A process cannot join more than one job; if you specify
CREATE_BREAKAWAY_FROM_JOB in CreateProcess flags, child processes can
escape the parent’s job.
3. Setting Limits
o SetInformationJobObject(hJob, JobObjectBasicLimitInformation,
&JOBOBJECT_BASIC_LIMIT_INFORMATION, size) lets you specify, for example:
 Per-process user-time limits (e.g. kill after X seconds of CPU).
 Maximum working‐set size, active process count, processor affinity, etc.
o If you include JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, closing the job handle will
forcibly terminate all members.
4. Querying Accounting Data

22
o QueryInformationJobObject(hJob, JobObjectBasicAccountingInformation,
&JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, size, NULL) returns totals:
 TotalProcesses, ActiveProcesses, TotalTerminatedProcesses.
 TotalUserTime (in 100 ns ticks), TotalKernelTime, TotalElapsedTime.

3. The JobObjectShell Example (Program 6-7)

 Extension of the earlier JobShell: It accepts an optional first argument—the


per-process user‐time limit in seconds (e.g. JobObjectShell 60).
 At startup:

CopyEdit
// Convert seconds to 100 ns units:
basicLimits.PerProcessUserTimeLimit.QuadPart = userSeconds * 10’000’000;
SetInformationJobObject(hJobObject,
JobObjectBasicLimitInformation,
&basicLimits, sizeof basicLimits);

 Every background launch (Jobbg):


1. Calls CreateProcess (suspended).
2. Calls AssignProcessToJobObject(hJobObject, processInfo.hProcess).
3. Registers it in the shared job file (via GetJobNumber).
4. Resumes the thread.
 Listing jobs (Jobs):
o First displays individual job-shell entries via DisplayJobs().
o Then calls QueryInformationJobObject to show a summary:

printf("Total: %u, Active: %u, Terminated: %u\n",


acct.TotalProcesses,
acct.ActiveProcesses,
acct.TotalTerminatedProcesses);
printf("User time all processes: %llu.%03llu\n",
acct.TotalUserTime.QuadPart / 1'000'000,
(acct.TotalUserTime.QuadPart % 1'000'000) / 1'000);

4. Author’s Observations & Exercises

23
 Name confusion: “job” refers both to your UNIX‐style background job and to the
Windows Job Object—keep them straight!
 Exercise 6-12 invites you to investigate some discrepancies between MSDN examples
and this code, particularly around structure initialization and flags usage.
 The author notes that console output can interleave messily when many processes
share the same window; recommending the -c (new‐console) option to isolate each
child.

5. Additional Insights & Modern Best Practices

1. Robust Initialization
o Rather than rely on C’s “partial brace” syntax, explicitly zero structures before
setting fields:
JOBOBJECT_BASIC_LIMIT_INFORMATION limits = {0};
limits.PerProcessUserTimeLimit.QuadPart = userLimit * 10'000'000;
limits.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_TIME |
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;

2. Graceful Shutdown
o Instead of forcible termination, consider GenerateConsoleCtrlEvent or named‐
pipe signaling so processes can clean up.
3. Handle Leaks
o Always check return values from AssignProcessToJobObject,
SetInformationJobObject, etc., and call CloseHandle promptly on both process
and job handles.
4. High-Resolution Timing
o If you need sub-second limits, use the JobObjectExtendedLimitInformation
structure with PerJobUserTimeLimit or leverage Windows’s high-precision
timers.
5. Security
o Supply appropriate SECURITY_ATTRIBUTES when creating named job objects so
that only intended users/processes can join or query them.

Bottom line: Job Objects provide a powerful, centralized way to group processes for collective
control, limit enforcement, and aggregated accounting—far more robust than a simple
file-based tracker alone. By combining them with your existing job-shell framework, you get
both per‐process bookkeeping (from your shared file) and system-level resource governance
(from the job object).

24

You might also like