5. Minix 3 Notes

5.1 Initialization of Privileged User Processes in MINIX 3

The programs that you'll develop in LCOM must run as privileged user processes so that they can access directly HW resources.

Privileged user processes in MINIX 3 are created by a special process, the ressurection server (RS), which is itself a privileged process. During initialization a privileged user process must synchronize with the RS. For simple privileged programs like this lab's this can be done just by including the function call:

#include <minix/drivers.h> sef_startup();

as the first instruction of main().

IMP.: In this and in other code segments where we introduce Minix-specific functions, the first lines are #include directives that you need to add in every file where you call the respective functions. These directives are needed so that the functions are declared before you invoke them.

IMP.: Although we already provide you a main() function in the lab2.c file, it does not include the call to the sef_startup() function, thus it is up to you to add it (as well as the #include directive).

5.2 I/O Ports Access

In this lab, you'll use only the C programming language. Because C 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 (SYS_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.

5.3 Interrupt Handling

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.

Subscribing an Interrupt Notification

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)
This function should be used to subscribe a notification on every interrupt in the input irq_line. The policy argument specifies whether or not the interrupt on that IRQ line should be automatically enabled by the generic interrupt handler, or whether the device driver interrupt handler will do it. Finally, the hook_id argument is used both for input to the call and output from the call. The caller should initialize it with a value that will be used in the interrupt notifications, as described below. The value returned by the call in this argument must be used in the other calls to specify the interrupt notification to operate on.
int sys_irqrmpolicy(int *hook_id);
This function unsubscribes a previous subscription of the interrupt notification associated with the specified hook_id
int sys_irqenable(int *hook_id)
This function enables interrupts on the IRQ line associated with the specified hook_id. This may be convenient if the policy specified in the sys_irqsetpolicy() call does not enable interrupts automatically.
int sys_irqdisable(int *hook_id)
This function disables interrupts on the IRQ line associated with the specified 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.

Receiving an Interrupt Notification

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 a 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 ( (r = 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.