As mentioned in Chapter 2, Windows 2000 implements an object model to provide consistent and secure access to the various internal services implemented in the executive. This section describes the Windows 2000 object manager, the executive component responsible for creating, deleting, protecting, and tracking objects. The object manager centralizes resource control operations that otherwise would be scattered throughout the operating system. It was designed to meet the goals listed in Chapter 2.
Exploring the Object Manager
Throughout this section, you'll find experiments that show you how to peer into the object manager database. These experiments use the following tools, which you should become familiar with if you aren't already:
- Object viewer There are two versions of this tool: the version from www.sysinternals.com (on this book's companion CD as \Sysint\Winobj.exe) and a different version in (the Platform SDK (in \Program Files\Microsoft Platform SDK\Bin\ Winnt\Winobj.exe). The object viewer from www.sysinternals.com displays more accurate information about objects (such as the reference count, the number of open handles, security descriptors, and so forth) than the object viewer in the Platform SDK does.
- Open handles Two tools from www.sysinternals.com show open handles: a GUI tool (on this book's companion CD in \Sysint\Handleex.exe) and a command-line tool (on the companion CD in \Sysint\Nthandle.exe). The Windows 2000 resource kits include another tool that shows open handles, called Oh.exe.
- The kernel debugger !handle command.
The object viewer provides a way to traverse the namespace that the object manager maintains. (As we'll explain later, not all objects have names.) Try running the WinObj object manager utility from the companion CD and examining the layout, shown here:
In the Windows 2000 Resource Kit Tools Help for OH, you'll find out that if object tracking—an internal debugging feature in the executive—isn't enabled, OH will enable it by setting a Windows 2000 global flag in the registry and then rebooting your system. Neither Nthandle nor Handleex from the companion CD require object tracking. (You can enable object tracking by manually setting the flag and rebooting your system. See the section "Windows 2000 Global Flags" for more on global flags.) Because this flag uses additional memory to track object usage information, you should disable it with the Gflags utility after you've experimented with OH and then reboot your system again.
The object manager was designed to meet these goals:
Internally, Windows 2000 has two kinds of objects: executive objects and kernel objects. Executive objects are objects implemented by various components of the executive (such as the process manager, memory manager, I/O subsystem, and so on). Kernel objects are a more primitive set of objects implemented by the Windows 2000 kernel. These objects are not visible to user-mode code but are created and used only within the executive. Kernel objects provide fundamental capabilities, such as synchronization, on which executive objects are built. Thus, many executive objects contain (encapsulate) one or more kernel objects, as shown in Figure 3-11.
Details about the structure of kernel objects and how they are used to implement synchronization are given later in this chapter. In the remainder of this section, we'll focus on how the object manager works and on the structure of executive objects, handles, and handle tables. Here we'll just briefly describe how objects are involved in implementing Windows 2000 security access checking; we'll cover this topic thoroughly in Chapter 8.
Figure 3-11 Executive objects that contain kernel objects
Each Windows 2000 environment subsystem projects to its applications a different image of the operating system. The executive objects and object services are primitives that the environment subsystems use to construct their own versions of objects and other resources.
Executive objects are typically created either by an environment subsystem on behalf of a user application or by various components of the operating system as part of their normal operation. For example, to create a file, a Win32 application calls the Win32 CreateFile function, implemented in the Win32 subsystem DLL Kernel32.dll. After some validation and initialization, CreateFile in turn calls the native Windows 2000 service NtCreateFile to create an executive file object.
The set of objects an environment subsystem supplies to its applications might be larger or smaller than the set the executive provides. The Win32 subsystem uses executive objects to export its own set of objects, many of which correspond directly to executive objects. For example, the Win32 mutexes and semaphores are directly based on executive objects (which are in turn based on corresponding kernel objects). In addition, the Win32 subsystem supplies named pipes and mailslots, resources that are based on executive file objects. Some subsystems, such as POSIX, don't support objects as objects at all. The POSIX subsystem uses executive objects and services as the basis for presenting POSIX-style processes, pipes, and other resources to its applications.
Table 3-3 lists the primary objects the executive provides and briefly describes what they represent. You can find further details on executive objects in the chapters that describe the related executive components (or in the case of executive objects directly exported to Win32, in the Win32 API reference documentation).
The executive implements a total of 27 object types in Windows 2000, many of which are for use only by the executive component that defines them and not directly accessible by Win32 APIs. Examples of these objects include Driver, Device, and EventPair.
Table 3-3 Executive Objects Exposed to Win32
|Symbolic link||A mechanism for referring to an object name indirectly.|
|Process||The virtual address space and control information necessary for the execution of a set of thread objects.|
|Thread||An executable entity within a process.|
|Job||A collection of processes manageable as a single entity through the job.|
|Section||A region of shared memory (called a file mapping object in Win32).|
|File||An instance of an opened file or an I/O device.|
|Access token||The security profile (security ID, user rights, and so on) of a process or a thread|
|Event||An object with a persistent state (signaled or not signaled) that can be used for synchronization or notification.|
|Semaphore||A counter that provides a resource gate by allowing some maximum number of threads to access the resources protected by the semaphore.|
|Mutex*||A synchronization mechanism used to serialize access to a resource.|
|Timer||A mechanism to notify a thread when a fixed period of time elapses.|
|IoCompletion||A method for threads to enqueue and dequeue notifications of the completion of I/O operations (called an I/O completion port in the Win32 API).|
|Key||A mechanism to refer to data in the registry. Although keys appear in the object manager namespace, they are managed by the configuration manager, in a way similar to that in which file objects are managed by file system drivers. Zero or more key values are associated with a key object; key values contain data about the key.|
|WindowStation||An object that contains a clipboard, a set of global atoms, and a group of desktop objects.|
|Desktop||An object contained within a window station. A desktop has a logical display surface and contains windows, menus, and hooks.|
* Externally in the Win32 API, mutants are called mutexes. Internally, the kernel object that underlies mutexes is called a mutant.
As shown in Figure 3-12, each object has an object header and an object body. The object manager controls the object headers, and the owning executive components control the object bodies of the object types they create. In addition, each object header points to the list of processes that have the object open and to a special object called the type object that contains information common to each instance of the object.
Figure 3-12 Structure of an object
The object manager uses the data stored in an object's header to manage objects without regard to their type. Table 3-4 briefly describes the object header attributes.
Table 3-4 Standard Object Header Attributes
|Object name||Makes an object visible to other processes for sharing|
|Object directory||Provides a hierarchical structure in which to store object names|
|Security descriptor||Determines who can use the object and what they can do with it|
|Quota charges||Lists the resource charges levied against a process when it opens a handle to the object|
|Open handle count||Counts the number of times a handle has been opened to the object|
|Open handles list||Points to the list of processes that have opened handles to the object|
|Object type||Points to a type object that contains attributes common to objects of this type|
|Reference count||Counts the number of times a kernel-mode component has referenced the address of the object|
In addition to an object header, each object has an object body whose format and contents are unique to its object type; all objects of the same type share the same object body format. By creating an object type and supplying services for it, an executive component can control the manipulation of data in all object bodies of that type.
The object manager provides a small set of generic services that operate on the attributes stored in an object's header and can be used on objects of any type (although some generic services don't make sense for certain objects). These generic services, some of which the Win32 subsystem makes available to Win32 applications, are listed in Table 3-5.
Table 3-5 Generic Object Services
|Close||Closes a handle to an object|
|Duplicate||Shares an object by duplicating a handle and giving it to another process|
|Query object||Gets information about an object's standard attributes|
|Query security||Gets an object's security descriptor|
|Set security||Changes the protection on an object|
|Wait for a single object||Synchronizes a thread's execution with one object|
|Wait for multiple objects||Synchronizes a thread's execution with multiple objects|
Although these generic object services are supported for all object types, each object has its own create, open, and query services. For example, the I/O system implements a create file service for its file objects, and the process manager implements a create process service for its process objects. Although a single create object service could have been implemented, such a routine would have been quite complicated, because the set of parameters required to initialize a file object, for example, differs markedly from that required to initialize a process object. Also, the object manager would have incurred additional processing overhead each time a thread called an object service to determine the type of object the handle referred to and to call the appropriate version of the service. For these reasons and others, the create, open, and query services are implemented separately for each object type.
Object headers contain data that is common to all objects but that can take on different values for each instance of an object. For example, each object has a unique name and can have a unique security descriptor. However, objects also contain some data that remains constant for all objects of a particular type. For example, you can select from a set of access rights specific to a type of object when you open a handle to objects of that type. The executive supplies terminate and suspend access (among others) for thread objects and read, write, append, and delete access (among others) for file objects. Another example of an object-type-specific attribute is synchronization, which is described shortly.
To conserve memory, the object manager stores these static, object-type-specific attributes once when creating a new object type. It uses an object of its own, a type object, to record this data. As Figure 3-13 illustrates, if the object-tracking debug flag (described in the Experiment "Exploring the Object Manager") is set, a type object also links together all objects of the same type (in this case the Process type), allowing the object manager to find and enumerate them, if necessary.
Figure 3-13 Process objects and the process type object
Viewing the Type Objects
You can see the list of type objects declared to the object manager with the Object Viewer utility on this book's companion CD. Just run \Sysint\Winobj.exe, and then in the Winobj object manager open the \ObjectTypes directory, as shown here:
Type objects can't be manipulated from user mode because the object manager supplies no services for them. However, some of the attributes they define are visible through certain native services and through Win32 API routines. The attributes stored in the type objects are described in Table 3-6.
Table 3-6 Type Object Attributes
|Type name||The name for objects of this type("process," "event," "port, " and so on)|
|Pool type||Whether objects of this type should be allocated from paged or nonpaged memory|
|Default quota||charges Default paged and nonpaged pool values to charge to process quotas|
|Access types||The types of access a thread can request when opening a handle to an object of this type ("read," "write," "terminate," "suspend," and so on)|
|Generic access rights mapping||A mapping between the four generic access rights (read, write, execute, and all) to the type-specific access rights|
|Synchronization||Whether a thread can wait on objects of this type|
|Methods||One or more routines that the object manager calls automatically at certain points in an object's lifetime|
Synchronization, one of the attributes visible to Win32 applications, refers to a thread's ability to synchronize its execution by waiting for an object to change from one state to another. A thread can synchronize with executive job, process, thread, file, event, semaphore, mutex, and timer objects. Other executive objects don't support synchronization. An object's ability to support synchronization is based on whether the object contains an embedded dispatcher object, a kernel object that is covered in the section "Executive Synchronization" later in this chapter.
The last attribute in Table 3-6, methods, comprises a set of internal routines that are similar to C++ constructors and destructors—that is, routines that are automatically called when an object is created or destroyed. The object manager extends this idea by calling an object method in other situations as well, such as when someone opens or closes a handle to an object or when someone attempts to change the protection on an object. Some object types specify methods, whereas others don't, depending on how the object type is to be used.
When an executive component creates a new object type, it can register one or more methods with the object manager. Thereafter, the object manager calls the methods at well-defined points in the lifetime of objects of that type, usually when an object is created, deleted, or modified in some way. The methods that the object manager supports are listed in Table 3-7.
Table 3-7 Object Methods
|Method||When Method Is Called|
|Open||When an object handle is opened|
|Close||When an object handle is closed|
|Delete||Before the object manager deletes an object|
|Query name||When a thread requests the name of an object, such as a file, that exists in a secondary object domain|
|Parse||When the object manager is searching for an object name that exists in a secondary object domain|
|Security||When a process reads or changes the protection of an object, such as a file, that exists in a secondary object domain|
The object manager calls the open method whenever it creates a handle to an object, which it does when an object is created or opened. However, only one object type, the Desktop, defines an open method. The Desktop object type requires an open method so that Win32k can share a piece of memory with the process that serves as a desktop-related memory pool.
An example of the use of a close method occurs in the I/O system. The I/O manager registers a close method for the file object type, and the object manager calls the close method each time it closes a file object handle. This close method checks whether the process that is closing the file handle owns any outstanding locks on the file and, if so, removes them. Checking for file locks isn't something the object manager itself could or should do.
The object manager calls a delete method, if one is registered, before it deletes a temporary object from memory. The memory manager, for example, registers a delete method for the section object type that frees the physical pages being used by the section. It also verifies that any internal data structures the memory manager has allocated for a section are deleted before the section object is deleted. Once again, the object manager can't do this work because it knows nothing about the internal workings of the memory manager. Delete methods for other types of objects perform similar functions.
The parse method (and similarly, the query name method) allows the object manager to relinquish control of finding an object to a secondary object manager if it finds an object that exists outside the object manager namespace. When the object manager looks up an object name, it suspends its search when it encounters an object in the path that has an associated parse method. The object manager calls the parse method, passing to it the remainder of the object name it is looking for. There are two namespaces in Windows 2000 in addition to the object manager's: the registry namespace, which the configuration manager implements, and the file system namespace, which the I/O manager implements with the aid of file system drivers. (See Chapter 5 for more information on the configuration manager and Chapter 9 for more about the I/O manager and file system drivers.)
For example, when a process opens a handle to the object named \Device\Floppy0\docs\resume.doc, the object manager traverses its name tree until it reaches the device object named Floppy0. It sees that a parse method is associated with this object, and it calls the method, passing to it the rest of the object name it was searching for—in this case, the string \docs\resume.doc. The parse method for device objects is an I/O routine because the I/O manager defines the device object type and registers a parse method for it. The I/O manager's parse routine takes the name string and passes it to the appropriate file system, which finds the file on the disk and opens it.
The security method, which the I/O system also uses, is similar to the parse method. It is called whenever a thread tries to query or change the security information protecting a file. This information is different for files than for other objects because security information is stored in the file itself rather than in memory. The I/O system, therefore, must be called in order to find the security information and read or change it.
When a process creates or opens an object by name, it receives a handle that represents its access to the object. Referring to an object by its handle is faster than using its name because the object manager can skip the name lookup and find the object directly. Processes can also acquire handles to objects by inheriting handles at process creation time (if the creator specifies the inherit handle flag on the CreateProcess call and the handle was marked as inheritable, either at the time it was created or afterward by using the Win32 SetHandleInformation function) or by receiving a duplicated handle from another process. (See the Win32 DuplicateHandle function.)
All user-mode processes must own a handle to an object before their threads can use the object. Using handles to manipulate system resources isn't a new idea. C and Pascal (and other language) run-time libraries, for example, return handles to opened files. Handles serve as indirect pointers to system resources; this indirection keeps application programs from fiddling directly with system data structures.
Executive components and device drivers can access objects directly because they are running in kernel mode and therefore have access to the object structures in system memory. However, they must declare their usage of the object by incrementing either the open handle count or the reference count so that the object won't be deallocated while it's still being used. (See the section "Object Retention" for more details.)
Object handles provide additional benefits. First, except for what they refer to, there is no difference between a file handle, an event handle, and a process handle. This similarity provides a consistent interface to reference objects, regardless of their type. Second, the object manager has the exclusive right to create handles and to locate an object that a handle refers to. This means that the object manager can scrutinize every user-mode action that affects an object to see whether the security profile of the caller allows the operation requested on the object in question.
Viewing Open Handles with Nthandle
As shown in the following example, the Nthandle tool (\Sysint\ Nthandle.exe, which you'll find on this book's companion CD) can display the handles open by any or all processes:
C:\>nthandle -a -p system Handle V1.2 Copyright (C) 1997-2000 Mark Russinovich Systems Internals - http://www.sysinternals.com ------------------------------------------------------------------- System pid: 8 4: Process 8: Key \REGISTRY c: Thread 10: Key HKLM\SYSTEM\ControlSet003\Control\ProductOptions 14: Key HKLM\SYSTEM\Setup 18: Key HKLM\SYSTEM\ControlSet003\Control\IDConfigDB\CurrentDockInfo 1c: Key HKLM\SYSTEM\ControlSet003\Hardware Profiles\Current 20: Key HKLM\HARDWARE\DESCRIPTION\SYSTEM\MultifunctionAdapter
The display above shows the first eight open handles in the System process. The process name and ID are displayed first, followed by a line for each handle. The handle value, object type, and object name are shown for each handle. Because we specified the -a flag, handles to objects that don't have names (handle numbers 0x4, 0xc, and 0x18) are included.
An object handle is an index into a process-specific handle table, pointed to by the executive process (EPROCESS) block (described in Chapter 6). The first handle index is 4, the second 8, and so on. A process's handle table contains pointers to all the objects that the process has opened a handle to. Handle tables are implemented as a three-level scheme, similar to the way that the x86 memory management unit implements virtual to physical address translation. (See Chapter 7 for details about memory management in x86 systems.) When a process is created, the object manager allocates the top level of the handle table, which contains pointers to the middle-level tables; the middle level, which contains the first array of pointers to subhandle tables; and the lowest level, which contains the first subhandle table. The arrays at each level consist of 256 entries, allowing the initial handle table for a process to hold up to 255 handles. The reason that there are only 255 handle entries instead of 256 is that the last entry in the subhandle table is initialized with a value of -1. The -1 is an indicator to the object manager's handle allocation routine that it has come to the last entry of a subhandle table and must move to the next entry or allocate a new one if there are no free entries in the existing subhandle table. The object manager treats the low 24 bits of an object handle's value as three 8-bit fields that index into each of the three levels in the handle table. Figure 3-14 illustrates the Windows 2000 handle table architecture.
Figure 3-14 Process handle table architecture in Windows 2000
In Windows NT 4, a handle table consists of a fixed header and a variable size portion. The variable size part is an array of handle table entries, each describing one open handle. If a process opens more handles than can fit in the variable portion, the system allocates a new, larger array and copies the old array into the new one. The changes were made in Windows 2000 to improve handle table performance by avoiding copy operations and minimizing scenarios where an entire handle table must be locked.
As shown in Figure 3-15, each handle entry consists of a structure with two 32-bit members. The first 32-bit member contains both a pointer to the object header and four flags. Because object headers are always 32-bit aligned, the low-order 3 bits of this field are free for use as flags. An entry's high bit is used as a lock. When the object manager translates a handle to an object pointer, it locks the handle entry while the translation is in progress. Because all objects are located in the system address space, the high bit of the object pointer is set. (The addresses are guaranteed to be higher than 0x80000000 even on systems with the /3GB boot switch.) Thus, the object manager can keep the high bit clear when a handle table entry is unlocked and, in the process of locking the entry, set the bit and obtain the object's correct pointer value. The object manager needs to lock a process's entire handle table, using a handle table lock associated with each process, only when the process creates a new handle or closes an existing handle. The second member of a handle table entry is the granted access mask for that object. (Access masks are described in Chapter 8.)
Figure 3-15 Structure of a handle table entry
The first flag is the inheritance designation—that is, whether processes created by this process will get a copy of this handle in their handle tables. As already noted, handle inheritance can be specified on handle creation or later with the SetHandleInformation function. The second flag indicates whether the caller is allowed to close this handle. (This flag can also be specified with the Win32 SetHandleInformation function.) The third flag indicates whether closing the object should generate an audit message. (This flag isn't exposed to Win32—the object manager uses it internally.)
System components and device drivers often need to open handles to objects that user-mode applications shouldn't have access to. In Windows NT 4, such a handle had to be created in the System process, a process reserved for system threads and kernel-mode handles.
To reference a handle from the System process on Windows NT 4 when a kernel-mode function is executing on a user-mode thread, and therefore running in the context of a user-mode process where the handle table in effect is that of the user-mode process, the function would have to somehow switch into the System process. Drivers and the executive components accomplished this either by requesting that a system worker thread (described in the section "System Worker Threads" later in this chapter), which executes in the context of the System process, execute a function on the function's behalf to reference the appropriate handle or by switching the current thread's process context to that of the System process via the KeAttachProcess API function. Both options are tedious and can negatively affect performance.
Microsoft introduces a special handle table called the kernel handle table in Windows 2000 (referenced internally with the name ObpKernelHandleTable). The handles in this table are accessible only from kernel mode and in any process context. This means that a kernel-mode function can reference the handle in any process context with no performance impact. Handles from the kernel handle table are differentiated from those of the current process's handle table because the high bit of the handle is set—that is, all handles from the kernel handle table have values greater than 0x80000000.
Viewing the Handle Table with the Kernel Debugger
The !handle command in the kernel debugger takes three arguments:
!handle <handle index> <flags> <processid>
The handle index identifies the handle entry in the handle table. (Zero means display all handles.) The first handle is index 4, the second 8, and so on. For example, typing !handle 4 will show the first handle for the current process.
The flags you can specify are a bitmask, where bit 0 means display only the information in the handle entry, bit 1 means display free handles (not just used handles), and bit 2 means display information about the object that the handle refers to. The following command displays full details about the handle table for process ID 0x408:
kd> !handle 0 7 408 processor number 0 Searching for Process with Cid == 408 PROCESS 865f0790 SessionId: 0 Cid: 0408 Peb: 7ffdf000 ParentCid: 01dc DirBase: 04fd3000 ObjectTable: 856ca888 TableSize: 21. Image: i386kd.exe Handle Table at e2125000 with 21 Entries in use 0000: free handle 0004: Object: e20da2e0 GrantedAccess: 000f001f Object: e20da2e0 Type: (81491b80) Section ObjectHeader: e20da2c8 HandleCount: 1 PointerCount: 1 0008: Object: 80b13330 GrantedAccess: 00100003 Object: 80b13330 Type: (81495100) Event ObjectHeader: 80b13318 HandleCount: 1 PointerCount: 1
When you open a file, you must specify whether you intend to read or to write. If you try to write to a file that is opened for read access, you get an error. Likewise, in the executive, when a process creates an object or opens a handle to an existing object, the process must specify a set of desired access rights—that is, what it wants to do with the object. It can request either a set of standard access rights (such as read, write, and execute) that apply to all object types or specific access rights that vary depending on the object type. For example, the process can request delete access or append access to a file object. Similarly, it might require the ability to suspend or terminate a thread object.
When a process opens a handle to an object, the object manager calls the security reference monitor, the kernel-mode portion of the security system, sending it the process's set of desired access rights. The security reference monitor checks whether the object's security descriptor permits the type of access the process is requesting. If it does, the reference monitor returns a set of granted access rights that the process is allowed, and the object manager stores them in the object handle it creates. How the security system determines who gets access to which objects is explored in Chapter 8.
Thereafter, whenever the process's threads use the handle, the object manager can quickly check whether the set of granted access rights stored in the handle corresponds to the usage implied by the object service the threads have called. For example, if the caller asked for read access to a section object but then calls a service to write to it, the service fails.
Because all user-mode processes that access an object must first open a handle to it, the object manager can easily track how many of these processes, and even which ones, are using an object. Tracking these handles represents one part in implementing object retention—that is, retaining temporary objects only as long as they are in use and then deleting them.
The object manager implements object retention in two phases. The first phase is called name retention, and it is controlled by the number of open handles to an object that exist. Every time a process opens a handle to an object, the object manager increments the open handle counter in the object's header. As processes finish using the object and close their handles to it, the object manager decrements the open handle counter. When the counter drops to 0, the object manager deletes the object's name from its global namespace. This deletion prevents new processes from opening a handle to the object.
The second phase of object retention is to stop retaining the objects themselves (that is, to delete them) when they are no longer in use. Because operating system code usually accesses objects by using pointers instead of handles, the object manager must also record how many object pointers it has dispensed to operating system processes. It increments a reference count for an object each time it gives out a pointer to the object; when kernel-mode components finish using the pointer, they call the object manager to decrement the object's reference count. The system also increments the reference count when it increments the handle count, and likewise decrements the reference count when the handle count decrements, because a handle is also a reference to the object that must be tracked. (For further details on object retention, see the DDK documentation on the functions ObReferenceObjectByPointer and ObDereferenceObject.)
Figure 3-16 illustrates two event objects that are in use. Process A has the first event open. Process B has both events open. In addition, the first event is being referenced by some kernel-mode structure; thus, the reference count is 3. So even if processes A and B closed their handles to the first event object, it would continue to exist because its reference count is 1. However, when process B closes its handle to the second event object, the object would be deallocated.
Figure 3-16 Handles and reference counts
So even after an object's open handle counter reaches 0, the object's reference count might remain positive, indicating that the operating system is still using the object. Ultimately, the reference count also drops to 0. When this happens, the object manager deletes the object from memory.
Because of the way object retention works, an application can ensure that an object and its name remain in memory simply by keeping a handle open to the object. Programmers who write applications that contain two or more cooperating processes need not be concerned that one process might delete an object before the other process has finished using it. In addition, closing an application's object handles won't cause an object to be deleted if the operating system is still using it. For example, one process might create a second process to execute a program in the background; it then immediately closes its handle to the process. Because the operating system needs the second process to run the program, it maintains a reference to its process object. Only when the background program finishes executing does the object manager decrement the second process's reference count and then delete it.
Resource accounting, like object retention, is closely related to the use of object handles. A positive open handle count indicates that some process is using that resource. It also indicates that some process is being charged for the memory the object occupies. When an object's handle count drops to 0, the process that was using the object should no longer be charged for it.
Many operating systems use a quota system to limit processes' access to system resources. However, the types of quotas imposed on processes are sometimes diverse and complicated, and the code to track the quotas is spread throughout the operating system. For example, in some operating systems, an I/O component might record and limit the number of files a process can open, whereas a memory component might impose a limit on the amount of memory a process's threads can allocate. A process component might limit users to some maximum number of new processes they can create or a maximum number of threads within a process. Each of these limits is tracked and enforced in different parts of the operating system.
In contrast, the Windows 2000 object manager provides a central facility for resource accounting. Each object header contains an attribute called quota charges that records how much the object manager subtracts from a process's allotted paged and/or nonpaged pool quota when a thread in the process opens a handle to the object.
Each process on Windows 2000 points to a quota structure that records the limits and current values for nonpaged pool, paged pool, and page file usage. However, all the processes in an interactive session share the same quota block (there's no documented way to create processes with their own quota blocks), and system processes, such as services, have no quota limits.
Although the system implements code to track quotas, it currently doesn't enforce them. A process's paged pool and nonpaged pool quotas default to 0 (no limit). There are registry values to override these defaults, but the limits are soft, in that the system attempts to increase process quotas automatically when they are exceeded. If opening an object will exceed the paged or nonpaged quota, the memory manager is called to see whether the quotas can be increased. The memory manager makes this decision based on the amount of memory remaining in the system pools. If it determines that the quota can't be increased, the open request to the object fails with a "quota exceeded" error. But on most systems, quotas continue to grow as needed.
Viewing Process Quotas
You can view the paged pool, nonpaged pool, and page file current usage, peak usage, and quota (limit) for a process with the Process Explode utility, Pview.exe, available on www.reskit.com. (The Performance tool displays only the usage information, not the quotas.) In the following example, the process selected has a peak paged pool usage of 1062 KB, current usage of 1028 KB, and a quota of 1504 KB:
An important consideration in creating a multitude of objects is devising a successful system for keeping track of them. The object manager requires the following information to help you do so:
The first requirement is served by allowing names to be assigned to objects. This is an extension of what most operating systems provide—the ability to name selected resources, files, pipes, or a block of shared memory, for example. The executive, in contrast, allows any resource represented by an object to have a name. The second requirement, finding and retrieving an object, is also satisfied by object names. If the object manager stores objects by name, it can find an object by looking up its name.
Object names also satisfy a third requirement, which is to allow processes to share objects. The executive's object namespace is a global one, visible to all processes in the system. One process can create an object and place its name in the global namespace, and a second process can open a handle to the object by specifying the object's name. If an object isn't meant to be shared in this way, its creator doesn't need to give it a name.
To increase efficiency, the object manager doesn't look up an object's name each time someone uses the object. Instead, it looks up a name under only two circumstances. The first is when a process creates a named object: the object manager looks up the name to verify that it doesn't already exist before storing the new name in the global namespace. The second is when a process opens a handle to a named object: the object manager looks up the name, finds the object, and then returns an object handle to the caller; thereafter, the caller uses the handle to refer to the object. When looking up a name, the object manager allows the caller to select either a case-sensitive or a case-insensitive search, a feature that supports POSIX and other environments that use case-sensitive filenames.
Where the names of objects are stored depends on the object type. Table 38 lists the standard object directories found on all Windows 2000 systems and what types of objects have their names stored there. Of the directories listed, only \BaseNamedObjects and \?? are visible to user programs.
Because the base kernel objects such as mutexes, events, semaphores, waitable timers, and sections have their names stored in a single object directory, no two of these objects can have the same name, even if they are of a different type. This restriction emphasizes the need to choose names carefully so that they don't collide with other names (for example, prefix names with your company and product name).
Table 3-8 Standard Object Directories
|Directory||Types of Object Names Stored|
|\??||MS-DOS device names (\DosDevices is a symbolic link to this directory.)|
|\BaseNamedObjects||Mutexes, events, semaphores, waitable timers, and section objects|
|\FileSystem||File system driver objects and file system recognizer device objects|
|\KnownDlls||Section names and path for known DLLs (DLLs mapped by the system at startup time)|
|\Nls||Section names for mapped national language support tables|
|\ObjectTypes||Names of types of objects|
|\RPC Control||Port objects used by remote procedure calls (RPCs)|
|\Security||Names of objects specific to the security subsystem|
|\Windows||Win32 subsystem ports and window stations|
Object names are global to a single computer (or to all processors on a multiprocessor computer), but they're not visible across a network. However, the object manager's parse method makes it possible to access named objects that exist on other computers. For example, the I/O manager, which supplies file object services, extends the functions of the object manager to remote files. When asked to open a remote file object, the object manager calls a parse method, which allows the I/O manager to intercept the request and deliver it to a network redirector, a driver that accesses files across the network. Server code on the remote Windows system calls the object manager and the I/O manager on that system to find the file object and return the information back across the network.
Looking at the Base Named Objects
You can see the list of base objects that have names with the Object Viewer utility on this book's companion CD. (Another version of this utility is also available with the Platform SDK.) Run \Sysint\Winobj.exe, and click on \BaseNamedObjects, as shown here:
The named objects are shown on the right. The icons indicate the object type.
- Mutexes are indicated with a stop sign.
- Sections (Win32 file mapping objects) are shown as memory chips.
- Events are shown as exclamation points.
- Semaphores are indicated with an icon that resembles a traffic signal.
- Symbolic links have icons that are curved arrows.
Object directories The object directory object is the object manager's means for supporting this hierarchical naming structure. This object is analogous to a file system directory and contains the names of other objects, possibly even other object directories. The object directory object maintains enough information to translate these object names into pointers to the objects themselves. The object manager uses the pointers to construct the object handles that it returns to user-mode callers. Both kernel-mode code (including executive components and device drivers) and user-mode code (such as subsystems) can create object directories in which to store objects. For example, the I/O manager creates an object directory named \Device, which contains the names of objects representing I/O devices.
Symbolic links In certain file systems (on NTFS and some UNIX systems, for example), a symbolic link lets a user create a filename or a directory name that, when used, is translated by the operating system into a different file or directory name. Using a symbolic link is a simple method for allowing users to indirectly share a file or the contents of a directory, creating a cross-link between different directories in the ordinarily hierarchical directory structure.
The object manager implements an object called a symbolic link object, which performs a similar function for object names in its object namespace. A symbolic link can occur anywhere within an object name string. When a caller refers to a symbolic link object's name, the object manager traverses its object namespace until it reaches the symbolic link object. It looks inside the symbolic link and finds a string that it substitutes for the symbolic link name. It then restarts its name lookup.
One place in which the executive uses symbolic link objects is in translating MS-DOS-style device names into Windows 2000 internal device names. In Win32, a user refers to floppy and hard disk drives using the names A:, B:, C:, and so on. Moreover, the user can add pseudo drive names with the subst (substitute) command or by mapping a drive letter to a network share. Once they are created, these drive names must be visible to all processes on the system.
The Win32 subsystem makes drive letters protected, global data by placing them in the object manager namespace under the \?? object directory. (Prior to Windows NT 4, this directory was named \DosDevices; it was renamed \?? for performance reasons—that name places it first alphabetically.) When the user or an application creates a new drive letter, the Win32 subsystem adds another object under the \?? object directory.
Windows NT was written with the assumption that only one user would log on to the system interactively and that the system would run only one instance of any interactive application. When you install Windows 2000 Terminal Services, these assumptions are violated, so for Windows 2000 supporting multiple interactive users required changes to the object manager.
A user logging on to the console has access to the global namespace, a namespace that serves as the first instance of the namespace. Users logging on remotely at terminals are given a view of the namespace known as a local namespace. The parts of the namespace that are localized for each remote user include \DosDevices, \Windows, and \BaseNamedObjects. Making separate copies of the same parts of the namespace is known as instancing the namespace. Instancing \DosDevices makes it possible for each user to have different drive letters and Win32 objects such as serial ports. The \Windows directory is where Win32k.sys creates the interactive window station, \WinSta0. A Terminal Services environment can support multiple interactive users, but each user needs an individual version of WinSta0 to preserve the illusion that he or she is accessing the predefined interactive window station in Windows 2000. Finally, applications and the system create shared objects in \BaseNamedObjects, including events, mutexes, and memory sections. If two users are running an application that creates the named object ApplicationInitialized, each user session must have a private version of the object so that the two instances of the application don't interfere with one another by accessing the same object.
The object manager implements a local namespace by creating the private versions of the three directories mentioned under a directory associated with the user's session under \Sessions\X (where X is the session identifier of the user). When a Win32 application in remote session two creates a named event, for example, the object manager transparently redirects the object's name from \BaseNamedObjects to \Sessions\2\BaseNamedObjects.
All object manager functions related to namespace management are aware of the instanced directories and participate in providing the illusion that nonconsole sessions use the same namespace as the console session. As an optimization, the process object has a field named DeviceMap that points to a data structure shared by other processes in the same session, which locates the \DosDevices object manager directory that belongs to the session as well as a list of drive letters that are valid for the session. The object manager uses the data structure when looking up objects in \DosDevices.
Under certain circumstances, applications that are Terminal-Services aware need to access objects in the console session even if the application is running in a remote session. The application might want to do this to synchronize with instances of itself running in other remote sessions or with the console session. For these cases, the object manager provides the special override "\Global" that an application can prefix to any object name to access the global namespace. For example, an application in session two opening an object named \Global\ApplicationInitialized is directed to \BasedNamedObjects\ApplicationInitialized instead of \Sessions\2\BaseNamedObjects\ApplicationInitialized.
Viewing Namespace Instancing
You can see the object manager instance the namespace on a Windows 2000 Server, Advanced Server, or Datacenter Server system on which you install Terminal Services. Logon to the server using the Terminal Services client, and run the \Sysint\Winobj.exe utility from this book's companion CD.
Click on the \Sessions directory, and you'll see a subdirectory with a numeric name for each active remote session. If you open one of these directories, you'll see subdirectories named \DosDevices, \Windows, and \BaseNamedObjects, which are the local namespace subdirectories of the session. The following screen shot shows a local namespace: