// imaginary syscall to copy file1 to file2, len bytes // problems with this style: // 1. duplicate cleanup code inside every "if": hard to maintain, bloated code // 2. unwieldy: hard to reorder un/initializations // 3. hard to debug: multiple exit points int sys_cp_v1(char *file1, char *file2, u_int len) { void *buf; struct file *fp1, fp2; // 1. verify valid params // 2. initializations // open file1 for reading fp1 = filp_open(file1, O_READ, ...); if (fp1 == NULL) { // then filp_open failed (WARNING: not quite) return -ENOENT; // return some other error } // open file2 for writing fp2 = filp_open(file2, O_WRITE, ...); if (fp2 == NULL) { // then filp_open failed (WARNING: not quite) filp_close(fp1); return -ENOENT; // return some other error } // allocate buffer of "len" to read/write bytes buf = kmalloc(len, ...); // kmalloc failed if (buf == NULL) { filp_close(fp2); filp_close(fp1); return -ENOMEM; } // if some other init here, will need to kfree + filp_close x 2 // 3. actually doing the work // copy len bytes from file1 to file 2. // 4. cleanup kfree(buf); filp_close(fp2); filp_close(fp1); return retval; // retval? } // alternative: use nested if-then-else int sys_cp_v2(char *file1, char *file2, u_int len) { void *buf; struct file *fp1, fp2; fp1 = filp_open(file1, O_READ, ...); if (fp1 == NULL) { // then filp_open failed (WARNING: not quite) return -ENOENT; // return some other error } else { fp2 = filp_open(file2, O_WRITE, ...); if (fp2 == NULL) { // then filp_open failed (WARNING: not quite) filp_close(fp1); return -ENOENT; // return some other error } else { buf = kmalloc(len, ...); // kmalloc failed if (buf == NULL) { filp_close(fp2); filp_close(fp1); return -ENOMEM; } } } // if some other init here, will need to kfree + filp_close x 2 // 3. actually doing the work // copy len bytes from file1 to file 2. // 4. cleanup kfree(buf); filp_close(fp2); filp_close(fp1); return retval; // retval? } // alternative: use goto label // two "good" uses to "goto" in C: // 1. implement a state machine (each state is a label, each transition is a goto) // 2. flatten and simplify code as below // advantages: shorter code, smaller footprint, easier to manage, debug int sys_cp_v3(char *file1, char *file2, u_int len) { void *buf; struct file *fp1, fp2; int retval = 0; // init to any reasonable value fp1 = filp_open(file1, O_READ, ...); if (fp1 == NULL) { retval = -ENOENT; goto out; } fp2 = filp_open(file2, O_WRITE, ...); if (fp2 == NULL) { retval = -ENOENT; goto out_close_fp1; } buf = kmalloc(len, ...); // kmalloc failed if (buf == NULL) { retval = -ENOMEM; goto out_close_fp2; } // if some other init here, will need to kfree + filp_close x 2 // if some init failed here, goto out_kfree // 3. actually doing the work // copy len bytes from file1 to file 2. // when done with actual work, you can fall-through to out_kfree label, or // goto that label if failed mid-way. Remember to set retval to proper // error or 0 if ok. // 4. cleanup out_kfree: kfree(buf); out_close_fp2: filp_close(fp2); out_close_fp1: filp_close(fp1); out: printk("..... exiting with value %d\n", retval); return retval; } // alternative: reduces so many goto labels. easier to read/maintain. // but: spend extra cycles to init variables and check "if" conditions at end. int sys_cp_v4(char *file1, char *file2, u_int len) { void *buf = NULL; struct file *fp1 = NULL, fp2 = NULL; int retval = 0; // init to any reasonable value fp1 = filp_open(file1, O_READ, ...); if (fp1 == NULL) { retval = -ENOENT; goto out; } fp2 = filp_open(file2, O_WRITE, ...); if (fp2 == NULL) { retval = -ENOENT; goto out; } buf = kmalloc(len, ...); // kmalloc failed if (buf == NULL) { retval = -ENOMEM; goto out; } // if some other init here, will need to kfree + filp_close x 2 // if some init failed here, goto out_kfree // 3. actually doing the work // copy len bytes from file1 to file 2. // when done with actual work, you can fall-through to out_kfree label, or // goto that label if failed mid-way. Remember to set retval to proper // error or 0 if ok. // 4. cleanup out: if (buf) kfree(buf); if (fp2) filp_close(fp2); if (fp1) filp_close(fp1); printk("..... exiting with value %d\n", retval); return retval; } // how functions signal success/failure int test_functions(void) { // void: don't return anything // next fxn returns void (void) this_fxn_returns_void(...); // alt: *ignore* a retval from a function (void) printk(....); // 1. boolean functions: return T (1 or non-zero) or F (0) (bool_t, bool) // don't assume that "T" is "1" -- can be any non-zero value // 2. return ptr or NULL ptr = kmalloc(...); if (ptr == NULL) { // error } // 3a. functions that return 0 on success, -1 on error // 3b. return 0 on success, -errno on error // 3c. return 0 or positive integer on success, else -errno on error ret = vfs_read(...); // 4. return -errno on error, 0 if "ok", and "1" if ok but let caller know // to do something else. Sometimes called a "tri-state" function. ret = dentry->d_revalidate(...); // validates if a cached directory entry // is still valid (maybe it changed on // the underlying (network) file system. // d_revalidate returns -errno on error; 0 meaning "valid"; 1 if object is // "stale". // 5. encode an error into a pointer // assume 32-bit address: 0..2^32-1 // assume upper 1024 numbers are for errors: 2^32-1024..2^32-1 // 2^32-1 -> err number 1 (ENOENT) // 2^32-2 -> err number 2 (EINVAL??) // 2^32-n (for n<1024) -> err number n // valid pointers are from 1..2^32-1024-1 // if you get a value back from a fxn that returns an encoded error, need // to check if it's an error or not: error if >= 2^32-1024 ptr = filp_open(...); if (IS_ERR(ptr)) { // filp_open returned an error retval = PTR_ERR(ptr); // converts PTR->ERR goto out_err; } // if you need to RETURN an encoded error, use ERR_PTR(-EPERM); } // kernel stack, malloc int test_stack_and_malloc(void) { // kernel stack is limited in size, b/c phys mem, can be configured in // kernel, but typically a few KB, e.g., 8KB. Unlike user prog. where // prog stack can grow and grow to consume all virt. addr. space. char buf[4096]; // bad: puts a lot of mem on limited kernel stack // if you overflow the kstack, kernel may crash, random mem corruptions. // see scripts/checkstack.pl to call amount of stack space each fxn takes. // preferred: use dynamic memory // Note: many diff. types of allocators in linux (TBD later) // kmalloc is general purpose allocator void *ptr; // in userland, malloc(3) is a libc function, uses brk(2) and sbrk(2) // userland: if os has no mem right now, process is suspended for longer // periods (that's why malloc rarely returns NULL). More advanced user // prog like web/db servers can (1) pre-allocate all/most mem they need, or // if get NULL from malloc, sleep and try again later. ptr = kmalloc(size, flags); // flags say: // 1. do I want to wait or not wait? if not wait, can get NULL. // 2. allocate mem in physical space or in virt space for a user process. // 3. allocate inside a dev. driver (cannot block). // cleanup kfree(ptr); // kmalloc guarantees contiguous space, // kmem may be fragmented, so don't ask for too much, else you may get a // "NULL". // alt: virtual mem alloc ptr = vmalloc(size, flags); // contiguous mem // warning: cannot use vmalloc anywhere you're not allowed to block: // interrupt handlers, inside certain locks (spinlock), etc. vfree(ptr); } // debugging int test_debug(void) { // in userland, you have symbolic debuggers and tracers, thanks to a // system call called: ptrace(2) // in kernel, no other program can run "under" the OS // there's examples of "user mode linux" (UML), a full running kernel as a // user process. Not helpful for debugging "real problems" // kgdb extension for linux. attach using serial port, redirect console to // serial port. But only works for some subsystems of the linux kernel. // problem: most difficult bugs are timing related, so anything that // changed the timing of a running kernel isn't as helpful. Kernel // developers assume a higher level of expertise... // "printf is your friend" }