2008年10月31日星期五

万圣节在世界之窗

公司加班到六点半,我就找理由逃脱了。周总说了:可以,我也在下棋呢!呵呵。

晚上有洪洪和曾胖组织的世界之窗活动,借助优惠券,我们可以在世界之窗享受廉价的万圣
节。感谢洪洪,感谢曾胖,感谢老外,嘿嘿。

万圣节又名鬼节,在西方是非常活跃的节日。最重要的标志,就是孩子们挨家挨户的要糖果,
这点我们可以在《老友记》中可以感受其气氛(因为被孩子哄的团团转,Rachel把糖果全部
给了一个小女孩)。另外你也可以看到Phoebe的超人服。


今天在世界之窗,你完全感受到另类的中国式万圣节。灰长灰长的热闹!中国人就是多,头
顶上带个犄角,更有各式各样的妖魔鬼怪齐聚这里。


我是第一次进世界之窗,这里你可以看到很多很多的世界之窗。由于在夜间,没有像曾胖同
志那样注意技巧。如果你想学摄影,可以找曾胖




这里是精简版本的曼哈顿,你也可以看到已经米稀的双子楼。


恐怖活动是主题,到处是南瓜头。在僵尸峡谷还可以看到专业级别的女鬼。


最后是万圣节party,不太专业的乐队,不太哈皮的派对。到最后出现赶鬼行动,用纸团相
互攻击。其实最后被群鬼完全吓跑。


最后是龙套小鬼,祝福大家异类的万圣节快乐。


_

2008年10月30日星期四

心诚则灵

经过接近三个月的离职期之后,我最近得以 光荣上岗

回顾这三个月,似乎经历了松山湖之行,回乡历程,孤单的七夕节,八月北京奥运,经济的
冰冻期;几经周折,我也完成了助学贷款,习惯了泡图书馆的日子,参与17爬爬吧的活动(1
23) 烦躁的求职生活。

这段时间也正是痛苦和快乐并重,有次扁桃体发炎,疼到吞咽困难也坚持不去看医生;多个
公司下遭遇闭门羹,面试题更是折磨的我苦不堪言;我和莹莹经历了最严重的争吵,甚至有
放弃感情的念头……

不过也终究过来了,我觉得生活还就这样,我也还就这样,2008年也就这样过来了。我总是
对这样苦中作乐的日子念念不忘,我还会回头想起高考的日子,踏上深圳的每一步。我甚至
可以一直这样慢悠悠的闲廷信步,一直等到花光手上所有积蓄,需要到必胜客去打钟点工来
找足乐子。尽管这样随性的性格伤害了一些好朋友。

上个星期回家,老妈告诉我家里的情况很乱。一般家里和游子总是处于“报喜不报忧”的状态
之中,假设他们不告诉家中的情形,那你得总怀揣着担忧的心思;而家人支支吾吾甚至直接
告知情况糟糕的时候,家里一定乱成一团糟了。因此找份工作仍旧是重中之重,回家过年也
会挂上日程。

现在的工作是在一家工控公司作嵌入式开发,公司名称保密。他们会为一些厂家作外包,来
实现一些工控,自动化设施的嵌入式主板,他们也有了PCB设计师,调板工,唯独让人郁闷
的是:除了经理以外,没有开发人员。很显然这个公司还处于起步阶段,办公室位于33层
(最高层),主板扔的到处都是,每个房间都有包装盒堆成的小山,室内环境比清华信息港
差上几个档次。最重要的是——在这个公司, 加班时间将成为家常便饭,而且没有加班费。

于是我进入了这样一个阶段:

  1. 始终拒绝加班的我,将会把加班作为一种享受
  2. 仍旧为成为一个顶级的软件工程师而努力
  3. 我将来的一段时间内,必须把工作放在第一位
  4. 那个小小的梦想还将被搁置

心诚则灵,收起邋遢随性的性格,为将来努力吧!

得知一大票人口在我和莹莹就职后也开始找到工作,恭喜你们,也祝愿全球金融冰冻期早日
结束。

2008年10月25日星期六

微软黑屏和盗版权益

自从微软发布WGA更新以后,中国网民的抗议声音不绝于耳: 微软黑屏引发威胁百倍于熊猫烧香微软黑屏引发国家安全话题,国家版权局将会对此事件 表态。真实情况将会怎样呢?是中国一家之言,还是全球群起反对?是网友过激,还是捍卫 安全呢?

环顾四周,我们才发现,微软黑屏是一个全球性的政策,然而对其他国家来说远远未像中国 那样表现过激,就连香港也对此冷漠。很多普及正版的国家和地区,人民是可以支付正版价 格的;而对于大多数发展中国家来说,他们也有更多的选择。

而就技术方面来说,微软只是针对盗版用户投放信息,现在没有任何组织和个人可以拿出证 据说微软控制电脑和数据。如果你说强制黑屏可以指明微软控制用户的计算机,我觉得这是 捕风捉影。操作系统本身具有利用硬件资源提供服务的功用,而在正常情况下,微软将会根 据EULA,在用户同意后才可以使用,更改任何用户数据,并通知处理用户信息的目的。但是 因为EULA的争议性,中国大陆还在考虑其是否适用。

如果中国官方现在考虑,那我建议微软黑屏和控制电脑之间需要更多的证据。当然我觉得更 值得考虑的事情是:1)盗版用户和软件制作商之间基于什么样的权力关系。2)国家如何扶植 操作系统和办公软件,用以改变对Windows和Office过度依赖的现状。3)如何发展本国自由 软件技术和提高重要机构的信息安全。

2008年10月22日星期三

我把事情搞砸了

事情得回溯到六月底,我一个人在康科办公室里,翻看着APUE的源码,我会很满足的告诉自
己:我在忙碌,我像一个有志向,发奋图强的年轻人一样在学习。但是面对着眼前的烂摊子
我就傻眼了————加拿大随时会发来稀奇古怪的需求,需要你改动一点点小玩意,在晚上下班
之前提交上去。

我讨厌了这样的工作,别人的软件让我在其之上修改一些小的界面,或者使用简单的汇编语
句取得一些硬件信息;完成之后我还要钻到实验室里,花费两天时间来测试效果。想起为了
完成新主板的MAPPING,我要投身两个星期来分析完芯片初期数据,那个经历真是糟糕透了。

我宁愿去看Linux内核,那是一个有创意的,有前途的东西,让我学习了一些硬件知识是如
何应用到操作系统设计之上的。后来我发现手头的工作极其枯燥,而且无法腾出时间来看内
核代码,我义无反顾的提交了辞呈。老板对于我的第二次辞呈没有发表太多意见,在一个月
后(七月底),我在一片静寂之中离开了付出两年汗水的公司。

八月初,我回到家乡,我见到了很多亲人老友,他们对我的经历一无所知,辞职的事情除了
老哥以外,我没有告诉任何人。家里有压在我心头的几块石头,完成这些琐碎之事以后,我
匆忙的回到了深圳,开始了漫长的求职时期。

我得说,我从没想过,求职期如此的漫长和烦躁,这让我生活的周边都显得更加煎熬。

我在八九月发出了超过30条求职信,而回复者寥寥无几。我认为这开始并不是什么稀奇事情,
很多人告诉我八到十月是招聘淡季,我对这些措辞不以为然,我不相信什么淡季,依据我的
能力,找到一个适合的工作没有大问题。现在回想起来,我对自己的自信慢慢感觉到羞愧!

九月份我的回复得到了稍许回应,我接到了几个公司的面试申请。于是我开始一一品尝面试
的滋味,腾讯公司,开源通讯公司,深信服公司,深圳傲天通讯,研祥科技大厦……这其中带
有趣味,然而更多的是苦痛。

在经历过如此多的面试之后,我得说,我根本不了解我工作的这一行是个什么样的状况,甚
至可以说,我根本没有对自己的工作做一个很好的评估————根本不知道自己要作什么。

想起毕业的时候我选择报一本《JAVA编程思想》南下找工作的情景,我觉得从理念上看,现
在和那个时候没有什么本质分别:我依然自信满满的认为我可以作自己要的工作,可对这一
份工作的概念都是模糊的。

我想作Linux程序员,但是来到人才招聘网上,我看到Linux程序员都有什么要求呢:
1) Linux驱动编程
2) Linux嵌入式开发
3) Linux后台开发

在这样的职位需求面前,我傻眼了,如果佯装是一个Linux程序员去面试职位,你几乎必须
面对以下问题之一:
1) Linux内核裁减
2) Linux驱动程序编写和移植
3) 嵌入式CPU架构和编程环境
4) Linux网络编程

如果你没有以上经验,那么你几乎没有和人事部经理谈话的资格,在技术面试官前就会被淘
汰。可是我就没有,全部NO,如我所说,80%的面试官都不会接受我的。

剩下20%是通过面试题来测试的,我得说,我是一个应试型的人才,面对考试不以为然。但
是在答复一些笔试题时候,我仍然深受打击————我的专业功底显得那么的不堪一击!我经历
过的面试题浩如烟海,他们覆盖了计算机知识绝大多数部分:
1. C/C++语言功底,C++庞大的语言特性,Shell高级编程
2. UNIX环境编程,Socket编程,多线程编程
3. UNIX网络编程,tcp/ip,http协议
4. 数据结构和算法设计,一些查找和排序算法
5. CPU架构,x96, Power, MIPS架构知识

要从理论上学习这些,我起码要看完手头的大堆书籍。他们包括:
Thinking in C++
Effective C++
UNIX环境编程 APUE
UNIX网络编程 UNP
编程艺术
算法设计
Linux设备驱动
深入理解Linux内核
... ...

顿时我觉得自己非常渺小,经过的两年工作时间在眼前化为乌有。

如果没有什么奇迹发生的话,我仍将领教上面所说的面试和笔试问题,我将再次面临自己薄
弱的知识和经验积累,我还会在离职的深渊中不断轮回。因此这样一个明显的选择摆在我的
面前:要么尝试其他低阶的工作甚至换行,要么就要费劲心力来补齐自己的不足。更要命的
是,我花费这么长的时间才发觉这个问题,那么离职时我在做什么呢?一年前呢?哦~这让
我实在难受————

天呐!我把事情搞砸了!

[LDD] C3. Character Device driver

代码阅读 <Linux Device Driver> v3
Chapter 3. Character Device Driver

一、scull介绍 Simple Chracter Utility for Loading Localities
scull是一个操作内存区域的字符设备。

scull源码实现的设备有:
scull0 ~ scull3
这四个设备分别由一个全局且持久的内存区域组成。
scullpipe0 ~ scullpipe3
这四个FIFO与管道类似。一个进程读取由另一个进程写入的数据。如果多个进程读
取同一个设备,它们就会为数据发生竞争。scullpipe的内部实现将说明在不借助
于中断的情况下如何实现阻塞式和非阻塞式读写操作。
scullsingle
scullpriv
sculluid
scullwuid
这些设备与scull0相似,但在何时允许open操作方面有一些限制。
scullsingle,一次只允许一个进程使用该驱动程序。
scullpriv,它对每个虚拟控制台是私有的。因为控制台进程会获取不痛的内存区。
sculluid和scullwuid可以被多次打开,但是每次只能由一个用户打开;如果另一
个用户锁定该设备,sculluid则返回“Device Busy”错误,而scullwuid则实现了阻
塞式open。

二、设备编号
对字符设备的访问是通过文件系统内的设备文件进行的,这些设备文件通常位于/dev。而这
些文件包含两个重要数据,主设备号和次设备号。

主设备号表示对应的驱动程序,现代的Linux允许多个驱动程序共享主设备号,但一般仍旧
遵循“一个主设备号对应一个驱动程序”的原则。

次设备号由内核使用,用于正确确定设备文件指定的设备。

MAJOR(dev_t dev); /* 返回主设备号 */
MINOR(dev_t dev); /* 返回次设备号 */
MKDEV(int major, int minor); /* 由主次设备得到dev_t类型的设备编号 */


关于分配和释放设备编号,系统在linux/fs.h中声明了以下函数:

/* 获得一个或者多个设备编号 */
int register_chrdev_region(dev_t first, unsigned int, char *name);

/* 运行过程中,使用以下函数会为我们恰当分配所需要的设备号 */
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, char *name);

/* 应该用以下函数释放这些设备编号 */
void unregister_chrdev_region(dev_t first, unsigned int count);


三、重要数据结构

#include <linux/fs.h>
struct inode {
struct hlist_node i_hash;
struct list_head i_list;
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino;
atomic_t i_count;
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev;
u64 i_version;
loff_t i_size;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
unsigned int i_blkbits;
blkcnt_t i_blocks;
unsigned short i_bytes;
umode_t i_mode;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
struct mutex i_mutex;
struct rw_semaphore i_alloc_sem;
const struct inode_operations *i_op;
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct super_block *i_sb;
struct file_lock *i_flock;
struct address_space *i_mapping;
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
/* ... */
};

struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

/* ... */
};

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};

/* 自定义结构 */
struct scull_qset {
void **data;
struct scull_qset *next;
};

struct scull_dev {
struct scull_qset *data;
int quantum;
int qset;
unsigned long size;
unsigned int access_key;
struct semaphore sem;
struct cdev cdev;
};


struct inode ————
内核使用inode结构在内部表示文件,因此它和file结构不同,后者表示打开的文件描述符。
对单个文件,可能会有许多个表示打开的文件描述符的file结构,但他们都指向单个inode
结构。

inode包含大量节点文件信息。而此驱动只是用到了以下两个字段:
dev_t i_rdev; // 对于表示设备的inode结构,该字段包含真正的设备编号
struct cdev *i_cdev; // 对于表示字符设备的inode结构,此字段包含struct cdev指针

另外,开发者可以通过两个宏来获取主次设备编号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

struct file ————
file结构代表一个打开的文件,系统中任何一个打开的文件都有一个对应的file结构,他由
内核在open时候创建,并传递给在文件上的所有函数,知道最后的close函数。在文件实例
关闭之后,内核会释放这个结构。用到的字段有:

struct path f_path;
#define f_dentry f_path.dentry // 目录项结构
const struct file_operations *f_op;
unsigned int f_flags; // 标志,是否阻塞
mode_t f_mode; // 权限检查
loff_t f_pos; // 当前读写位置
/* needed for tty driver, and maybe others */
void *private_data; // 跨系统调用时候保存状态信息


struct file_operations ————
file_operations用来建立驱动程序操作和驱动设备编号之间的连接,他包含了一组函数指
针,这些函数和某些系统调用相关联。程序员可以视此结构为面向对象的C编程的一个例证。

file_operations的函数指针必须指向驱动程序实现特定操作的函数,对于不支持的操作,
对应的字段被赋值NULL,这种情况下,内核的处理行为是不尽相同的。
在本例中,我们实现了以下操作:

struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};


四、scull内存使用
两个函数 ————
void *kmalloc(size_t size, gfp_t flags);
void kfree(void *ptr);
这个两个函数定义在linux/stab.h中,是内核中非常重要的两个内存操作函数。用kmalloc
函数分配,然后用kfree释放,同时也注意不应该把非kmalloc返回的指针传递给kfree。
(但是将NULL传递给kfree是合法的)


scull驱动中,每一个scull设备(scull_dev)都是一个链表头,他的指针指向scull_qset
结构的链表。而每一个scull_qset则是典型的链表结构,包含了一个4000bytes * 1000的内
存区域,这里的每一个区域就被称为是一个量子(quantum)。
4000和1000的尺寸,由宏SCULL_QUANTUM和SCULL_QSET定义。当然,你也可以设置为模块参
数,也可以在运行时使用ioctl来更改。

由于scull设备是虚拟的内存区域,所以我们的read/write操作也将是对内存的操作,顶多
的麻烦是跨越内核空间和用户空间的拷贝而已。这让实现变的非常容易。

五、实现
初始化和清理函数

int scull_init_module(void)
{
int result, i;
dev_t dev = 0;

if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
"scull");
scull_major = MAJOR(dev);
}

if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}

/* allocate the devices -- we can't have them static, as the number
* can be specified at load time.
*/
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_devices) {
result = -ENOMEM;
goto fail;
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));

/* initialize each device */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
init_MUTEX(&scull_devices[i].sem);
scull_setup_cdev(&scull_devices[i], i);
}

/* At this point call the init function for any friend device */
dev = MKDEV(scull_major, scull_minor + scull_nr_devs);
// dev += scull_p_init(dev);
// dev += scull_access_init(dev);

#ifdef SCULL_DEBUG /* only when debugging */
// scull_create_proc();
#endif
printk(KERN_INFO "init scull successful\n");
return 0; /* succeed */

fail:
scull_cleanup_module();
return result;
}

void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(scull_major, scull_minor);

/* Get rid of our char dev entries */
if (scull_devices) {
for (i = 0; i < scull_nr_devs; i++) {
scull_trim(scull_devices + i);
cdev_del(&scull_devices[i].cdev);
}
kfree(scull_devices);
}

#ifdef SCULL_DEBUG /* use proc only if debugging */
scull_remove_proc();
#endif

/* cleanup_module is never called if registering failed */
unregister_chrdev_region(devno, scull_nr_devs);

/* and call the cleanup functions for friend devices */
// scull_p_cleanup();
// scull_access_cleanup();
printk(KERN_INFO "cleanup scull successful\n");
}

init_module过程包括 ————
1) 注册设备
2) 初始化scull_dev设备
3) 初始化其他设备
为了简化设计,此章节没有引入pipe等设备,所以注销了某些方法。

cleanup_module过程 ————
1)释放scull_dev结构,注销字符设备
2) 注销设备号


然后是在file_operations指明的函数,这里重点讲解read和write的实现 ————

ssize_t scull_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if(*f_pos >= dev->size)
goto out;
if(*f_pos + count > dev->size)
count = dev->size - *f_pos;

item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

dptr = scull_follow(dev, item);

if (dptr == NULL || !dptr->data || !dptr->data[s_pos])
goto out;

if (count > quantum - q_pos)
count = quantum - q_pos;

if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
out:
up(&dev->sem);
return(retval);
}


ssize_t scull_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = 0;

if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
if(*f_pos >= dev->size)
goto out;
if(*f_pos + count > dev->size)
count = dev->size - *f_pos;

item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

dptr = scull_follow(dev, item);

if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset *sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}

if (count > quantum - q_pos)
count = quantum - q_pos;

if (copy_from_user(dptr->data[s_pos] + q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

/* 更新文件大小 */
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
up(&dev->sem);
return(retval);
}


scull_read从filp的f_pos处读取count个字节,写到buf中。
首先要作的是,确定f_pos的位置,根据f_pos,我们要确定写的位置,位于第几个item的
第s_pos数组的第q_pos个量子处,然后根据item返回我们要的scull_qset结构体。最后使用
copy_to_user完成读操作。

写操作和读操作很相似,使用了逆向的copy_from_user函数。

这两个重要的系统调用实现,都使用了跨越内核和用户空间的写操作:
copy_to_user(void *to, void *from, int count);
copy_from_user(void *to, void *from, int count);

六、总结
scull_init_module
* alloc_chrdev_region
* scull_setup_cdev
** cdev_init, cdev_add
** scull_fops(llseek, read, write, ioctl, open, release)
* scull_create_proc
* scull_p_init
* scull_access_init

_