340 likes | 355 Views
Explore the design considerations and functioning of Linux device drivers, including the concept of hardware devices appearing as files. Learn about the basic components of character drivers, LKM layout, callback functions, and package of function pointers.
E N D
UNIX’s “grand illusion” How Linux makes a hardware device appear to be a ‘file’
Basic char-driver components Device-driver LKM layout module’s ‘payload’ is a collection of callback-functions having prescribed prototypes function function function AND a ‘package’ of function-pointers fops . . . the usual pair of module-administration functions init registers the ‘fops’ exit unregisters the ‘fops’
Background • To appreciate the considerations that have motivated the over-all Linux driver’s design requires an understanding of how normal application-programs get their access to services that the operating system offers • This access is indirect – through specially protected interfaces (i.e., system calls) – usually implemented as ‘library’ functions
Standard File-I/O functions int open( char *pathname, int flags, … ); int read( int fd, void *buf, size_t count ); int write( int fd, void *buf, size_t count ); int lseek( int fd, loff_t offset, int whence ); int close( int fd ); (and other less-often-used file-I/O functions)
Our ‘elfcheck.cpp’ example #include <fcntl.h> # for open() #include <stdio.h> # for perror(), printf() #include <unistd.h> # for read(), close() #include <string.h> # for strncpy() char buf[4]; int main( int argc, char *argv[] ) { if ( argc == 1 ) return -1; // command-line argument is required int fd = open( argv[1], O_RDONLY ); // open specified file if ( fd < 0 ) { perror( argv[1] ); return -1; // quit if open failed int nbytes = read( fd, buf, 4 ); // read first 4 bytes if ( nbytes < 0 ) { perror( argv[1] ); return -1; // quit if read failed if ( strncmp( buf, “\177ELF”, 4 ) == 0 ) // check for ELF id printf( “File \’%s\’ has ELF signature \n”, argv[1] ); else printf( “File \’%s\’ is not an ELF file \n”, argv[1] ); close( fd ); // close the file }
Special ‘device’ files • UNIX systems treat hardware-devices as special files, so that familiar functions can be used by application programmers to access devices (e.g., open, read, close) • But a System Administrator has to create these device-files (in the ‘/dev’ directory) • Or alternatively (as we’ve seen), an LKM could create these necessary device-files
UNIX ‘man’ pages • A convenient online guide to prototypes and semantics of the C library functions • Example of usage: $ man 2 open
The ‘open’ function • #include <fcntl.h> • int open( char *pathname, int flags, … ); • Converts a pathname to a file-descriptor • File-descriptor is a nonnegative integer • Used as a file-ID in subsequent functions • ‘flags’ is a symbolic constant: O_RDONLY, O_WRONLY, O_RDWR
The ‘close’ function • #include <unistd.h> • int close( int fd ); • Breaks link between file and file-descriptor • Returns 0 on success, or -1 if an error
The ‘write’ function • #include <unistd.h> • int write( int fd, void *buf, size_t count ); • Attempts to write up to ‘count’ bytes • Bytes are taken from ‘buf’ memory-buffer • Returns the number of bytes written • Or returns -1 if some error occurred • Return-value 0 means no data was written
The ‘read’ function • #include <unistd.h> • int read( int fd, void *buf, size_t count ); • Attempts to read up to ‘count’ bytes • Bytes are placed in ‘buf’ memory-buffer • Returns the number of bytes read • Or returns -1 if some error occurred • Return-value 0 means ‘end-of-file’
Notes on ‘read()’ and ‘write()’ • These functions have (as a “side-effect”) the advancement of a file-pointer variable • They return a negative function-value of -1 if an error occurs, indicating that no actual data could be transferred; otherwise, they return the number of bytes read or written • The ‘read()’ function normally does not return 0, unless ‘end-of-file’ is reached
The ‘lseek’ function • #include <unistd.h> • off_t lseek( int fd, off_t offset, int whence ); • Modifies the file-pointer variable, based on the value of whence: enum { SEEK_SET, SEEK_CUR, SEEK_END }; • Returns the new value of the file-pointer (or returns -1 if any error occurred)
Getting the size of a file • For normal files, your application can find out how many bytes belong to a file using the ‘lseek()’ function: int filesize = lseek( fd, 0, SEEK_END ); • But afterward you need to ‘rewind’ the file if you want to read its data: lseek( fd, 0, SEEK_SET );
Device knowledge • Before you can write a device-driver, you must understand how the hardware works • Usually this means you need to obtain the programmer manual (from manufacturer) • Nowdays this can often be an obstacle • But some equipment is standardized, or is well understood (because of its simplicity)
Our RTC example • We previously learned how the Real-Time Clock device can be accessed by module code, using the ‘inb()’ and ‘outb()’ macros: • So we can create a simple char driver that lets application-programs treat the RTC’s memory as if it were in an ordinary file outb( addr, 0x70 ); outb( data, 0x71 ); outb( addr, 0x70 ); data = inb( 0x71 );
How device-access works Application program user space (unprivileged) call int $0x80 ret Standard runtime library int $0x80 iret iret Operating system (software) call supervisor space (privileged) ret Device-driver module (software) out in Physical peripheral device (hardware)
Our ‘cmosram.c’ driver • We implement three callback functions: • llseek: // sets file-pointer’s position • read: // inputs a byte from CMOS • write: // outputs a byte to CMOS • We omit other callback functions, such as: • open: // we leave this function-pointer NULL • release: // we leave this function-pointer NULL • The kernel has its own ‘default’ implementation for any function with NULL as its pointer-value
The ‘fops’ syntax • The GNU C-compiler supports a syntax for ‘struct’ field-initializations that lets you give your field-values in any convenient order: struct file_operations my_fops = { llseek: my_llseek, write: my_write, read: my_read, };
‘init’ and ‘exit’ • The module’s initialization function has to take care of registering the driver’s ‘fops’: register_chrdev( major, devname, &fops ); • Then the module’s cleanup function must make sure to unregister the driver’s ‘fops’: unregister_chrdev( major, devname ); • (These are prototyped in <linux/fs.h>)
Our ‘dump.cpp’ utility • We have written an application that lets users display the contents of any file in both hexadecimal and ascii formats • It also works on ‘device’ files! • With our driver-module installed, you can use it to view the CMOS memory-values: $ ./dump /dev/cmos
Now for a useful char-driver… • We can create a character-mode driver for the processor’s physical memory (i.e. ram) • (Our machines have 2-GB of physical ram) • But another device-file is named ‘/dev/ram’ so ours will be: ‘/dev/dram’ (dynamic ram) • We’ve picked 85 as its ‘major’ ID-number • Our SysAdmin setup a device-node using: root# mknod /dev/dram c 85 0
2-GB RAM has ‘zones’ ZONE_HIGH 1024+128-MB 2048-MB (= 2GB) ZONE_NORMAL 896-MB 16-MB ZONE_LOW Installed physical memory
Legacy DMA • Various older devices rely on the PC/AT’s DMA controller to perform data-transfers • This chip could only use 24-bit addresses • Only the lowest 16-megabytes of physical memory are ‘visible’ to these devices: 224 = 0x01000000 (16-megabytes) • Linux tries to conserve its use of memory from this ZONE_LOW region (so devices will find free DMA memory if it’s needed)
‘Normal’ memory zone • This zone extends from 16MB to 896MB • Linux uses the lower portion of this zone for an important data-structure that tracks how all the physical memory is being used • It’s an array of records: mem_map[] • (We will soon be studying this structure) • The remainder of ZONE_NORMAL is free for ‘dynamic’ allocations by the OS kernel
‘HIGH’ memory • Linux traditionally tried to ‘map’ as much physical memory as possible into virtual addresses allocated to the kernel-space • Before the days when systems had 1-GB or more of installed memory, Linux could linearly map ALL of the physical memory into the 1-GB ‘virtual’ kernel-region: 0xC0000000 – 0xFFFFFFFF • But with 2-GB there’s not enough room!
The 896-MB limit HIGH MEMORY temporary mappings using kmap() Installed ram (2-GB) HIGH ADDRESSES Kernel space (1-GB) 896-MB 896-MB always mapped Physical address-space application program’s code and data goes here User space (3-GB) A special pair of kernel-functions named ‘kmap()’ and ‘kunmap()’ can be called by device-drivers to temporarily map ‘vacant’ areas in the kernel’s ‘high’ address-space to pages of actual physical memory Virtual address-space
‘dram.c’ module-structure • We will need three kernel header-files: • #include <linux/module.h> // for printk(), register_chrdev(), unregister_chrdev() • #include <linux/highmem.h> // for kmap(), kunmap(), and ‘num_physpages’ • #include <asm/uaccess.h> // for copy_to_user()
Our ‘dram_size’ global • Our ‘init_module()’ function will compute the size of the installed physical memory • It will be stored in a global variable, so it can be accessed by our driver ‘methods’ • It is computed from a kernel global using the PAGE_SIZE constant (=4096 for x86) dram_size = num_physpages * PAGE_SIZE
‘major’ ID-number • Our ‘major’ device ID-number is needed when we ‘register’ our device-driver with the kernel (during initialization) and later when we ‘unregister’ our device-driver (during the cleanup procedure): int my_major = 85; // static ID-assignment
Our ‘file_operations’ • Our ‘dram’ device-driver does not need to implement special ‘methods’ for doing the ‘open()’, ‘write()’, or ‘release()’ operations (the kernel ‘default’ operations will suffice) • But we DO need to implement ‘read()’ and ‘llseek()’ methods • Our ‘llseek()’ code here is very standard • But ‘read()’ is specially crafted for DRAM
Using our driver • We have provided a development tool on the class website (named ‘fileview.cpp’) which can be used to display the contents of arbitrary files -- including device-files! • The data is shown in hex and ascii formats • The arrow-keys can be used for navigation • The enter-key allows an offset to be typed • Keys ‘b’, ‘w’, ‘d’ and ‘q’ adjust data-widths
In-class exercise #1 • Install the ‘dram.ko’ device-driver module; then use ‘fileview’ to browse the contents of the processor’s physical memory: $ ./fileview /dev/dram • Be sure to try the ‘b’, ‘w’, ‘d’, ‘q’, <ENTER> and <ESCAPE> keys • Also try: $ ./fileview /dev/cmos
In-class exercise #2 • The read() and write() callback functions in our ‘cmosram.c’ device-driver only transfer a single byte of data for each time called, so it takes 128 system-calls to read all of the RTC storage-locations! • Can you improve this driver’s efficiency, by modifying its read() and write() functions, so they’ll transfer as many valid bytes as the supplied buffer’s space could hold?