The objective of this lab is twofold. First that you learn how to use the PC's timer and speaker. Second that you understand the concept of interrupt, and learn how to use it in the context of this lab.
Write in C language several functions to use the PC's timer and speaker. The goal is to develop a generic module that will be used to create a library, which you will be able to use in the course's project.
Like in previous labs we specify the prototype of functions that operate on the I/O device. However, for testing/evaluation purposes you will also have to develop a set of functions, known as scaffolding test code, whose prototype we specify. In later labs, we will specify only the testing functions and you will have to design the functions for operating on the I/O device.
Specifically, you shall develop the following test functions:
The first two functions are declared in header file timer.h, and file timer.c contains their implementation stubs. The last function is declared in header file speaker.h, and file speaker.c contains its implementation stub. You may find it convenient to add your test code to these files; this way you will avoid mistakes in their definition. Section 6 describes what these functions should do, and specifies the functions that you have to develop to implement them. You can also read the documentation generated by doxygen for the timer and for the speaker modules.
Furthermore, you will also have to develop the function main()
, which must be in a file named lab3.c
. This will allow us to use our own main()
without having to manually editing your files.
To avoid polluting the source tree in /usr/src/drivers/ with SVN metadata, you can create a working copy for this lab in the home directory of user lcom in the Minix 3 VMPlayer image of the lab's PCs. All you need to do is to issue the following commands in lcom's home directory (/home/lcom) in Minix :
$ svn checkout https://svn.fe.up.pt/repos/lcom1213-txgy
$ cd lcom1213-txgy
$ svn mkdir lab3
These commands should be issued as user lcom. You should use root only to install the configuration file lab3 in /etc/system.conf.d/, to install your program in /usr/sbin (actually this is not strictly necessary as described below in Section 7) and to run it with service.
So that you can accomplish this lab's objectives by the end of your lab class, you should do some homework. In addition to read, and understand, this handout and the class notes, you should implement timer_test_square()
and all the functions that it needs to call, as described in Section 6.1.
Every PC "has" an i8254 an IC with 3 timers, whose block diagram is shown in Figure 1. In this lab, you'll use timers 0 and 2. You must not change timer 1 configuration.
The 3 timers are identical and operate independently of one another. Thus we will describe only one timer.
A timer has a 16 bit counter, two input lines, Clock and Enable, and an output line Out. The Enable line is used to enable/disable the timer. When a timer is enabled, the value of its counter is decremented by one on every pulse of the Clock line. The value of Out depends on the counter's value and on the operating mode.
The i8254 supports several modes, but in this lab you will use only mode 3, square wave generator. In this mode, the Out line is high initially and until the counter reaches half of its initial value. It then goes low until the counter reaches zero, at which time the counter is reloaded with its (pre-programmed) initial value, Out is set to high and the cycle begins again. Thus, in mode 3, the timer generates a square wave with a frequency given by the expression clock/div, where clock is the frequency of the Clock input and div is the value loaded initially in the timer.
Each timer has a 16 bit counter, which may be both read and written. Furthermore, the i8254 has a single control register that can be written only, and that is used to configure the operation of all timers.
Each timer is programmed independently of the other timers. Programming a timer requires two steps:
The format of the control word (a 8-bit value) is shown in Table 1.
Bit | Value | Function | Bit | Value | Function |
7,6 | Select counter | 3,2,1 | Operating Mode | ||
00 | 0 | 000 | 0 | ||
01 | 1 | 001 | 1 | ||
10 | 2 | x10 | 2 | ||
11 | Read-Back Command | x11 | 3 | ||
5,4 | Type of Access | 100 | 4 | ||
01 | LSB | 101 | 5 | ||
10 | MSB | 0 | Counting mode | ||
11 | LSB followed by MSB | 0 | Binary (16 bits) | ||
1 | BCD (4 decades) |
Thus, bits 6 and 7 specify which timer to program. Bits 1, 2 and 3 specify the operating mode. Bit 0 specifies whether the counter is a binary or a BCD counter, i.e. whether the inital value should be interpreted as a binary or a BCD value. Bits 4 and 5 specify how the initial value is loaded. The following paragraph provides some more details regarding these bits.
Although the counters are 16 bits, the i8254 has only 8 data lines. Thus to load the initial value of a counter, the LSB and the MSB must be written separately. The i8254 allows loading either of these bytes, or both of them. In the latter case, the LSB must be loaded first. Which bytes of the counter will be loaded in the second step is specified by bits 4 and 5.
The PC uses each timer for a different purpose. Nevertheless, all timers use the same clock signal with frequency 1193181 Hz.
As shown in Figure 1, the output of timer 2 is connected to the PC speaker, and is used to generate tones by generating a square wave of an audible frequency. For example, to generate a 1000 Hz tone, the timer must be programmed to operate in mode 3, with an initial value of 1193.
Furthermore, to enable the speaker, you must set to 1 both bits 0 and 1 of I/O port 0x61 (SPEAKER_CTRL
) . As shown in Figure 1, bit 0 is connected to the GATE of Timer 2 and if low, it will disable the timer. In addition, the OUT line of timer 2 is not connected directly to the speaker, instead it is gated via a NAND, whose other input line comes from bit 1 of port 0x61. Thus, unless that bit is set to 1, the input to the speaker will always be high, and no tone will be generated.
Figure 1 also shows that the output of the timer 0 is directly connected to line IRQ0 of the PC's interrupt controller. It is usually programmed in mode 2, to generate a more or less stable time reference that can be used by the operating system to measure time with a resolution of a few milliseconds. In Section 4 we describe the interrupt mechanism used by the PC, and in Section 5 we describe the use in Minix 3 of the interrupts generated by the timer 0.
The registers of the i8254, like those of most other PC's I/O devices, are mapped in the I/O address space of the PC's processor. The address of the control register (TIMER_CTRL) is 0x43
, the address of timer 0 is 0x40
(TIMER_0) and the address of timer 2 is 0x42
(TIMER_2). Although the counters of these timers are 16 bit, all these registers are 8 bits, and access to the MSB and the LSB of each counter is done as described above in Section 3.2.
Note: This section describes the PC's priority interrupt handling based on the i8259. Although current systems support a more advanced mechanism (the APIC), it is still possible to use the older interface.
In the PC, all HW interrupts are processed using the i8259 IC, the priority interrupt controller (PIC). This IC has 8 interrupt request (IRQ) lines which are connected directly to I/O devices. These lines have an implicit priority: IRQ line 0 has the highest priority, next comes IRQ line 1, and so on until IRQ line 7. This means, that the PIC will forward an interrupt request to the processor, only if no interrupt with the same or higher priority is being processed. Furthermore, it is possible to mask each line independently: while an IRQ line is masked, the PIC will not forward any interrupt request on that line to the CPU.
The PC uses two PICs in cascade, as shown in Figure 2, with the INT line of the second one (the slave) connected to the IRQ line 2 of the first one (the master). This means that IRQ lines 0 and 1 of the master have higher priority than the IRQ lines of the slave PIC. However, all IRQ lines of the slave have higher priority than IRQ lines 3 to 7 of the master.
Figure 2 outlines the interrupt mechanism used in the PC. When an I/O device activates its interrupt request line, the PIC will activate the CPU's interrupt line, initiating an interrupt sequence. The CPU will then save the address of the instruction being executed and the flags register on the stack and will disable interrupts, and will respond by activating an interrupt acknowledgment line. When the PIC detects that this line is active, it will put an 8-bit value, which was previously programmed on the PIC, in the data bus. The processor then uses this 8-bit value as an index to a table (the Interrupt Descriptor Table) whose entries contain the addresses of interrupt service routines. The processor will then transfer control to the address of the entry corresponding to the 8-bit value received from the PIC. As a result, the processor will execute the device's interrupt service routine, or interrupt handler, which is responsible for informing the PIC that it has "finished" handling the interrupt, and must terminate with instruction IRETD
.
The sequence described in the previous paragraph, assumes that:
Because the PIC does not generate another interrupt for the same device or for another with lower priority until it is informed that the current interrupt has already been handled, it is up to the interrupt handler (IH) to do it, by writing the EOI command (0x20) to the control register. If the interrupt originates on the slave PIC, the IH will need to issue the EOI command to both the slave and the master PICs. Table 4 shows the addresses of the PIC registers and Table 5 shows the IRQ lines and the interrupt vectors for common I/O devices of a PC.
PIC | PIC Controller Register | Interrupt Mask Register |
PIC1 | 0x20 | 0x21 |
PIC2 | 0xA0 | 0xA1 |
PIC 1 | Device | Vector | PIC 2 | Device | Vector |
IRQ0 | Timer 0 | 0x08 | IRQ8 | Real Time Clock | 0x70 |
IRQ1 | Keyboard | 0x09 | IRQ9 | Replace IRQ2 | 0x71 |
IRQ2 | slave 8259 | 0x0A | IRQ10 | Reserved | 0x72 |
IRQ3 | Serial device COM2 | 0x0B | IRQ11 | Reserved | 0x73 |
IRQ4 | Serial device COM1 | 0x0C | IRQ12 | Mouse | 0x74 |
IRQ5 | Reserved/Sound card | 0x0D | IRQ13 | Math coprocessor | 0x75 |
IRQ6 | Diskette | 0x0E | IRQ14 | Hard disk | 0x76 |
IRQ7 | Parallel port | 0x0F | IRQ15 | Reserved | 0x77 |
An interrupt handler cannot take any arguments nor return any values. Furthermore, it must save all the registers that it uses and must terminate with the IRETD
instruction, which resets the stack and the processor to its state at the time the interrupt occurred. Because the interrupts are disabled while the interrupt handler executes, the CPU will not be handle further interrupts, therefore an interrupt handler should be as short as possible; if necessary, the interrupt handler may enable interrupts by executing the STI
instruction.
In this lab, you'll use only C language. Because the C language does not provide any operators or standard functions that allow access to I/O ports, you'll have to use functions provided by Minix 3 instead.
Direct I/O port access is a very powerful capability that can be easily misused and that can interfere with the proper operation of the operating system and other processes. Thus, in Minix 3, I/O port access is a privileged operation, and is provided via the SYS_DEVIO kernel call, and several libsys.a functions, that hide the details of making a kernel call from the user-level programmer. For this lab, you may find useful the following functions:
#include <minix/syslib.h>
int sys_inb(port_t port, unsigned long *byte);
int sys_outb(port_t port, unsigned long byte);
As usual, this requires adding a permission to execute this kernel call (SY_DEVIO) to the file lab3 that you'll have to add to the /etc/system.conf.d/ directory. Furthermore, that file should also specify the I/O ports that the process is allowed to access via this call.
Interrupt handling with Minix 3 is somewhat unusual, because device drivers are user level processes. Indeed, to ensure that the interrupt handler of a device driver does not mess with the kernel, interrupt servicing is also done at the user level by the device driver.
To allow this, the Minix 3 (micro) kernel has a generic interrupt handler, which notifies a device driver when an interrupt it may have to service occurs. Although this might appear strange at first, the truth is that an interrupt handler does not take any arguments and does not return any value. Thus, a simple notification is all that is required from the kernel. Handling of the interrupt proper must be done by the device driver.
The major disadvantage of this approach is that the interrupt servicing latency may become too large for devices that are very fast, such as gigabit Ethernet cards.
To support this model, Minix 3 provides also the SYS_IRQCTL kernel call, and several libsys.a functions, that hide the details of making a kernel call from the user-level programmer. For this lab, you may find useful the following functions:
#include <minix/syslib.h>
int sys_irqsetpolicy(int irq_line, int policy, int *hook_id);
int sys_irqrmpolicy(int *hook_id);
int sys_irqenable(int *hook_id);
int sys_irqdisable(int *hook_id);
All these calls return OK on success and 3 other values on failure.
sys_irqsetpolicy(int irq_line, int policy, int *hook_id)
int sys_irqrmpolicy(int *hook_id);
int sys_irqenable(int *hook_id)
int sys_irqdisable(int *hook_id)
Again, subscribing and unsubscribing interrupt notifications, and enabling and disabling interrupts are privileged operations. Therefore, the execution of these operations, and the IRQ lines on which they are allowed to operate must be specified in a file /etc/system.conf.d/ for your process.
The Minix 3 generic interrupt handler uses the Minix 3 interprocess communication (IPC) mechanism to notify a subscriber of the occurrence of an interrupt. This IPC mechanism is essentially a mechanism for sending and receiving messages between processes. Interrupt notifications are a special kind of message supported by Minix 3 IPC.
As a consequence, in Minix 3 a device driver is an event driven service that receives and processes messages, either interrupt notifications from the kernel, or service requests (usually I/O operations) from other processes.
The program that you will have to develop in this lab, is not a standard device driver, in that it does not receive service requests from other processes, but only interrupt notifications from the kernel. Thus, your program should include loop in which interrupt notifications are received and handled. In a standard Minix 3 device driver, this loop is endless. In this lab, you may want to terminate the loop after a few iterations, or on some event. The following code segment illustrates the general structure of the main loop and the Minix 3 functions that should be used.
1: #include <minix/drivers.h>
2: #include <minix/com.h>
3:
4: int ipc_status;
5: message msg;
6:
7: while( 1 ) { /* You may want to use a different condition */
8: /* Get a request message. */
9: if ( driver_receive(ANY, &msg, &ipc_status) != 0 ) {
10: printf("driver_receive failed with: %d", r);
11: continue;
12: }
13: if (is_ipc_notify(ipc_status)) { /* received notification */
14: switch (_ENDPOINT_P(msg.m_source)) {
15: case HARDWARE: /* hardware interrupt notification */
16: if (msg.NOTIFY_ARG & irq_set) { /* subscribed interrupt */
17: ... /* process it */
18: }
19: break;
20: default:
21: break; /* no other notifications expected: do nothing */
22: }
23: } else { /* received a standard message, not a notification */
24: /* no standard messages expected: do nothing */
25: }
26: }
Function driver_receive()
in line 9, is a function provided by the libdrivers.a library. It should be used by device drivers to receive messages, including notifications, from the kernel or from other processes. The first argument specifies the sender of the messages we want to receive. The value ANY
means that the driver accepts messages from any process. The second and third arguments are the addresses of variables of type message
and int
, which will be initialized, by the driver_receive()
code, with the message received and IPC related status, respectively.
The macro is_ipc_notify()
in line 13, returns true if the message received is a notification or false otherwise, i.e. if it is a standard message.
The member m_source
of type message
, used in line 14, contains the endpoint of the sender of the message. The endpoint is an address used by Minix 3 IPC to specify the communication endpoints, i.e. the source and destination of a communication instance. The macro _ENDPOINT_P
allows to extract the process identifier from a process's endpoint. The value HARDWARE
, used in line 15, is a special process identifier value to indicate a HW interrupt notification. The reason for the use of a process identifier different from the endpoint is that the endpoint of a process may change in time, just like an address, but most of the time we are not interested in the address, but rather on the entity behind that address. Now, you may ask: if so, why use endpoints? Good question, but this is "out of scope" of this course.
Finally, the NOTIFY_ARG
macro in line 16 is a reader friendly name for the member of a message
type that contains the "argument" of an interrupt notification. This is a 32-bit bitmask with the bits of the active interrupts subscribed set to 1. The bit of an interrupt notification is the one passed in the hook_id
argument of the sys_irqsetpolicy()
call. For example, if the value used for one interrupt was 2, then when that interrupt occurs, the generic interrupt handler will send a notification to the device driver with bit 2 of the NOTIFY_ARG
member of a message
set to 1. This scheme allows the kernel to use a single notification message to notify a process of the occurrence of several interrupts on different IRQ lines.
In this lab, as in all other labs, you need only to handle one interrupt. This is because usually a device only uses one interrupt line. Thus, the code in line 17, will just handle the interrupt, or better call the interrupt handler.
However, in your project, the program you'll develop will use more than one device and therefore will have to handle more than one interrupt. Thus, variable irq_set
should be a bitmask with the bits corresponding to the hook_id
values used in the sys_irqsetpolicy()
set to 1. The code in line 17, should then identify which of these bits are set and call the corresponding handler.
As stated above, one of the goals of this lab is to develop a generic module that will be used to create a library, which you will be able to use in the course's project. We could certainly specify the API, as we have done for the previous labs. (Actually, we will also do it for this lab.) But we want you to play a more active role. Rather than being just an implementer of an API, we want you to design an API.
We'll use this lab to show you by example how you can develop a fairly general API, which can be used to operate on the timer/counter, and that you can use to implement the testing functions that we will use to test your code. In the next labs, we'll just specify the testing functions, as we have done above, and it will be up to you to design the API for operating on the I/O devices.
timer_test_square()
The purpose of this function is to test your code to configure a timer to generate a square wave with a given frequency.
As mentioned above, Minix 3 uses Timer 0 as a time reference to maintain the time of the day and for implementing SW timers (for measuring the duration of time intervals). Thus, when it starts up, it configures Timer 0 to generate interrupts at a fixed rate, by default 60 Hz. Minix 3 then uses a counter that it increments on every Timer 0 interrupt, and uses its value to measure the time -- it expects that counter to be incremented by 60 every second.
timer_test_square()
should configure Timer 0 to generate a square wave with a frequency equal to its argument. Thus, if, for example, it is invoked with an argument of 30 (meaning 30 Hz), Timer 0 counter will be incremented by 30 every second rather than by 60. As a result it will rapidly become late, which you'll be able to check by giving the command date
$ date
and by comparing with the time reported by Linux.
You could easily implement this function by loading the appropriate value into Timer 0 counter. However, this would hardly be reusable (except by cut and pasting). It would not allow us to control, for example, other timers. What we want is that you implement a slightly more general function that is able to configure a timer to generate a square wave with a given frequency:
int timer_set_square(unsigned char timer, unsigned long freq)
This function can then be reused for example to configure the frequency of the tone to be generated by the speaker. Function timer_test_square()
would need only to call timer_set_square()
with the appropriate arguments. Function timer_set_square()
should also be implemented in timer.c. (You may wish to use the file timer.c, which includes stubs for this function.) For more information about it please read its doxygen documentation.
timer_test_int()
The purpose of this function is to test your code that handles interrupts generated by Timer 0. With that in mind, it should print a message on every second, using the function printf()
, for a time interval whose duration is specified in its argument. These messages will appear on the console and will be appended to the file /usr/log/messages. Thus, if you are on a terminal different from the console, you can still see these messages by giving the command:
$ tail -f /usr/log/messages
Again, you could implement the entire functionality required-- subscribing an interrupt, handling the interrupts and unsubscribing an interrupt -- inside timer_test_int()
, by invoking the right Minix 3 kernel calls. Again, that would not be reusable. More interesting is to design functions that you can use later in the project:
void timer_int_handler()
int timer_subscribe_int()
int timer_unsubscribe_int()
which should be implemented in a file named timer.c. (You may wish to use the file timer.c, which includes stubs for these functions.) The following paragraphs describe these functions. You can also read their doxygen documentation.
As stated above the main use for the periodic interrupt generated by Timer 0 is as a time reference that can be used to measure time. This requires incrementing a counter on every interrupt. Thus all timer_int_handler()
needs to do is to increment a global counter variable.
The implementation of timer_subscribe_int()
is straightforward. You must call the sys_irqsetpolicy()
and the sys_irqenable()
calls, described in the paragraph on interrupt subscription in Subsection 5.2 . The policy you should specify in sys_irqsetpolicy()
is IRQ_REENABLE
, so that the generic interrupt handler will acknowledge the interrupt, i.e. ouput the EOI
command to the PIC, thus enabling further interrupts on the corresponding IRQ line. The implementation of timer_unsusbscribe_int()
is even simpler.
Although these functions can be reused to (un)subscribe Timer 0 interrupts, they cannot be used to (un)subscribe interrupts for other I/O devices. Although, we would like to be able to do that, it is a bit too early to devise a general API. It requires more experience with the use of interrupts on Minix 3, as well as the use of some C features that you do not know yet.
This is even "more true", for the "interrupt loop", i.e. the loop for receiving an interrupt notification. Thus, right now I suggest that the interrupt loop be included in timer_test_int()
.
speaker_test()
The purpose of this function is to test your code that controls the PC speaker. With that in mind, it should configure the speaker to generate a tone, i.e. a sound with a fixed frequency, for a specified time interval.
Configuring the speaker to generate a tone is fairly straightforward. First, the code must configure Timer 2 to generate a square wave of a fixed frequency. Then, the code must enable the output of Timer 2 to the speaker for the specified time interval. How to do this is described in the paragraph on Timer 2 in Subsection 3.3.
To program Timer 2, your code can invoke function timer_set_square()
, which was already described above in Section 6.1. In order to control the output you should implement function:
speaker_ctrl(unsigned char on)
in a file named speaker.c. (You may wish to use the file speaker.c, which includes stubs for these functions.)
As described in its doxygen documentation, this function should set/reset the appropriate bits on port 0x61 to enable/disable the speaker. One alternative would be to provide a function that would allow to individually control each of the bits. However, this is likely to be a useless generalization, because we'll be using port 0x61 only to control the speaker.
As for measuring the time interval, if you were able to complete timer_test_int()
, you should use Timer 0 interrupts. Otherwise, you can use the function:
sleep()
which suspends the execution of the process for the number of seconds specified in its argument.
Because VMware Player does not emulate the PC speaker, testing of this part of the lab, must be done on the Minix 3 image in the hard disk (HD) partition.
I suggest that you develop your code on Linux using Eclipse's RSE plugin. Once it compiles and runs in VMware Player you can try it on the PC. For that, you'll need to move your code to the Minix 3 partition on the HD. Possible ways to do it are described in Annex 1.
If your program does not run as expected on the PC you can try to fix it using one the available editors (if you have never used vi I suggest you use nano). Compile and run it as usual. Although, you can go back to Linux and use Eclipse to fix your program, compile it and run it before testing it on the PC, the development cycle will be much slower.
Remember that:
Compilation, installation and execution of your program can be performed by issuing the same commands you used in the previous labs, this time in the /home/lcom/lcomt1213-txgy/lab3 directory you should have created at the beginning of this lab.
Actually, you need not install your program in /usr/sbin/. You can use the executable in /home/lcom/lcom1213-txgy/lab3/ as argument of service. For example, assuming that you are in this directory you can issue the following command as root:
# service run `pwd`/lab3
The `pwd` expression evaluates to the output of the command pwd, which returns the present working directory (/home/lcom/lcom1213-txgy/lab3, according to our assumptions). This is simpler than having to type the full path:
# service run /home/lcom/lcom1213-txgy/lab3
Your program invokes functions that are privileged. Thus, before you can run it, you need to add a file named lab3 to the /etc/system.conf.d/ directory to grant your program the necessary permissions.
Unless you use privileged functions that are not really necessary, the following entry is enough:
service lab3
{
system
DEVIO
IRQCTL
;
ipc
SYSTEM
rs
vm
pm
vfs
;
io
40:4 # i8254 ports
61 # port for speaker control
;
irq
0 # TIMER 0 IRQ
;
uid 0
;
};
You should submit both the code you prepared for the class and the final code via the SVN repository of the project you created on Redmine. This code should be under directory lab3, which should be at the top level of the SVN repository of your Redmine project.
Thanks to the dedication and hard work of the lab's technician, Rui Fernandes, you can now use the network to transfer your code to the Minix partition on the lab's PCs.
You can transfer your code via the network, by using rsync in a way similar to that describe in Lab 2's Annex 2. Now, however, you must first upload your files to a computer with an rsync server, such as pinguim.fe.up.pt, gnomo.fe.up.pt or yoda.fe.up.pt. For example, let's assume that your code for lab3 is in /home/lcom/lab3 on Minix in the VMwarePlayer. To upload your code to your personal area, assuming that your current working directory is /home/lcom/, you can use the command:
$ rsync -auv lab3 <your_login_name>@yoda.fe.up.pt:
Upon request, introduce your FEUP password, and if valid, the entire lab3
directory should be transferred to a new directory named lab3 under the top level directory in your storage area in FEUP (assuming, that no such directory existed).
After booting Minix on the PC, you can download the files you have previously uploaded. Just login as lcom and after that issue the command:
$ rsync -auv <your_login_name>@yoda.fe.up.pt:lab3 ./
Again, you'll be prompted for your password, after entering it, rsync
will create a directory named lab3 under the directory where you run rsync (should be /home/lcom, if you followed the instructions).
If using the network is not a choice, because e.g your PC's network card is not supported by Minix, you'll need to use a floppy to move the code from Linux to Minix. In Linux, to copy a file from the HD to a floppy, and vice-versa, you need to use the utility mcopy. For example, to copy file timer.c to the floppy, you can issue the following command:
$ mcopy timer.c a:
In Minix, to copy from the floppy to the HD you need to use the command dosread. For example, to copy file timer.c from the floppy to your current directory, you can issue the following command:
$ dosread a timer.c > timer.c
If later you wish to make a copy the code you have changed in Minix on the HD, you can use the Minix command doswrite.
$ doswrite a timer.c < timer.c
For more information on how to use these commands, please read their man pages.
This lab is based on a lab by João Cardoso and Miguel P. Monteiro for DJGPP running on Windows98.