next up previous contents index
Next: 代替printk Up: Linux 内核模块编程 Previous: 系统调用

     
阻塞进程

当某人要求你什么事而你当时不能时你在做什么?如果你是人而你被别人打扰,你唯一能说的是 :‘现在不行,我正忙着呢。 走开!’。但是如果你是一个内核模块而你被一个进程打扰,你有另外的可能。你可以让那个进程睡眠直到你能为它服务。毕竟,内核可以让进程睡眠并且可以随时唤醒它(那就是在单CPU上呈现同一时间多个进程运行的方式)。    

这个内核模块就是这样的例子。那个文件(被称为 /proc/sleep)在同一时间只能被一个进程打开。如果那个文件已经打开了,内核模块调用module_interruptible_sleep_on8.1。这个函数改变那个任务(任何任务是包含有关进程的信息和系统调用的内核的一种数据结构)的状态为TASK_INTERRUPTIBLE,它的意思是任务不能运行,除非它被唤醒。并且该任务被加入 WaitQ-- 等待访问该文件的任务队列。然后函数调用调度程序进行上下文转换到一个还要使用CPU的不同的进程。        

当一个进程用完该文件,它关闭该文件,然后module_close 被调用。那个函数唤醒队列中的所有进程(没有机制只唤醒其中的一个)。然后它返回而刚刚关闭该文件的进程可以继续运行。调度程序及时地决定那个进程已经用了够多的时间而将CPU的控制权交给另一个进程。最后,队列中的某个进程会获得调度程序赋予的CPU的控制权。它正好在对module_interruptible_sleep_on8.2的调用后开始。然后它能继续设置一个全局变量以告诉所有其他进程该文件仍然打开,它们将继续它们等待的生活。当另一个进程得到CPU时间片,它们将看到那个全局变量而继续去睡眠。        

为了使我们的生活更有趣, module_close 没有唤醒等待访问该文件的进程的垄断权。一个信号,例如Ctrl-C (SIGINT)也可以唤醒一个进程8.3。在那种情况下,我们想立即用 -EINTR 返回。这是很重要的,例如用户因此可以在进程收到该文件前将它杀死。        

还有一点需要记住。有时候进程不想睡眠,它们想要么立即得到它们需要的要么被告知它不能完成。当文件打开时这类进程使用 O_NONBLOCK 标志。例如当文件打开时,内核被假设从阻塞操作中返回错误代码-EAGAIN作为回应。在这章的源目录中有的程序 cat_noblock 能用O_NONBLOCK打开文件。        

范例 sleep.c   

 
/* sleep.c - 创建一个 /proc 文件,而如果几个进程同时试图打开它,除了一个外使其他所有的睡眠 */


/* Copyright (C) 1998-99 by Ori Pomerantz */


/* 必要头文件 */

/* 标准头文件 */
#include <linux/kernel.h>   /* 内核工作 */
#include <linux/module.h>   /* 明确指定是模块 */

/* 处理 CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif        

/* 使用 proc 文件系统所必须的 */
#include <linux/proc_fs.h>

/* 为了使进程睡眠和唤醒 */
#include <linux/sched.h>
#include <linux/wrapper.h>



/*  2.2.3 版/usr/include/linux/version.h 包含该宏
 * 但 2.0.35 版不包含--假如以备需要 */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif


#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
#include <asm/uaccess.h>  /* 为了得到 get_user 和 put_user */
#endif


/* 模块的文件函数 ********************** */


/* 保存收到的上一个消息以证明能够处理输入 */
#define MESSAGE_LENGTH 80
static char Message[MESSAGE_LENGTH];


/* 既然使用文件操作结构,我们就不能特殊的 proc 输出 - 我们不得不使用这个标准的读函数 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t module_output(
    struct file *file,   /* 要读的文件 */
    char *buf, /* 放置数据的缓冲区(在用户内存段) */
    size_t len,  /* 缓冲区长度 */
    loff_t *offset) /* 文件偏移量 - 忽略 */
#else
static int module_output(
    struct inode *inode, /* 要读的节点 */
    struct file *file,   /* 要读的文件 */
    char *buf, /* 放置数据的缓冲区(在用户内存段) */
    int len)  /* 缓冲区长度 */
#endif
{
  static int finished = 0;
  int i;
  char message[MESSAGE_LENGTH+30];

  /* 返回0指明文件尾--没有更多说的 */
  if (finished) {
    finished = 0;
    return 0;
  }

  /* 如果你到现在还不懂这个,你没有希望成为内核程序员 */
  sprintf(message, "Last input:%s\n", Message);
  for(i=0; i<len && message[i]; i++) 
    put_user(message[i], buf+i);

  finished = 1;
  return i;  /* 返回‘读’的字节数 */
}


/* 当用户向 /proc 文件写说这个函数接收用户输入 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t module_input(
    struct file *file,   /* 文件自己 */
    const char *buf,     /* 带有输入的缓冲区 */
    size_t length,       /* 缓冲区长度 */
    loff_t *offset)      /* 文件偏移量-忽略 */
#else
static int module_input(
    struct inode *inode, /* 文件的节点 */
    struct file *file,   /* 文件自己 */
    const char *buf,     /* 带有输入的缓冲区 */
    int length)          /* 缓冲区长度 */
#endif
{
  int i;

  /* 将输入放入 Message, module_output 随后将能使用它 */
  for(i=0; i<MESSAGE_LENGTH-1 && i<length; i++)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
     get_user(Message[i], buf+i);
#else
  Message[i] = get_user(buf+i);
#endif
/* 我们需要一个标准的以0终止的字符串 */
  Message[i] = '\0';  
  
  /* 返回使用过的输入的字符数 */
  return i;
}

/*  如果文件当前被某人打开则为1 */
int Already_Open = 0;

/* 需要我们的文件的进程队列 */
static struct wait_queue *WaitQ = NULL;


/* 当 /proc 文件被打开时被调用 */
static int module_open(struct inode *inode,
                       struct file *file)
{
  /* 如果文件的标志包含 O_NONBLOCK 则意味着进程不想等待该文件。
   * 在这种情况下,如果文件已经打开,我们将用返回 -EAGAIN 表示失败,意思是“你将再试一次”
   * 而不让宁愿保持醒状态的进程阻塞。 */
  if ((file->f_flags & O_NONBLOCK) && Already_Open) 
    return -EAGAIN;

  /* 这是放置 MOD_INC_USE_COUNT 的合适的位置,因为如果一个进程处于内核模块的循环中,
   * 内核模块不应被清除。 */
  MOD_INC_USE_COUNT;

  /* 如果文件已经打开,等待,直到它不再如此 */
  while (Already_Open) 
  {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
    int i, is_sig=0;
#endif

    /* 该函数使当前的进程睡眠,包括任何系统调用,例如我们的。
     * 执行将在函数调用后恢复,要么因为某人调用了 wake_up(&WaitQ) (只有当文件被关闭时
     * 由 module_close 做这个),要么诸如 Ctrl-C 之类的信号发送到进程 */
    module_interruptible_sleep_on(&WaitQ);
 
    /* 如果因为得到一个信号我们醒来,我们不再阻塞,返回 -EINTR (系统调用失败)。 
     * 这允许进程被杀死或停止。 */



/*
 * Emmanuel Papirakis:
 *
 * 在 2.2.* 上有一点更新。信号现在被包含在一个双字中(64 位) 并被存储在一个
 * 包含双无符号长整型数组的结构中。必要的话我们需要做两次检查。
 *
 * Ori Pomerantz:
 *
 * 没有人向我许诺他们决不使用多于64位,或者这本书不在16位字长的 Linux 版本上使用。
 * 这些代码无论如何都可以工作。
 */	  
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)

    for(i=0; i<_NSIG_WORDS && !is_sig; i++)
      is_sig = current->signal.sig[i] & 
        ~current->blocked.sig[i];
    if (is_sig) {
#else
    if (current->signal & ~current->blocked) {
#endif
      /* 在这儿放置MOD_DEC_USE_COUNT 是很重要的,因为对于进程打开操作被中断的地方永远相应关闭
       * 如果我们不在这儿减小使用计数,我们将留下一个正的使用计数而他没有办法降为0而
       * 给我们一个不朽的只能通过重新启动机器杀死的模块。 */
      MOD_DEC_USE_COUNT;
      return -EINTR;
    }
  }

  /* 如果我们到这儿, Already_Open 必须为0 */

  /* 打开文件 */
  Already_Open = 1;
  return 0;  /* 允许访问 */
}



/* 当 /proc 文件被关闭时调用 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
int module_close(struct inode *inode, struct file *file)
#else
void module_close(struct inode *inode, struct file *file)
#endif
{
  /* 将 Already_Open 设为0,所以等待队列 WaitQ 中的某个进程能将 Already_Open设回1以打开文件。
   * 所有的其他进程在 Already_Open 为1时被调用而它们将回去继续睡眠。 */
  Already_Open = 0;

  /* 唤醒等待队列 WaitQ中的所有进程,所以如果某个正在等待该文件,它们能有机会。 */
  module_wake_up(&WaitQ);

  MOD_DEC_USE_COUNT;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
  return 0;  /* success */
#endif
}




/* 这个函数决定是否允许一个操作(返回0表示允许,返回非零数用于指明为什么不允许)。
 *
 * 操作可以是下面的某个值:
 * 0 - 执行(运行“文件” - 在我们的例子中没有意义)
 * 2 - 写(向内核输入)
 * 4 - 读(从内核输出)
 *
 * 这是检查文件权限的实际的函数。用 ls -l 返回的权限只作参考并且可以被忽略。
 */
static int module_permission(struct inode *inode, int op)
{
  /* 允许任何人从模块中读,但只有 root (uid 为0) 可以写 */ 
  if (op == 4 || (op == 2 && current->euid == 0))
    return 0; 

  /* 如果是其他任何值,访问被拒绝。 */
  return -EACCES;
}


/* 作为 /proc 文件登记的结构,包含所有相关函数指针 */


/* 对我们的 proc 文件的文件操作。这是放置当某人试图对我们的文件做什么时将被调用的函数的指针的地方 
 * NULL 意味着我们不想处理某些事。 */
static struct file_operations File_Ops_4_Our_Proc_File =
  {
    NULL,  /* lseek */
    module_output,  /* 从文件“读” */
    module_input,   /* 向文件“写” */
    NULL,  /* 读目录 */
    NULL,  /* 选择 */
    NULL,  /* ioctl */
    NULL,  /* mmap */
    module_open,/* 当 /proc 文件被打开时调用 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
    NULL,   /* 刷新 */
#endif
    module_close      /* 当它被关闭时调用 */
  };



/* 对我们的proc文件的节点操作。我们需要它,因此我们将在某个地方指明我们想使用的文件操作结构及
 * 用于权限的函数。也可能指定对节点进行操作的任何其他函数(我们不关心但放置 NULL)。 */
static struct inode_operations Inode_Ops_4_Our_Proc_File =
  {
    &File_Ops_4_Our_Proc_File,
    NULL, /* 创建 */
    NULL, /* 查找 */
    NULL, /* 连接 */
    NULL, /* 删除连接 */
    NULL, /* 符号连接 */
    NULL, /* 建目录 */
    NULL, /* 删除目录 */
    NULL, /* mknod */
    NULL, /* 更名 */
    NULL, /* 读连接 */
    NULL, /* 跟随连接 */
    NULL, /* 读页 */
    NULL, /* 写页 */
    NULL, /* bmap */
    NULL, /* 截短 */
    module_permission /* 权限检查 */
  };

/* 目录项 */
static struct proc_dir_entry Our_Proc_File = 
  {
    0, /* 节点数-忽略,将被 proc_register[_dynamic] 填充*/
    5, /* 文件名长度 */
    "sleep", /* 文件名 */
    S_IFREG | S_IRUGO | S_IWUSR, 
    /* 文件模式-这是一个可以被其拥有者,用户组及其他人读的普通。当然其拥有者也可以写。
     *
     * 实际上这个成员只用于引用,它的 module_permission 进行实际的检查。
     * 它可以使用这个成员,但在我们的实现中为了简单而没有用。 */
    1,  /* 连接数 (文件被引用的目录) */
    0, 0,  /* 文件的UID和GID - 赋予 root */
    80, /* 由 ls 报告的文件长度。 */
    &Inode_Ops_4_Our_Proc_File, 
    /* 如果需要则是文件节点结构的指针。在我们的实现中我们需要一个写函数。 */
    NULL  /* 文件的读函数。不相关,我们已经将它放置在上面的节点结构中。 */
  }; 



/* 初始化和清除模块 **************** */


/* 初始化模块 - 登记 proc 文件 */
int init_module()
{
  /* 如果 proc_register_dynamic 成功则成功,否则失败 */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
  return proc_register(&proc_root, &Our_Proc_File);
#else
  return proc_register_dynamic(&proc_root, &Our_Proc_File);
#endif 

  /* proc_root 是 proc 文件系统的根目录(/proc)。这是我们想将我们的文件放置的地方。   */
}


/* 清除 - 从 /proc 中注销文件。如果在 WaitQ 中仍然有进程这将变得危险,因为它们在我们的打开函数里
 * 而它将不能被卸载。我将在第10章里面解释在这样的情况下如何避免移除内核模块。*/
void cleanup_module()
{
  proc_unregister(&proc_root, Our_Proc_File.low_ino);
}


next up previous contents index
Next: 代替 printk Up: Linux 内核模块编程 Previous: 系统调用

1999-05-19