Advanced Systems Programming - Lesson 4: UNIX’s “grand illusion”

How Linux makes a hardware device appear to be a ‘file’Basic char-driver components 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

pdf34 trang | Chia sẻ: candy98 | Lượt xem: 973 | Lượt tải: 0download
Bạn đang xem trước 20 trang tài liệu Advanced Systems Programming - Lesson 4: UNIX’s “grand illusion”, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
UNIX’s “grand illusion” How Linux makes a hardware device appear to be a ‘file’ Basic char-driver components init exit fops function function function . . . Device-driver LKM layout registers the ‘fops’ unregisters the ‘fops’ module’s ‘payload’ is a collection of callback-functions having prescribed prototypes AND a ‘package’ of function-pointers the usual pair of module-administration functions 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 # for open() #include # for perror(), printf() #include # for read(), close() #include # 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 • 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 • int close( int fd ); • Breaks link between file and file-descriptor • Returns 0 on success, or -1 if an error The ‘write’ function • #include • 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 • 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 • 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 Physical peripheral device (hardware) Device-driver module (software) Operating system (software) Application program Standard runtime library supervisor space (privileged) user space (unprivileged) call ret int $0x80 iret int $0x80 iret call ret out in 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 ) 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_NORMAL ZONE_HIGH ZONE_LOW 1024+128-MB 16-MB 2048-MB (= 2GB) 896-MB 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! always mapped The 896-MB limit Virtual address-space HIGH MEMORY application program’s code and data goes here HIGH ADDRESSES 896-MB896-MB User space (3-GB) Kernel space (1-GB) Physical address-space Installed ram (2-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 temporary mappings using kmap() ‘dram.c’ module-structure • We will need three kernel header-files: – #include // for printk(), register_chrdev(), unregister_chrdev() – #include // for kmap(), kunmap(), and ‘num_physpages’ – #include // 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’, and 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?
Tài liệu liên quan