[PATCH] [47/48] Suspend2 2.1.9.8 for 2.6.12: 623-generic-block-io.patch

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



diff -ruNp 624-filewriter.patch-old/kernel/power/suspend_file.c 624-filewriter.patch-new/kernel/power/suspend_file.c
--- 624-filewriter.patch-old/kernel/power/suspend_file.c	1970-01-01 10:00:00.000000000 +1000
+++ 624-filewriter.patch-new/kernel/power/suspend_file.c	2005-07-05 23:48:59.000000000 +1000
@@ -0,0 +1,1616 @@
+/*
+ * Filewriter.c
+ *
+ * Copyright 2005 Nigel Cunningham <[email protected]>
+ *
+ * Distributed under GPLv2.
+ * 
+ * This file encapsulates functions for usage of a simple file as a
+ * backing store. It is based upon the swapwriter, and shares the
+ * same basic working. Here, though, we have nothing to do with
+ * swapspace, and only one device to worry about.
+ *
+ * The user can just
+ *
+ * echo Suspend2 > /path/to/my_file
+ *
+ * and
+ *
+ * echo /path/to/my_file > /proc/software_suspend/filewriter_target
+ *
+ * then put what they find in /proc/software_suspend/resume2
+ * as their resume2= parameter in lilo.conf (and rerun lilo if using it).
+ *
+ * Having done this, they're ready to suspend and resume.
+ *
+ * TODO:
+ * - File resizing.
+ */
+
+#include <linux/suspend.h>
+#include <linux/module.h>
+#include <linux/blkdev.h>
+#include <linux/file.h>
+#include <linux/stat.h>
+#include <linux/mount.h>
+#include <linux/statfs.h>
+
+#include "suspend2_core/suspend.h"
+#include "suspend2_core/suspend2_common.h"
+#include "suspend2_core/version.h"
+#include "suspend2_core/proc.h"
+#include "suspend2_core/plugins.h"
+#include "suspend2_core/ui.h"
+#include "suspend2_core/extent.h"
+#include "suspend2_core/utility.h"
+#include "suspend2_core/io.h"
+
+#include "block_io.h"
+
+/*
+ *		General Declarations.
+ */
+
+static struct suspend_proc_data filewriter_proc_data[];
+static struct suspend_plugin_ops filewriterops;
+
+/*
+ *		External Declarations
+ */
+
+extern asmlinkage long sys_open(const char __user * filename, int flags, int mode);
+extern asmlinkage long sys_close(unsigned int fd);
+
+/*
+ *		Forward Declarations
+ */
+
+static int filewriter_invalidate_image(void);
+static int filewriter_storage_available(void);
+
+/*
+ *		Details of our target.
+ */
+
+char filewriter_target[256];
+static struct inode * target_inode;
+static int target_fd = -1;
+static struct block_device * target_bdev;
+static int used_devt = 0;
+static dev_t target_dev_t = 0;
+static int target_firstblock = 0;
+static int target_blocksize = PAGE_SIZE;
+static int target_storage_available = 0;
+static unsigned int target_blkbits;
+#define target_blockshift (PAGE_SHIFT - target_blkbits)
+#define target_blocksperpage (1 << target_blockshift)
+
+static int target_type = -1;
+
+/*
+static char * description[7] = {
+	"Socket",
+	"Link",
+	"Regular file",
+	"Block device",
+	"Directory",
+	"Character device",
+	"Fifo",
+};
+*/
+
+static char HaveImage[] = "HaveImage\n";
+static char NoImage[] =   "Suspend2\n";
+static const int resumed_before_byte = sizeof(HaveImage) + 1;
+#define sig_size resumed_before_byte
+
+/* Header_pages must be big enough for signature */
+static int header_pages, main_pages;
+
+static unsigned long * header_link = NULL;
+#define BYTES_PER_HEADER_PAGE (PAGE_SIZE - sizeof(sector_t))
+
+#define target_is_normal_file() (S_ISREG(target_inode->i_mode))
+
+/*
+ *		Readahead Variables
+ */
+
+// Higher Level
+static int readahead_index = 0, readahead_submit_index = 0;
+static int readahead_allocs = 0, readahead_frees = 0;
+
+static char * filewriter_buffer = NULL;
+static int filewriter_buffer_posn = 0;
+static int filewriter_page_index = 0;
+
+/*
+ * ---------------------------------------------------------------
+ *
+ *     Internal Data Structures
+ *
+ * ---------------------------------------------------------------
+ */
+
+/* header_data contains data that is needed to reload pagedir1, and
+ * is therefore saved in the suspend header.
+ *
+ * Pagedir2 data gets stored before pagedir1 (save order), and the first
+ * page for pagedir1 to use is set when pagedir2 is written (when we know how
+ * much storage it used). Since this first entry is almost certainly not at the
+ * start of a extent, the firstoffset variable below tells us where to start in
+ * the extent. All of this means we don't have to worry about getting different
+ * compression ratios for the kernel and cache (when compressing the image).
+ * We can simply allocate one pool of storage (size determined using expected
+ * compression ratio) and use it without worrying whether one pageset
+ * compresses better and the other worse (this is what happens). As long as the
+ * user gets the expected compression right, it will work.
+ */
+
+static struct {
+	/* Location of start of pagedir 1 */
+	struct extent * pd1start_block_extent;
+	int pd1start_extent_number;
+	unsigned long pd1start_block_offset;
+
+} filewriter_header_data;
+
+/* Extent chain for blocks */
+static struct extentchain block_chain;
+
+/*
+ * ---------------------------------------------------------------
+ *
+ *     Current state.
+ *
+ * ---------------------------------------------------------------
+ */
+
+/* Which pagedir are we saving/reloading? Needed so we can know whether to
+ * remember the last block used at the end of writing pageset2, and
+ * get that location when saving or reloading pageset1.*/
+static int current_stream = 0;
+
+/* Pointer to current entry being loaded/saved. */
+static struct extent * currentblockextent = NULL;
+static unsigned long currentblockoffset = 0;
+
+/* Header Page Information */
+static struct submit_params * first_header_submit_info = NULL,
+ * last_header_submit_info = NULL, * current_header_submit_info = NULL;
+
+/*
+ *		Helpers.
+ */
+
+/* 
+ * Return the type of target we have, an index into the descriptions
+ * above.
+ */
+static int get_target_type(struct inode * inode)
+{
+	switch (inode->i_mode & S_IFMT) {
+		case S_IFSOCK:
+			target_type = 0;
+			break;
+		case S_IFLNK:
+			target_type = 1;
+			break;
+		case S_IFREG:
+			target_type = 2;
+			break;
+		case S_IFBLK:
+			target_type = 3;
+			break;
+		case S_IFDIR:
+			target_type = 4;
+			break;
+		case S_IFCHR:
+			target_type = 5;
+			break;
+		case S_IFIFO:
+			target_type = 6;
+			break;
+	}
+	return target_type;
+}
+	
+#define target_is_usable (!(target_type == 1 || target_type == 4))
+#define target_num_sectors (target_inode->i_size >> target_blkbits)
+
+static int size_ignoring_sparseness(void)
+{
+	int mappable = 0, i;
+	
+	if (target_is_normal_file()) {
+		int extent_min = -1, extent_max = -1;
+
+		for (i = 0; i <= target_num_sectors; i++) {
+			sector_t new_sector = bmap(target_inode, i);
+			if (!new_sector) {
+				if (i == extent_max + 1)
+					extent_max++;
+				else
+					extent_min = extent_max = i;
+			} else
+				mappable++;
+		}
+	
+		return mappable >> (PAGE_SHIFT - target_blkbits);
+	} else
+		return filewriter_storage_available();
+}
+
+static void get_main_pool_phys_params(void)
+{
+	int i;
+	
+	if (block_chain.first)
+		put_extent_chain(&block_chain);
+
+	if (target_is_normal_file()) {
+		int header_sectors = (header_pages << target_blockshift);
+		int extent_min = -1, extent_max = -1, real_sector = 0;
+
+
+		for (i = 0; i <= target_num_sectors; i++) {
+			sector_t new_sector =
+				bmap(target_inode, header_sectors + i);
+			
+			/* 
+			 * I'd love to be able to fill in holes and resize 
+			 * files, but not yet...
+			 */
+
+			if (!new_sector)
+				continue;
+			
+			real_sector++;
+
+			if (real_sector < header_sectors)
+				continue;
+
+			if (new_sector == extent_max + 1)
+				extent_max++;
+			else {
+				if (extent_min > -1)
+					append_extent_to_extent_chain(
+						&block_chain,
+						extent_min, extent_max);
+				extent_min = extent_max = new_sector;
+			}
+		}
+		if (extent_min > -1)
+			append_extent_to_extent_chain(&block_chain,
+				       extent_min, extent_max);
+	} else
+		if (target_storage_available > 0) {
+			unsigned long new_start =
+			 last_header_submit_info ?
+			 last_header_submit_info->block[target_blocksperpage -1]
+				+ 1: 0;
+
+			append_extent_to_extent_chain(&block_chain,
+			 new_start, new_start +
+			 (min(main_pages, target_storage_available) << 
+			  		target_blockshift) - 1);
+		}
+}
+
+static void get_target_info(void)
+{
+	if (target_bdev) {
+		/* 
+		 * Don't replace the inode if we got the bdev from opening
+		 * a file.
+		 */
+		if (!target_inode)
+			target_inode = target_bdev->bd_inode;
+		target_type = get_target_type(target_inode);
+		target_blkbits = target_bdev->bd_inode->i_blkbits;
+		target_storage_available = size_ignoring_sparseness();
+	} else {
+		target_type = -1;
+		target_inode = NULL;
+		target_blkbits = 0;
+		target_storage_available = 0;
+	}	
+}
+
+static int set_target_blocksize(void)
+{
+	if ((suspend_bio_ops.get_block_size(target_bdev) 
+					!= target_blocksize) &&
+	    (suspend_bio_ops.set_block_size(target_bdev, target_blocksize)
+	    			 == -EINVAL)) {
+		printk(KERN_ERR name_suspend "Filewriter: Failed to set the blocksize.\n");
+		return 1;
+	}
+
+	return 0;
+		
+}
+
+static int try_to_open_target_device(void)
+{
+	if (!target_dev_t)
+		return 1;
+
+	if (!target_bdev) {
+		target_bdev = open_by_devnum(target_dev_t, FMODE_READ);
+
+		if (IS_ERR(target_bdev)) {
+			target_bdev = NULL;
+			return 1;
+		}
+		used_devt = 1;
+
+		if (set_target_blocksize()) {
+			blkdev_put(target_bdev);
+			target_bdev = NULL;
+			return 1;
+		}
+	}
+
+	get_target_info();
+
+	return 0;
+}
+
+static int try_to_parse_target_dev_t(char * commandline)
+{
+	struct kstat stat;
+	int error;
+
+	target_dev_t = name_to_dev_t(commandline);
+
+	if (!target_dev_t) {
+		error = vfs_stat(commandline, &stat);
+		if (!error)
+			target_dev_t = stat.rdev;
+	}
+
+	if (!target_dev_t) {
+		if (test_suspend_state(SUSPEND_TRYING_TO_RESUME))
+			suspend_early_boot_message(1, SUSPEND_CONTINUE_REQ,
+				"Failed to translate \"%s\" into a device id.\n",
+				commandline);
+		else
+			printk(name_suspend "Can't translate \"%s\" into a device id yet.\n",
+					commandline);
+		return 1;
+	}
+	
+	try_to_open_target_device();
+
+	if (IS_ERR(target_bdev)) {
+		printk("Open by devnum returned %p given %x.\n",
+				target_bdev, target_dev_t);
+		target_bdev = NULL;
+		if (test_suspend_state(SUSPEND_BOOT_TIME))
+			suspend_early_boot_message(1, SUSPEND_CONTINUE_REQ,
+				"Failed to get access to the device on which"
+				" Software Suspend's header should be found.");
+		else
+			printk("Failed to get access to the device on which "
+				"Software Suspend's header should be found.\n");
+		return 1;
+	}
+
+	return 0;
+}
+
+static void filewriter_noresume_reset(void)
+{
+ 	/* 
+	 * If we have read part of the image, we might have filled header_data with
+	 * data that should be zeroed out.
+	 */
+
+	memset((char *) &filewriter_header_data, 0, sizeof(filewriter_header_data));
+}
+
+/*
+ *
+ */
+
+int parse_signature(char * header, int restore)
+{
+	int have_image = !memcmp(HaveImage, header, sizeof(HaveImage) - 1);
+	int non_image_header = !memcmp(NoImage, header, sizeof(NoImage) - 1);
+
+	if (!have_image && !non_image_header)
+		return -1;
+
+	if (non_image_header)
+		return 0;
+	
+	clear_suspend_state(SUSPEND_RESUMED_BEFORE);
+
+	if (header[resumed_before_byte] & 1)
+		set_suspend_state(SUSPEND_RESUMED_BEFORE);
+
+	/* Invalidate Image */
+	if (restore)
+		strcpy(header, NoImage);
+
+	return 1;
+}
+
+/*
+ * prepare_signature
+ */
+
+static int prepare_signature(struct submit_params * header_page_info,
+		char * current_header)
+{
+	/* 
+	 * Explicitly put the \0 that clears the 'tried to resume from
+	 * this image before' flag.
+	 */
+	strncpy(current_header, HaveImage, sizeof(HaveImage));
+	current_header[resumed_before_byte] = 0;
+	return 0;
+}
+
+static void free_header_data(void)
+{
+	if (!first_header_submit_info)
+		return;
+
+	while (first_header_submit_info) {
+		struct submit_params * next = first_header_submit_info->next;
+		kfree(first_header_submit_info);
+		first_header_submit_info = next;
+	}
+	
+	suspend_message(SUSPEND_WRITER, SUSPEND_LOW, 1,
+			" Freed swap pages in free_header_data.\n");
+	first_header_submit_info = last_header_submit_info = NULL;
+	return;
+}
+
+static int filewriter_storage_available(void)
+{
+	int result = 0;
+
+	if (!target_inode)
+		return 0;
+
+	switch (target_type) {
+		case 0:
+		case 5:
+		case 6: /* Socket, Char, Fifi */
+			return -1;
+		case 2: /* Regular file: current size - holes + free space on part */
+			result = target_storage_available;
+			break;
+		case 3: /* Block device */
+			if (target_bdev->bd_disk) {
+				if (target_bdev->bd_part)
+					result = (unsigned long)target_bdev->bd_part->nr_sects >> (PAGE_SHIFT - 9);
+				else
+					result = (unsigned long)target_bdev->bd_disk->capacity >> (PAGE_SHIFT - 9);
+			} else {
+				printk("bdev->bd_disk null.\n");
+				return 0;
+			}
+	}
+
+	return result;
+}
+
+static int filewriter_storage_allocated(void)
+{
+	int result;
+
+	if (!target_inode)
+		return 0;
+
+	if (target_is_normal_file()) {
+		result = (int) target_storage_available;
+	} else
+		result = header_pages + main_pages;
+
+	return result;
+}
+
+static int filewriter_initialise(int starting_cycle)
+{
+	if (!starting_cycle)
+		return 0;
+
+	target_fd = sys_open(filewriter_target, O_RDWR, 0);
+
+	if (target_fd < 0) {
+		printk("Open file %s returned %d.\n", filewriter_target, target_fd);
+		return target_fd;
+	}
+
+	target_inode = current->files->fd[target_fd]->f_dentry->d_inode;
+	BUG_ON(target_bdev);
+	target_bdev = target_inode->i_bdev ? target_inode->i_bdev : target_inode->i_sb->s_bdev;
+	set_target_blocksize();
+	get_target_info();
+
+	return 0;
+}
+
+static void filewriter_cleanup(int finishing_cycle)
+{
+	if (target_bdev) {
+		if (used_devt) {
+			blkdev_put(target_bdev);
+			used_devt = 0;
+		}
+		target_bdev = NULL;
+		get_target_info();
+	}
+
+	if (!finishing_cycle)
+		return;
+
+	if (target_fd >= 0)
+		sys_close(target_fd);
+
+	target_fd = -1;
+}
+
+static int filewriter_release_storage(void)
+{
+	if ((TEST_ACTION_STATE(SUSPEND_KEEP_IMAGE)) && test_suspend_state(SUSPEND_NOW_RESUMING))
+		return 0;
+
+	/* Free metadata */
+	free_header_data();
+
+	put_extent_chain(&block_chain);
+
+	header_pages = main_pages = 0;
+	return 0;
+}
+
+static int filewriter_allocate_header_space(int space_requested)
+{
+	int i, j, pages_to_get;
+	int ret = 0;
+
+	/* We only steal pages from the main pool. If it doesn't have any yet... */
+	
+	if (!block_chain.first)
+		return 0;
+
+	pages_to_get = space_requested - header_pages;
+
+	if (pages_to_get < 1)
+		return 0;
+
+	for (i= header_pages; i < space_requested; i++) {
+		struct submit_params * new_submit_param;
+
+		/* Get a submit structure */
+		new_submit_param = kmalloc(sizeof(struct submit_params), GFP_ATOMIC);
+		
+		if (!new_submit_param) {
+			printk("Failed to kmalloc a struct submit param.\n");
+			ret = -ENOMEM;
+			goto out;
+		}
+
+		memset(new_submit_param, 0, sizeof(struct submit_params));
+
+		if (last_header_submit_info) {
+			last_header_submit_info->next = new_submit_param;
+			last_header_submit_info = new_submit_param;
+		} else
+			last_header_submit_info = first_header_submit_info =
+				new_submit_param;
+
+		for (j = 0; j < target_blocksperpage; j++) {
+			unsigned long newvalue;
+
+			/*
+			 *  Steal one from main extent chain. If, as a result,
+			 *  it is too small, more storage will be allocated or
+			 *  memory eaten.
+			 */
+
+			if (block_chain.first->minimum <
+					block_chain.first->maximum) {
+				newvalue = block_chain.first->minimum;
+				block_chain.first->minimum++;
+			} else {
+				struct extent * oldfirst =
+					block_chain.first;
+				block_chain.first = oldfirst->next;
+				block_chain.frees++;
+				if (block_chain.last == oldfirst)
+					block_chain.last = NULL;
+				newvalue = oldfirst->minimum;
+				put_extent(oldfirst);
+			}
+			
+			block_chain.size--;
+
+			new_submit_param->block[j] = newvalue;
+		}
+
+		new_submit_param->dev = target_bdev;
+		new_submit_param->readahead_index = -1;
+
+		header_pages++;
+
+		suspend_message(SUSPEND_WRITER, SUSPEND_MEDIUM, 0,
+			" Got header page %d/%d. Dev is %x. Block is %lu. "
+			"Target block size is %d.\n",
+			i, space_requested,
+			new_submit_param->dev,
+			new_submit_param->block[0],
+			new_submit_param->dev->bd_block_size);
+
+		if (!block_chain.size)
+			break;
+	}
+out:
+	return ret;
+}
+
+static int filewriter_allocate_storage(int space_requested)
+{
+	int result = 0;
+	int blocks_to_get = (space_requested << target_blockshift) - block_chain.size;
+	
+	/* Only release_storage reduces the size */
+	if (blocks_to_get < 1)
+		return 0;
+
+	main_pages = space_requested;
+
+	get_main_pool_phys_params();
+
+	suspend_message(SUSPEND_WRITER, SUSPEND_MEDIUM, 0,
+		"Finished with block_chain.size == %d.\n",
+		block_chain.size);
+
+	if (block_chain.size < ((header_pages + main_pages) << target_blockshift))
+		result = -ENOSPC;
+
+	return result;
+}
+
+static int filewriter_write_header_chunk(char * buffer, int buffer_size);
+static int filewriter_write_header_init(void)
+{
+	char new_sig[sig_size];
+	struct extent * extent;
+	
+	filewriter_buffer = (char *) get_zeroed_page(GFP_ATOMIC);
+	header_link =
+		(unsigned long *) (filewriter_buffer + BYTES_PER_HEADER_PAGE);
+	filewriter_page_index = 1;
+	filewriter_buffer_posn = 0;
+
+	current_header_submit_info = first_header_submit_info;
+	
+	/* We change it once the whole header is written */
+	strcpy(new_sig, NoImage);
+	filewriter_write_header_chunk(new_sig, sig_size);
+
+	/* Must calculate extent number before writing the header! */
+	filewriter_header_data.pd1start_extent_number = 1;
+	extent = block_chain.first;
+
+	while (extent != filewriter_header_data.pd1start_block_extent) {
+		filewriter_header_data.pd1start_extent_number++;
+		extent = extent->next;
+	}
+
+	/* Info needed to bootstrap goes at the start of the header.
+	 * First we save the 'header_data' struct, including the number
+	 * of header pages. Then we save the structs containing data needed
+	 * for reading the header pages back.
+	 * Note that even if header pages take more than one page, when we
+	 * read back the info, we will have restored the location of the
+	 * next header page by the time we go to use it.
+	 */
+	filewriter_write_header_chunk((char *) &filewriter_header_data, 
+			sizeof(filewriter_header_data));
+
+	return 0;
+}
+
+static int filewriter_write_header_chunk(char * buffer, int buffer_size)
+{
+	int bytes_left = buffer_size;
+	
+	/* 
+	 * We buffer the writes until a page is full and to use the last
+	 * sizeof(swp_entry_t) bytes for links between pages. This is 
+	 * totally transparent to the caller.
+	 *
+	 * Note also that buffer_size can be > PAGE_SIZE.
+	 */
+
+	suspend_message(SUSPEND_WRITER, SUSPEND_HIGH, 0,
+		"\nStart of write_header_chunk loop with %d bytes to store.\n",
+		buffer_size);
+
+	while (bytes_left) {
+		char * source_start = buffer + buffer_size - bytes_left;
+		char * dest_start = filewriter_buffer + filewriter_buffer_posn;
+		int dest_capacity = BYTES_PER_HEADER_PAGE - filewriter_buffer_posn;
+		sector_t next_header_page;
+		if (bytes_left <= dest_capacity) {
+			memcpy(dest_start, source_start, bytes_left);
+			filewriter_buffer_posn += bytes_left;
+			return 0;
+		}
+	
+		/* A page is full */
+		memcpy(dest_start, source_start, dest_capacity);
+		bytes_left -= dest_capacity;
+
+		BUG_ON(!current_header_submit_info);
+
+		if (!current_header_submit_info->next) {
+			*header_link = 0;
+		} else {
+			next_header_page =
+				current_header_submit_info->next->block[0];
+
+			*header_link = next_header_page;
+		}
+
+		suspend_message(SUSPEND_WRITER, SUSPEND_HIGH, 0,
+			"Writing header page %d. "
+			"Dev is %x. Block is %lu. Blocksperpage is %d. Bd_block_size is %d.\n",
+			filewriter_page_index,
+			current_header_submit_info->dev->bd_dev,
+			current_header_submit_info->block[0],
+			target_blocksperpage,
+			current_header_submit_info->dev->bd_block_size);
+		
+		current_header_submit_info->page =
+			virt_to_page(filewriter_buffer);
+		check_shift_keys(0, NULL);
+		suspend_bio_ops.submit_io(WRITE, current_header_submit_info, 0);
+
+		filewriter_buffer_posn = 0;
+		filewriter_page_index++;
+		current_header_submit_info = current_header_submit_info->next;
+	}
+
+	return 0;
+}
+
+static int filewriter_write_header_cleanup(void)
+{
+	/* Write any unsaved data */
+	if (filewriter_buffer_posn) {
+		*header_link = 0;
+
+		suspend_message(SUSPEND_WRITER, SUSPEND_HIGH, 0,
+			"Writing header page %d. "
+			"Dev is %x. Block is %lu. Blocksperpage is %d.\n",
+			filewriter_page_index,
+			current_header_submit_info->dev->bd_dev,
+			current_header_submit_info->block[0],
+			target_blocksperpage);
+		
+		current_header_submit_info->page =
+			virt_to_page(filewriter_buffer);
+		suspend_bio_ops.submit_io(WRITE, 
+				current_header_submit_info, 0);
+	}
+
+	suspend_bio_ops.finish_all_io();
+
+	/* Adjust image header */
+	suspend_bio_ops.bdev_page_io(READ, target_bdev, target_firstblock,
+			virt_to_page(filewriter_buffer));
+
+	prepare_signature(first_header_submit_info, filewriter_buffer);
+		
+	suspend_bio_ops.bdev_page_io(WRITE, target_bdev, target_firstblock,
+			virt_to_page(filewriter_buffer));
+
+	free_pages((unsigned long) filewriter_buffer, 0);
+	filewriter_buffer = NULL;
+	header_link = NULL;
+	
+	suspend_bio_ops.finish_all_io();
+
+	return 0;
+}
+
+/* ------------------------- HEADER READING ------------------------- */
+
+/*
+ * read_header_init()
+ * 
+ * Description:
+ * 1. Attempt to read the device specified with resume2=.
+ * 2. Check the contents of the swap header for our signature.
+ * 3. Warn, ignore, reset and/or continue as appropriate.
+ * 4. If continuing, read the filewriter configuration section
+ *    of the header and set up block device info so we can read
+ *    the rest of the header & image.
+ *
+ * Returns:
+ * May not return if user choose to reboot at a warning.
+ * -EINVAL if cannot resume at this time. Booting should continue
+ * normally.
+ */
+
+static int filewriter_read_header_init(void)
+{
+	filewriter_page_index = 1;
+
+	filewriter_buffer = (char *) get_zeroed_page(GFP_ATOMIC);
+	filewriter_buffer_posn = sig_size;
+
+	/* Read filewriter configuration */
+	suspend_bio_ops.bdev_page_io(READ, target_bdev, target_firstblock,
+			virt_to_page((unsigned long) filewriter_buffer));
+	
+	suspend_message(SUSPEND_WRITER, SUSPEND_HIGH, 0,
+		"Retrieving %d bytes from %x:%x to page %d, %p-%p.\n",
+		target_bdev->bd_dev, target_firstblock,
+		sizeof(filewriter_header_data),
+		filewriter_page_index,
+		filewriter_buffer, filewriter_buffer + sizeof(filewriter_header_data) - 1);
+	memcpy(&filewriter_header_data,
+			filewriter_buffer + filewriter_buffer_posn,
+			sizeof(filewriter_header_data));
+	
+	filewriter_buffer_posn += sizeof(filewriter_header_data);
+
+	return 0;
+}
+
+static int filewriter_read_header_chunk(char * buffer, int buffer_size)
+{
+	int bytes_left = buffer_size, ret = 0;
+	
+	/* Read a chunk of the header */
+	while ((bytes_left) && (!ret)) {
+		sector_t next =
+		   *((sector_t *) (filewriter_buffer + BYTES_PER_HEADER_PAGE));
+		char * dest_start = buffer + buffer_size - bytes_left;
+		char * source_start =
+			filewriter_buffer + filewriter_buffer_posn;
+		int source_capacity =
+			BYTES_PER_HEADER_PAGE - filewriter_buffer_posn;
+
+		if (bytes_left <= source_capacity) {
+			memcpy(dest_start, source_start, bytes_left);
+			filewriter_buffer_posn += bytes_left;
+			return buffer_size;
+		}
+
+		/* Next to read the next page */
+		memcpy(dest_start, source_start, source_capacity);
+		bytes_left -= source_capacity;
+
+		filewriter_page_index++;
+
+		suspend_bio_ops.bdev_page_io(READ, target_bdev,
+				next, virt_to_page(filewriter_buffer));
+
+		filewriter_buffer_posn = 0;
+	}
+
+	return buffer_size - bytes_left;
+}
+
+static int filewriter_read_header_cleanup(void)
+{
+	free_pages((unsigned long) filewriter_buffer, 0);
+	return 0;
+}
+
+static int filewriter_serialise_extents(void)
+{
+	serialise_extent_chain(&block_chain);
+	return 0;
+}
+
+static int filewriter_load_extents(void)
+{
+	int i = 1;
+	struct extent * extent;
+	
+	load_extent_chain(&block_chain);
+
+	extent = block_chain.first;
+
+	while (i < filewriter_header_data.pd1start_extent_number) {
+		extent = extent->next;
+		i++;
+	}
+
+	filewriter_header_data.pd1start_block_extent = extent;
+
+	return 0;
+}
+
+static int filewriter_write_init(int stream_number)
+{
+	if (stream_number == 1) {
+		currentblockextent = filewriter_header_data.pd1start_block_extent;
+		currentblockoffset = filewriter_header_data.pd1start_block_offset;
+	} else {
+		currentblockextent = block_chain.first;
+		currentblockoffset = currentblockextent->minimum;
+	}
+
+	BUG_ON(!currentblockextent);
+
+	filewriter_page_index = 1;
+	current_stream = stream_number;
+
+	suspend_bio_ops.reset_io_stats();
+
+	return 0;
+}
+
+static int filewriter_write_chunk(struct page * buffer_page)
+{
+	int i;
+	struct submit_params submit_params;
+
+	BUG_ON(!currentblockextent);
+	submit_params.readahead_index = -1;
+	submit_params.page = buffer_page;
+	submit_params.dev = target_bdev;
+		
+	/* Get the blocks */
+	for (i = 0; i < target_blocksperpage; i++) {
+		submit_params.block[i] = currentblockoffset;
+		GET_EXTENT_NEXT(currentblockextent, currentblockoffset);
+	}
+
+	if(!submit_params.block[0])
+		return -EIO;
+
+	if (TEST_ACTION_STATE(SUSPEND_TEST_FILTER_SPEED))
+		return 0;
+		
+	suspend_bio_ops.submit_io(WRITE, &submit_params, 0);
+
+	filewriter_page_index++;
+
+	return 0;
+}
+
+static int filewriter_write_cleanup(void)
+{
+	if (current_stream == 2) {
+		filewriter_header_data.pd1start_block_extent = currentblockextent;
+		filewriter_header_data.pd1start_block_offset = currentblockoffset;
+	}
+	
+	suspend_bio_ops.finish_all_io();
+	
+	suspend_bio_ops.check_io_stats();
+
+	return 0;
+}
+
+static int filewriter_read_init(int stream_number)
+{
+	if (stream_number == 1) {
+		currentblockextent = filewriter_header_data.pd1start_block_extent;
+		currentblockoffset = filewriter_header_data.pd1start_block_offset;
+	} else {
+		currentblockextent = NULL;
+		currentblockoffset = 0;
+		currentblockextent =
+			block_chain.first;
+		currentblockoffset = currentblockextent->minimum;
+	}
+
+	BUG_ON(!currentblockextent);
+
+	filewriter_page_index = 1;
+
+	suspend_bio_ops.reset_io_stats();
+
+	readahead_index = readahead_submit_index = -1;
+	readahead_allocs = readahead_frees = 0;
+
+	return 0;
+}
+
+static int filewriter_begin_read_chunk(struct page * page, 
+		int readahead_index, int sync)
+{
+	int i;
+	struct submit_params submit_params;
+
+	BUG_ON(!currentblockextent);
+	
+	submit_params.readahead_index = readahead_index;
+	submit_params.page = page;
+	submit_params.dev = target_bdev;
+		
+	/* Get the blocks. There is no chance that they span chains. */
+	for (i = 0; i < target_blocksperpage; i++) {
+		submit_params.block[i] = currentblockoffset;
+		GET_EXTENT_NEXT(currentblockextent, currentblockoffset);
+	}
+
+	if ((i = suspend_bio_ops.submit_io(READ, &submit_params, sync)))
+		return -EPERM;
+
+	filewriter_page_index++;
+
+	check_shift_keys(0, NULL);
+
+	return 0;
+}
+
+/* Note that we ignore the sync parameter. We are implementing
+ * read ahead, and will always wait until our readhead buffer has
+ * been read before returning.
+ */
+
+static int filewriter_read_chunk(struct page * buffer_page, int sync)
+{
+	static int last_result;
+	unsigned long * virt;
+
+	if (sync == SUSPEND_ASYNC)
+		return filewriter_begin_read_chunk(buffer_page, -1, sync);
+
+	/* Start new readahead while we wait for our page */
+	if (readahead_index == -1) {
+		last_result = 0;
+		readahead_index = readahead_submit_index = 0;
+	}
+
+	/* Start a new readahead? */
+	if (last_result) {
+		/* We failed to submit a read, and have cleaned up
+		 * all the readahead previously submitted */
+		if (readahead_submit_index == readahead_index)
+			return -EPERM;
+		goto wait;
+	}
+	
+	do {
+		if (suspend_bio_ops.prepare_readahead(readahead_submit_index))
+			break;
+
+		readahead_allocs++;
+
+		last_result = filewriter_begin_read_chunk(
+			suspend_bio_ops.readahead_pages[readahead_submit_index], 
+			readahead_submit_index, SUSPEND_ASYNC);
+		if (last_result) {
+			printk("Begin read chunk for page %d returned %d.\n",
+				readahead_submit_index, last_result);
+			suspend_bio_ops.cleanup_readahead(readahead_submit_index);
+			break;
+		}
+
+		readahead_submit_index++;
+
+		if (readahead_submit_index == MAX_READAHEAD)
+			readahead_submit_index = 0;
+
+	} while((!last_result) && (readahead_submit_index != readahead_index) &&
+			(!suspend_bio_ops.readahead_ready(readahead_index)));
+
+wait:
+	suspend_bio_ops.wait_on_readahead(readahead_index);
+
+	virt = kmap_atomic(buffer_page, KM_USER1);
+	memcpy(virt, page_address(suspend_bio_ops.readahead_pages[readahead_index]),
+			PAGE_SIZE);
+	kunmap_atomic(virt, KM_USER1);
+
+	suspend_bio_ops.cleanup_readahead(readahead_index);
+
+	readahead_frees++;
+
+	readahead_index++;
+	if (readahead_index == MAX_READAHEAD)
+		readahead_index = 0;
+
+	return 0;
+}
+
+static int filewriter_read_cleanup(void)
+{
+	suspend_bio_ops.finish_all_io();
+	while (readahead_index != readahead_submit_index) {
+		suspend_bio_ops.cleanup_readahead(readahead_index);
+		readahead_frees++;
+		readahead_index++;
+		if (readahead_index == MAX_READAHEAD)
+			readahead_index = 0;
+	}
+	suspend_bio_ops.check_io_stats();
+	BUG_ON(readahead_allocs != readahead_frees);
+	return 0;
+}
+
+/* filewriter_invalidate_image
+ * 
+ */
+static int filewriter_invalidate_image(void)
+{
+	char * cur;
+	int result = 0;
+	
+	cur = (char *) get_zeroed_page(GFP_ATOMIC);
+	if (!cur) {
+		printk("Unable to allocate a page for restoring the image signature.\n");
+		return -ENOMEM;
+	}
+
+	/*
+	 * If nr_suspends == 0, we must be booting, so no swap pages
+	 * will be recorded as used yet.
+	 */
+
+	if (nr_suspends > 0)
+		filewriter_release_storage();
+
+	/* 
+	 * We don't do a sanity check here: we want to restore the swap 
+	 * whatever version of kernel made the suspend image.
+	 * 
+	 * We need to write swap, but swap may not be enabled so
+	 * we write the device directly
+	 */
+	
+	suspend_bio_ops.bdev_page_io(READ, target_bdev,
+			target_firstblock, virt_to_page(cur));
+
+	result = parse_signature(cur, 1);
+		
+	if (result == -1)
+		goto out;
+
+	strcpy(cur, NoImage);
+	cur[resumed_before_byte] = 0;
+
+	suspend_bio_ops.bdev_page_io(WRITE, target_bdev, target_firstblock,
+			virt_to_page(cur));
+
+	if (!nr_suspends)
+		printk(KERN_WARNING name_suspend "Image invalidated.\n");
+out:
+	suspend_bio_ops.finish_all_io();
+	free_pages((unsigned long) cur, 0);
+	return 0;
+}
+
+/*
+ * workspace_size
+ *
+ * Description:
+ * Returns the number of bytes of RAM needed for this
+ * code to do its work. (Used when calculating whether
+ * we have enough memory to be able to suspend & resume).
+ *
+ */
+static unsigned long filewriter_memory_needed(void)
+{
+	return 0;
+}
+
+/* Print debug info
+ *
+ * Description:
+ */
+
+static int filewriter_print_debug_stats(char * buffer, int size)
+{
+	int len = 0;
+	struct sysinfo sysinfo;
+	
+	if (active_writer != &filewriterops) {
+		len = suspend_snprintf(buffer, size, "- Filewriter inactive.\n");
+		return len;
+	}
+
+	len = suspend_snprintf(buffer, size, "- Filewriter active.\n");
+
+	si_swapinfo(&sysinfo);
+	
+	len+= suspend_snprintf(buffer+len, size-len, "  Storage available for image: %ld pages.\n",
+			sysinfo.freeswap + filewriter_storage_allocated());
+
+	return len;
+	return 0;
+}
+
+/*
+ * Storage needed
+ *
+ * Returns amount of space in the image header required
+ * for the filewriter's data.
+ *
+ * We ensure the space is allocated, but actually save the
+ * data from write_header_init and therefore don't also define a
+ * save_config_info routine.
+ */
+static unsigned long filewriter_storage_needed(void)
+{
+	return strlen(filewriter_target) + 1;
+}
+
+/*
+ * Image_exists
+ *
+ */
+
+static int filewriter_image_exists(void)
+{
+	int signature_found;
+	char * diskpage;
+	
+	if (try_to_open_target_device())
+		return 0;
+
+	diskpage = (char *) get_zeroed_page(GFP_ATOMIC);
+
+	/* FIXME: Make sure bdev_page_io handles wrong parameters */
+	suspend_bio_ops.bdev_page_io(READ, target_bdev,
+			target_firstblock, virt_to_page(diskpage));
+	suspend_bio_ops.finish_all_io();
+	signature_found = parse_signature(diskpage, 0);
+	free_pages((unsigned long) diskpage, 0);
+
+	if (!signature_found) {
+		return 0;	/* non fatal error */
+	} else if (signature_found == -1) {
+		printk(KERN_ERR name_suspend
+			"Unable to find a signature. Could you have moved "
+			"the file?\n");
+		return 0;
+	}
+
+	return 1;
+}
+
+/*
+ * Mark resume attempted.
+ *
+ * Record that we tried to resume from this image.
+ */
+
+static void filewriter_mark_resume_attempted(void)
+{
+	char * diskpage;
+	int signature_found;
+	
+	if (!target_dev_t) {
+		printk("Not even trying to record attempt at resuming"
+				" because target_dev_t is not set.\n");
+		return;
+	}
+	
+	diskpage = (char *) get_zeroed_page(GFP_ATOMIC);
+
+	/* FIXME: Make sure bdev_page_io handles wrong parameters */
+	suspend_bio_ops.bdev_page_io(READ, target_bdev, target_firstblock, virt_to_page(diskpage));
+	signature_found = parse_signature(diskpage, 0);
+
+	switch (signature_found) {
+		case 1:
+			diskpage[resumed_before_byte] |= 1;
+			break;
+	}
+	
+	suspend_bio_ops.bdev_page_io(WRITE, target_bdev, target_firstblock,
+			virt_to_page(diskpage));
+	suspend_bio_ops.finish_all_io();
+	free_pages((unsigned long) diskpage, 0);
+	return;
+}
+
+/*
+ * Parse Image Location
+ *
+ * Attempt to parse a resume2= parameter.
+ * Swap Writer accepts:
+ * resume2=swap:DEVNAME[:FIRSTBLOCK][@BLOCKSIZE]
+ *
+ * Where:
+ * DEVNAME is convertable to a dev_t by name_to_dev_t
+ * FIRSTBLOCK is the location of the first block in the swap file
+ * (specifying for a swap partition is nonsensical but not prohibited).
+ * BLOCKSIZE is the logical blocksize >= 512 & <= PAGE_SIZE, 
+ * mod 512 == 0 of the device.
+ * Data is validated by attempting to read a swap header from the
+ * location given. Failure will result in filewriter refusing to
+ * save an image, and a reboot with correct parameters will be
+ * necessary.
+ */
+
+static int filewriter_parse_image_location(char * commandline, int only_writer)
+{
+	char *thischar, *devstart = NULL, *colon = NULL, *at_symbol = NULL;
+	char * diskpage = NULL;
+	int signature_found, result = -EINVAL, temp_result;
+
+	if (strncmp(commandline, "file:", 5)) {
+		if (!only_writer)
+			return 1;
+	} else
+		commandline += 5;
+
+	devstart = thischar = commandline;
+	while ((*thischar != ':') && (*thischar != '@') &&
+		((thischar - commandline) < 250) && (*thischar))
+		thischar++;
+
+	if (*thischar == ':') {
+		colon = thischar;
+		*colon = 0;
+		thischar++;
+	}
+
+	while ((*thischar != '@') && ((thischar - commandline) < 250) && (*thischar))
+		thischar++;
+
+	if (*thischar == '@') {
+		at_symbol = thischar;
+		*at_symbol = 0;
+	}
+	
+	if (colon)
+		target_firstblock = (int) simple_strtoul(colon + 1, NULL, 0);
+	else
+		target_firstblock = 0;
+
+	if (at_symbol) {
+		target_blocksize = (int) simple_strtoul(at_symbol + 1, NULL, 0);
+		if (target_blocksize & 0x1FF)
+			printk("Filewriter: Blocksizes are usually a multiple of 512. Don't expect this to work!\n");
+	} else
+		target_blocksize = 4096;
+	
+	temp_result = try_to_parse_target_dev_t(devstart);
+
+	if (colon)
+		*colon = ':';
+	if (at_symbol)
+		*at_symbol = '@';
+
+	if (temp_result)
+		goto out;
+
+	diskpage = (char *) get_zeroed_page(GFP_ATOMIC);
+	temp_result = suspend_bio_ops.bdev_page_io(READ, target_bdev, target_firstblock, virt_to_page(diskpage));
+
+	suspend_bio_ops.finish_all_io();
+	
+	if (temp_result) {
+		printk(KERN_ERR name_suspend "Filewriter: Failed to submit I/O.\n");
+		goto out;
+	}
+
+	signature_found = parse_signature(diskpage, 0);
+
+	if (signature_found != -1) {
+		printk(KERN_ERR name_suspend "Filewriter: File signature found.\n");
+		result = 0;
+	} else
+		printk(KERN_ERR name_suspend "Filewriter: Sorry. No signature found at specified location.\n");
+
+out:
+	if (diskpage)
+		free_page((unsigned long) diskpage);
+	return result;
+}
+
+static void set_filewriter_target(int test_it)
+{
+	char * buffer = (char *) get_zeroed_page(GFP_ATOMIC);
+	char * buffer2 = (char *) get_zeroed_page(GFP_ATOMIC);
+	int offset = 0, fd;
+	int sector;
+
+	if(target_bdev)
+		filewriter_cleanup(0);
+
+	fd = sys_open(filewriter_target, O_RDONLY, 0);
+
+	if (fd < 0) {
+		printk("Filewriter: Unable to open %s.\n", filewriter_target);
+		goto cleanup1;
+	}
+
+	*resume2_file = 0;
+	
+	target_inode = current->files->fd[fd]->f_dentry->d_inode;
+	target_type = get_target_type(target_inode);
+	
+	if (!target_is_usable) {
+		printk("Filewriter: %s is a link or directory. You can't suspend to them!\n",
+				filewriter_target);
+		goto cleanup2;
+	}
+
+	target_bdev = target_inode->i_bdev ? target_inode->i_bdev : target_inode->i_sb->s_bdev;
+	sector = bmap(target_inode, 0);
+
+	if (target_bdev && sector) {
+
+		target_blkbits = target_bdev->bd_inode->i_blkbits;
+
+		suspend_bio_ops.bdev_page_io(READ, target_bdev, sector,
+			virt_to_page(buffer));
+
+		bdevname(target_bdev, buffer2);
+		offset += snprintf(buffer + offset, PAGE_SIZE - offset, 
+				"/dev/%s", buffer2);
+		
+		if (sector)
+			offset += snprintf(buffer + offset, PAGE_SIZE - offset,
+				":0x%x", sector);
+
+		if (target_inode->i_sb->s_blocksize != PAGE_SIZE)
+			offset += snprintf(buffer + offset, PAGE_SIZE - offset,
+				"@%lu", target_inode->i_sb->s_blocksize);
+		
+	} else
+		offset += snprintf(buffer + offset, PAGE_SIZE - offset,
+				"%s is not a valid target.", filewriter_target);
+			
+	sprintf(resume2_file, "file:%s", buffer);
+
+cleanup2:
+	if (test_it)
+		sys_close(fd);
+
+cleanup1:
+	free_pages((unsigned long) buffer, 0);
+	free_pages((unsigned long) buffer2, 0);
+}
+
+/* filewriter_save_config_info
+ *
+ * Description:	Save the target's name, not for resume time, but for all_settings.
+ * Arguments:	Buffer:		Pointer to a buffer of size PAGE_SIZE.
+ * Returns:	Number of bytes used for saving our data.
+ */
+
+static int filewriter_save_config_info(char * buffer)
+{
+	strcpy(buffer, filewriter_target);
+	return strlen(filewriter_target) + 1;
+}
+
+/* filewriter_load_config_info
+ *
+ * Description:	Reload target's name.
+ * Arguments:	Buffer:		Pointer to the start of the data.
+ *		Size:		Number of bytes that were saved.
+ */
+
+static void filewriter_load_config_info(char * buffer, int size)
+{
+	strcpy(filewriter_target, buffer);
+}
+
+static void test_filewriter_target(void)
+{
+	set_filewriter_target(1);
+}
+
+extern void attempt_to_parse_resume_device(void);
+
+static struct suspend_proc_data filewriter_proc_data[] = {
+
+	{
+	 .filename			= "filewriter_target",
+	 .permissions			= PROC_RW,
+	 .type				= SUSPEND_PROC_DATA_STRING,
+	 .data = {
+		 .string = {
+			 .variable	= filewriter_target,
+			 .max_length	= 256,
+		 }
+	 },
+	 .write_proc			= test_filewriter_target,
+	},
+
+	{ .filename			= "disable_filewriter",
+	  .permissions			= PROC_RW,
+	  .type				= SUSPEND_PROC_DATA_INTEGER,
+	  .data = {
+		.integer = {
+			.variable	= &filewriterops.disabled,
+			.minimum	= 0,
+			.maximum	= 1,
+		}
+	  },
+	  .write_proc			= attempt_to_parse_resume_device,
+	}
+};
+
+static struct suspend_plugin_ops filewriterops = {
+	.type					= WRITER_PLUGIN,
+	.name					= "File Writer",
+	.module					= THIS_MODULE,
+	.memory_needed				= filewriter_memory_needed,
+	.print_debug_info			= filewriter_print_debug_stats,
+	.save_config_info			= filewriter_save_config_info,
+	.load_config_info			= filewriter_load_config_info,
+	.storage_needed				= filewriter_storage_needed,
+	.initialise				= filewriter_initialise,
+	.cleanup				= filewriter_cleanup,
+
+	.write_init				= filewriter_write_init,
+	.write_cleanup				= filewriter_write_cleanup,
+	.read_init				= filewriter_read_init,
+	.read_cleanup				= filewriter_read_cleanup,
+
+	.ops = {
+		.writer = {
+		 .write_chunk		= filewriter_write_chunk,
+		 .read_chunk		= filewriter_read_chunk,
+		 .noresume_reset	= filewriter_noresume_reset,
+		 .storage_available 	= filewriter_storage_available,
+		 .storage_allocated	= filewriter_storage_allocated,
+		 .release_storage	= filewriter_release_storage,
+		 .allocate_header_space	= filewriter_allocate_header_space,
+		 .allocate_storage	= filewriter_allocate_storage,
+		 .image_exists		= filewriter_image_exists,
+		 .mark_resume_attempted	= filewriter_mark_resume_attempted,
+		 .write_header_init	= filewriter_write_header_init,
+		 .write_header_chunk	= filewriter_write_header_chunk,
+		 .write_header_cleanup	= filewriter_write_header_cleanup,
+		 .read_header_init	= filewriter_read_header_init,
+		 .read_header_chunk	= filewriter_read_header_chunk,
+		 .read_header_cleanup	= filewriter_read_header_cleanup,
+		 .serialise_extents	= filewriter_serialise_extents,
+		 .load_extents		= filewriter_load_extents,
+		 .invalidate_image	= filewriter_invalidate_image,
+		 .parse_image_location	= filewriter_parse_image_location,
+		}
+	}
+};
+
+/* ---- Registration ---- */
+static __init int filewriter_load(void)
+{
+	int result;
+	int i, numfiles = sizeof(filewriter_proc_data) / sizeof(struct suspend_proc_data);
+	
+	printk("Software Suspend FileWriter loading.\n");
+
+	if (!(result = suspend_register_plugin(&filewriterops))) {
+		for (i=0; i< numfiles; i++)
+			suspend_register_procfile(&filewriter_proc_data[i]);
+	} else
+		printk("Software Suspend FileWriter unable to register!\n");
+	return result;
+}
+
+#ifdef MODULE
+static __exit void filewriter_unload(void)
+{
+	int i, numfiles = sizeof(filewriter_proc_data) / sizeof(struct suspend_proc_data);
+
+	printk("Software Suspend FileWriter unloading.\n");
+
+	for (i=0; i< numfiles; i++)
+		suspend_unregister_procfile(&filewriter_proc_data[i]);
+	suspend_unregister_plugin(&filewriterops);
+}
+
+module_init(filewriter_load);
+module_exit(filewriter_unload);
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Nigel Cunningham");
+MODULE_DESCRIPTION("Suspend2 filewriter");
+#else
+late_initcall(filewriter_load);
+#endif

-
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to [email protected]
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Please read the FAQ at  http://www.tux.org/lkml/

[Index of Archives]     [Kernel Newbies]     [Netfilter]     [Bugtraq]     [Photo]     [Gimp]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Video 4 Linux]     [Linux for the blind]
  Powered by Linux