The kernel is written in C wherever possible. However, certain functions are written in assembly language for either efficiency or necessity. These functions typically perform direct machine control, such as stack manipulation and flipping interrupt bits. You will find this code in a number of assembly files along with C to assembly interface functions.
The dispatcher directs a system call into the kernel. A large number of calls are directed into the dosfns, or DOS Functions, layer. This layer works with the lower layers to add the DOS appearance to the system calls. It does this by combining calls into the various managers in the next lower layers with code to add features to the manager calls that are unique to a DOS interface. For example, this layer contains functions that encompass all rules for handling file I/O that deals with PSP, handles or character and block I/O functionality.
The fnode table is the foundation for the design of the FAT file system manager. The fnode data structure controls all internal file operations and virtualizes the file. When fs performs an operation such as create or open, it allocates an entry from the fnode table and populates the fnode fields. The call returns a number that is the index into the fnode table. From here on out, any function that performs an operation on the file receives this number. The fnode table entry that corresponds to this index controls all file parameters.
If you are familiar with DOS internals from the undocumented internals books and articles, you will recognize the need to map internal fnode to the SFT (System File Table) structure used by DOS. We handle this through the dosfns layer function. The module dosfns maps the fnode number to an SFT handle, which is part its "DOS personality" responsibility. At the fs level, it does its work in an "OS neutral" mode in order to localize unique features to the personality module.
The arena header in DOS-C is known as the MCB (Memory Control Block). As with MS-DOS, this data structure is in front of each allocated and free memory block. DOS-C uses the same 'M' and 'Z' signatures to identify the block type. Also, the segment value of the PSP of the program it belongs to or a 0008h if it belongs to DOS-C, identifies the owner of the block in the same way. DOS-C uses all other entries in an identical manner. Many applications make use of this information and DOS-C provides this same information for sake of compatibility. Unlike fs where MS-DOS personality splits between it and dosfns, memmgr contains all DOS personality.
Both executable file types start from a single load entry. It is up to DOS-C to identify which file type it is. It does this by examining the first two bytes of the file. If it is an 'MZ', then it is an .EXE file and task invokes the .EXE loader, otherwise task assumes it to be a .COM binary image and task invokes the .COM loader.
Both loaders initially allocate memory from memmgr to place the environment strings in. The differences begin when the actual file loading occurs. The .COM loader merely allocates memory and begins loading the file into memory for a maximum of 64K. The .EXE loader, on the other hand, computes the size required and then allocates memory. It then proceeds to load the image. When task completes loading the image, it does a seek to the relocation offset and does a segment fixup (necessary for the segmented architecture of the 80X86 family). From this point, both loaders proceed with creating the PSP, cloning the file table, initializing the task's registers and executing the task if so requested.
Both I/O handlers are loosely based on the UNIX I/O model. As with UNIX, there are two types of I/O handlers. The first type is the character I/O type. This type of I/O appears as a stream of bytes to the kernel. The kernel either sequentially reads a byte from or writes a byte to a device driver. There are also functions to read a buffer into memory and handle the familiar DOS line editing functions.
The block I/O interface provides functions to read and write a block of data to a block device -- usually a disk. Each block corresponds to a sector and is represented in memory as a data structure in a block cache. When the kernel reads data from a disk, blockio reads the sector into a buffer and places it into a Least Recently Used (LRU) chain. When blockio needs a new buffer, it writes the tail of the list to disk, if needed, and returns that buffer. When blockio completes the data transfer, the buffer goes to the head of the LRU chain, indicating that it is the newest buffer. Management functions handle these operations for dirty buffer write back, as well as buffer fill. There are also buffer management functions to perform operations such as LRU management and flush.
As with the API, the device driver interface is well documented. Since the interface is designed for an assembly language system, a special assembly language function interfaces all device drivers. This function, execrh, accepts a request packet from the I/O handlers that contains a function number requested by the device driver. It handles the correct sequence of calls to the strategy and interrupt entry points for the device driver and returns the packet to the I/O handler.
The DOS-C device drivers are the bottom layer of the kernel. Like the remainder of the kernel, C is used in these device drivers also. The device drivers perform the necessary device interface between the kernel and the device itself. DOS-C has the same device drivers as MS-DOS and they perform the same functions as their MS-DOS counterparts.