linux 设备驱动程序 (6) – 阻塞型 io、poll

linux 设备驱动程序 (6) – 阻塞型 io、poll

阻塞型 IO

上篇的例子中, 当资源不可获取时 (读写指针指向同一个位置) 我们提到了采取阻塞的方式. 这一篇展开叙述阻塞 io.

在用户态程序中, 调用进程通常只会简单的调用 read 和 write, 不会考虑数据是否可用, 输出缓冲区是否已满之类的问题. 在我们的驱动程序中, 当遇见这种情况应当 [默认] 阻塞该进程, 直到数据可用.

但在这之前, 先来讨论一下 unix 中的 io 模型. unix 中的 io 大概包括以下几类:

  • 阻塞型 IO
  • 非阻塞型 IO
  • 异步 IO
    关于异步 IO 及异步通知机制在下一篇中讲述. 这一篇关注驱动程序如何处理阻塞型 IO 和非阻塞型 IO 的问题.

非阻塞型 io 语义

非常简单, 当数据不可用时, read/write 函数返回 EAGAIN. 所以我们的驱动程序只需当数据不可用时返回 - EAGAIN 即可

阻塞型 IO 语义

这是 unix 中最常见的的 IO. 对于阻塞型 IO 语义的总结如下:

对于从设备读取数据:

  • 如果缓冲区中有数据, 并且驱动程序可以保证数据可以很快到达 (难以察觉的延迟), 那么即使就绪的数据比请求的少 read 函数也应返回
  • 如果缓冲区中无数据, 默认下 read 应当阻塞等待直到至少一个字节到达
  • 如果缓冲区中无数据, 并且设置了 O_NONBLOCK 标志, read 应立即返回 - EAGAIN
  • 如果缓冲区中无数据, poll 函数必须报告设备不可读, 直到至少一个字节到达
  • 如果到达文件尾, read 应立即返回 0, 无论 O_NONBLOCK 标志
  • 如果到达文件尾, poll 应报告 POLLHUP

对于向设备写入数据:

  • 如果输出缓冲区中有空间, write 应立即返回即使可接收的数据比请求的数据少
  • 如果缓冲区已满, 默认下 write 应阻塞等待直到有空间被释放
  • 如果缓冲区已满, 并且设置了 O_NONBLOCK 标志, write 应立即返回 - EAGAIN
  • 如果缓冲区已满, poll 函数应报告文件不可写
  • 如果设备不能再接受任何数据, write 应返回 - ENOSPC
  • 不要在 write 中等待数据传输完成 [既数据传输到真正的硬件中]write 函数只需保证数据可靠写入输出缓冲区中
  • 如果使用设备的程序必须保证数据写入硬件, 则驱动必须提供 fsync 函数
    [未完]

linux 设备驱动程序 (5) – 字符设备驱动例子

linux 设备驱动程序 (5) – 字符设备驱动例子

这次给出一个例子, 比 (3) 中的例子稍微复杂一些. 在 (3) 中, 给出了一个字符设备的例子, 会保存最后一个写入的字节. 这次的例子使用一个链表保存写入的数据, 读取时从链表中移出数据:
![image_1bl0atutt1kn7iv5t3b18q8fr49.png-5.9kB][1]

因此这个驱动和 FIFO 和管道非常相似, 当用户写入数据时, write point 后移, 并且写入数据 (如果当前链表 node 空间用尽则创建新 node). 当用户读取数据时, read pointer 也后移 (如果 read pointer 指向后一个 node 则删除前一个 node)

所以首先写一个简单的单向链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define BYTE_PER_NODE 4096	/* 每个节点的数据size */
struct linked_node
{
char *data; /* 指向一个数组, 数据保存在这里 */
struct linked_node* next; /* 指向下一个节点 */
};
/* 创建一个新node */
struct linked_node* create_node(void)
{
struct linked_node* ret;
if ((ret = kmalloc(sizeof(struct linked_node), GFP_KERNEL)) == NULL)
{
DEBUG_LOG(KERN_WARNING, "no memory\n");
goto err;
}
if ((ret->data = kmalloc(BYTE_PER_NODE, GFP_KERNEL)) == NULL)
{
DEBUG_LOG(KERN_WARNING, "no memory\n");
goto err1;
}
return ret;

err1:
kfree(ret);
err:
return NULL;
}
/* 删除一个node */
void free_node(struct linked_node* node)
{
kfree(node->data);
kfree(node);
}

因为我们的读写指针 (read pointer/write pointer) 需要保存指向哪个节点, 以及在节点中的哪个位置. 所以用一个结构体来保存这个指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct pos_pointer
{
struct linked_node* node;
size_t pos;
};
struct chr_dev
{
struct pos_pointer read_ptr;
struct pos_pointer write_ptr;
struct semaphore sem;
wait_queue_head_t queue;

struct cdev cdev;
};

在我们的设备结构体中使用 pos_pointer 保存读写指针.

另外发现多了一个 sem 成员, 这个是上篇中学习的信号量. queue 成员是一个” 等待队列头”, 用于当无数据可读时进行休眠 [下一篇讲述].

在 chr_init() 函数中应该设置我们的读写指针和信号量设施:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* in function chr_init() */

/* 设置私有数据 */
DEBUG_LOG(DEFAULT_LEVEL, "setup private data\n");
if ((node = create_node()) == NULL)
{
goto err2;
}
chr_dev->read_ptr.node = node;
chr_dev->read_ptr.pos = 0;
chr_dev->write_ptr.node = node;
chr_dev->write_ptr.pos = 0;
DEBUG_LOG(DEFAULT_LEVEL, "success\n");
/* 初始化信号量 */
DEBUG_LOG(DEFAULT_LEVEL, "initialize semaphore\n");
sema_init(&chr_dev->sem, 1);
DEBUG_LOG(DEFAULT_LEVEL, "success\n");
/* 初始化等待队列 */
DEBUG_LOG(DEFAULT_LEVEL, "initialize wait queue\n");
init_waitqueue_head(&chr_dev->queue);
DEBUG_LOG(DEFAULT_LEVEL, "success\n");

在 chr_exit() 中也应该释放资源:

1
2
3
4
5
6
7
8
9
10
p = chr_dev->read_ptr.node;
chr_dev->read_ptr.node = NULL;
chr_dev->write_ptr.node = NULL;
while (p)
{
q = p->next;
free_node(p);
p = q;
}
cdev_del(&chr_dev->cdev);

写入函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
ssize_t write(struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
struct chr_dev *dev = filp->private_data;
size_t this_write, total_write = 0;
char* p;

if (down_interruptible(&dev->sem) != 0)
return -ERESTARTSYS;

DEBUG_LOG(DEFAULT_LEVEL, "write from user,pid=%d [%s]\n",
current->pid, current->comm);

while (count != 0)
{
this_write = MIN(BYTE_PER_NODE - dev->write_ptr.pos,
count);
p = &dev->write_ptr.node->data[dev->write_ptr.pos];
if (copy_from_user(p, buf, this_write) != 0)
{
DEBUG_LOG(KERN_WARNING, "copy_from_user error\n");
goto err;
}
count -= this_write;
total_write += this_write;
buf += this_write;
dev->write_ptr.pos += this_write;
if (dev->write_ptr.pos == BYTE_PER_NODE)
{
dev->write_ptr.node->next = create_node();
dev->write_ptr.node = dev->write_ptr.node->next;
dev->write_ptr.pos = 0;
}
}

DEBUG_LOG(DEFAULT_LEVEL, "write success total_write=%d,pos=%d\n",
total_write, dev->write_ptr.pos);

up(&dev->sem);

wake_up_interruptible(&dev->queue);
return total_write;

err:
up(&dev->sem);
return -EFAULT;
}

我们使用循环将数据写入, 每次循环开始时检测当前 node 剩余空间 (BYTE_PER_NODE – dev->write_ptr.pos) 和用户要求写入的 count 谁更小, 取较小作为 this_write(这一次写入的数据). 写入 node 之后, 令 count-=this_write 和并且令写指针 +=this_write. 之后检测写指针是否到 node 的末尾, 如果是的话, create 一个新 node.

读取函数和写入很类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
struct chr_dev *dev = filp->private_data;
size_t this_read, total_read = 0;
char* p;

if (down_interruptible(&dev->sem) != 0)
return -ERESTARTSYS;

DEBUG_LOG(DEFAULT_LEVEL, "read from user,pid=%d [%s]\n",
current->pid, current->comm);

while (dev->read_ptr.node == dev->write_ptr.node &&
dev->read_ptr.pos == dev->write_ptr.pos)
{
up(&dev->sem);
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;

DEBUG_LOG(DEFAULT_LEVEL, "[%s] going to sleep\n", current->comm);

if (wait_event_interruptible(dev->queue, (dev->read_ptr.node != dev->write_ptr.node ||
dev->read_ptr.pos != dev->write_ptr.pos) ) != 0)
return -ERESTARTSYS;

if (down_interruptible(&dev->sem) != 0)
return -ERESTARTSYS;
}

while (count != 0)
{
if (dev->read_ptr.node == dev->write_ptr.node)
{
this_read = MIN(dev->write_ptr.pos - dev->read_ptr.pos,
count);
}
else
{
this_read = MIN(BYTE_PER_NODE - dev->read_ptr.pos,
count);
}
if (this_read == 0)
break;

p = &dev->read_ptr.node->data[dev->read_ptr.pos];
if (copy_to_user(buf, p, this_read) != 0)
{
DEBUG_LOG(KERN_WARNING, "copy_to_user error\n");
goto err;
}
count -= this_read;
total_read += this_read;
buf += this_read;
dev->read_ptr.pos += this_read;
if (dev->read_ptr.pos == BYTE_PER_NODE)
{
struct linked_node* node;
node = dev->read_ptr.node;
dev->read_ptr.node = dev->read_ptr.node->next;
free_node(node);
dev->read_ptr.pos = 0;
}
}

DEBUG_LOG(DEFAULT_LEVEL, "read success total_read=%d,pos=%d\n",
total_read, dev->read_ptr.pos);

up(&dev->sem);
return total_read;

err:
up(&dev->sem);
return -EFAULT;
}

[EOF]
[1]: /images/86e4c5cbc6dbb08baa8ba93e91a4fccd.png

c 标准库字符输入输出

c 标准库字符输入输出

按功能分类

读取一个字符

getchar Get character from stdin (function)
getc Get character from stream (function)
fgetc Get character from stream (function)
1
2
3
int getchar ( void );
int getc ( FILE * stream );
int fgetc ( FILE * stream );
第一个是从 stdin 中读取, 后两个都是从 FILE * 中读取但是 getc 可能被实现为宏, 返回值都为 int.

当成功时, 返回被读取的字符 (会被提升为 int); 当读取失败时返回 EOF, 需检测 feof 和 ferror 函数来检测是读到文件尾或是发生错误.

写入一个字符

putchar Write character to stdout (function)
putc Write character to stream (function)
fputc Write character to stream (function)
1
2
3
int putchar ( int character );
int putc ( int character, FILE * stream );
int fputc ( int character, FILE * stream );
当写入时 character 会被强制为 unsigned int.

成功时返回写入的字符, 失败时返回 EOF, 应检测 ferror.

读取一行

gets Get string from stdin (function)
fgets Get string from stream (function)
1
2
char * gets ( char * str );
char * fgets ( char * str, int num, FILE * stream );
分别从 stdin 和 stream 中读取一行字符保存到 str 中, 使用 gets 比较危险可能会造成缓冲区溢出. `当使用 fgets 时最多会读取 num 个字符`.

当成功读取时, 返回参数 str. 当第一个字符就为 EOF 时, 返回 NULL. 当遇到错误时返回 NULL. 所以应检测 feof 和 ferror.

写入一行

puts Write string to stdout (function)
1
int puts ( const char * str );
将 str 写到 stdout, 并且在后面加一个 \ n

成功时返回非零值, 出错时返回 EOF.

写入字符串

fputs Write string to stream (function)
1
int fputs ( const char * str, FILE * stream );
和 puts 不同, 这个不在 str 后添加 \ n 字符

成功时返回非零值, 出错时返回 EOF.

读回

ungetc Unget character from stream (function)
1
int ungetc ( int character, FILE * stream );

将 character 写回 stream 中

成功时返回 character, 失败时返回 EOF

[EOF]

拥有锁的时候最好不要进行休眠

拥有锁的时候最好不要进行休眠

当拥有一个自旋锁,seqlock 和 RCU 锁时禁止休眠。
关闭中断时禁止休眠
原因是,当拥有这些锁的时候必须确保程序处于一个原子的上下文中。具体可见驱动 4

拥有一个信号量的时候尽量使休眠的短一些。原因很简单,你拿着一个锁不用,却去睡觉了,不是相当于占着茅坑不拉屎吗?结果导致其他等待信号量的线程也要休眠。

linux 设备驱动程序 (4) – 并发

linux 设备驱动程序 (4) – 并发

进行 linux 驱动开发不得不考虑的问题就是并发问题. 因为在内核态, 代码是可抢占的, 你不知道什么时候内核会抢占你对 CPU 的使用权来执行另一段代码 (这段代码可能会修改掉你的数据). 而且现在大多使用 SMP(对称多处理器), 代码甚至可以同时执行. 性能得到了很大提升但是编程的复杂程度也高了很多. 特别是在如何防止数据被其他执行线程修改上. 幸运的是, linux 已经提供了很多设施来完成这个功能.

1. 信号量 & 互斥体

这个在多线程编程中太常见了, 就不赘述了. 另外记一下 semaphore 这个单词, 总是拼错.

列一下函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <linux/semaphore.h> /* 不像书中所写
并没有<asm/semaphore.h> */
void sema_init(struct semaphore *sem, int val);
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
void up(struct semaphore *sem);

void init_rwsem(struct rw_semaphore *sem);
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);

<<linux 设备驱动程序>> 上所说的 init_MUTEX 函数貌似在新版本中已经删掉了, 可以用 sema_init(&sem, 1); 来代替.
down_interruptible 函数当被中断时会返回非零值, down_trylock 当信号量不可获得时会返回非零值.

2. 自旋锁

当对信号量执行 down 函数时, 如果当前无法获取信号量, 会阻塞当前执行线程, 但是并非 CPU 空转不工作. 而是” 进入休眠”. 进入休眠是一个有明确定义的术语. 当” 进入休眠” 时, 执行线程会进入休眠状态, 这时会把 CPU 让给其他执行线程知道将来它能获取信号量为止.

但是自旋锁不一样, 当线程对自旋锁进行” 锁定” 动作时, 如果自旋锁已经被其他线程锁定, 那么当前线程将进行” 自旋”. 所谓自旋, 其实就是一个 while 循环 [它循环重复检查这个锁直到锁可用为止]. 所以说可见自旋锁当锁定时不会让出 CPU.

所以自旋锁简单, 而且也比信号量快 (因为不用设计到 CPU 调度). 但是使用却有一些限制:

  • 考虑当前系统是单处理器非抢占系统, 那么如果一个线程进入自旋状态, 那么因为没有抢占其他线程得不到执行, 所以无法解锁自旋锁. 那么这个线程会一直循环下去. 整个系统会被卡死. 所以在非抢占式单处理器系统上自旋锁被优化为不做任何事.
  • 考虑在一个单处理器抢占式系统上. 一个线程获得了一个自旋锁, 然后再临界区执行时丢掉了 CPU(可能被抢占, 可能调用了进入休眠的函数). 如果获得 CPU 的线程也想获取那个自旋锁, 那么整个系统会死锁下去. 所以为了避免这个, 当一个线程获得自旋锁之后此线程所在的 CPU 的抢占会被禁止. 另外, 人们要注意不要再获得自旋锁之后执行会丢掉 CPU 的函数.
  • 另外, 当线程获得自旋锁之后, 发生了中断, 中断例程也请求获取自旋锁, 这时整个系统也会进入死锁. 可以在获取锁时关闭当前 CPU 中断来解决.
  • 最后, 自旋锁的重要准则是: “自旋锁必须在可能的最短时间内拥有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <linux/spinlock.h>

spinlock_t lock = SPIN_LOCK_UNLOCKED;
void spin_lock_init(spinlock_t *lock);

void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);

void spin_unlock(spinlock_t *lock);
void spin_unlock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);

int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);

irqsave 会将中断状态保存在 flags 中, 当 unlock 时必须提供同一个 flags.

irq 函数会禁止本处理器的中断.

bh 会关闭软中断.

同样, 自旋锁有 rw 版本.

3. 使用锁的一些准则与陷阱

  • 在编写函数时, 被调用的函数不能锁定此函数中一定锁定的锁否则会造成死锁. 所以当编写那些假定调用者已经获取锁的函数时, 最好在注释中写明, 在此函数被调用之前调用者已经加锁. 防止几个月后重写时在函数中误加锁造成死锁. [最好养成习惯只在某一类函数中加锁 (如只在系统调用直接调用的函数中加锁)]
  • 必须同时获取多个锁时, 最好都按照一定顺序获取.
  • 先获取局部的锁, 再获取全局的锁.
  • 先获取信号量, 再获取自旋锁.

4. 循环队列

使用循环队列是一种免锁算法. 生产者在队列的一端中写入, 消费者从另一端取走. 如果设计的好, 可以不必使用锁.

在 <linux/kfifo.h> 中有实现好的循环队列.

5. 原子变量

当对一个简单的整数进行加减的时候也加锁显得有些小题大做了. 但是很多整数运算确实不是原子的, 如 ++i;

所以 linux 内核实现了原子类型 atomic_t 来进行高效的原子的整形运算.

具体参见 <asm/atomic.h>

6. 原子位操作

除了原子的整数变量, 内核也提供了原子的位操作类型和函数. 集体参见 <asm/bitops.h>

7.seqlock

8. 读取 - 复制 - 更新 (RCU)

linux 数据结构函数

linux 数据结构函数

今天翻 man page, 发现 linux 下的 libc 比标准库要丰富很多, 除了提供了一些系统相关的包装函数 (fdopen popen dprintf 之类) 来降低编程难度以外, 还提供了一组数据结构相关的函数. 粗略看了一下, 提供了以下数据结构:

  • 哈希表
  • 队列
  • 二叉查找树

还提供了简单的线性查找算法.
哈希表:

1
2
3
4
5
6
7
8
9
#include <search.h>
int hcreate(size_t);
void hdestroy(void);
ENTRY *hsearch(ENTRY, ACTION);
#define _GNU_SOURCE
int hcreate_r(size_t nel, struct hsearch_data *htab);
int hsearch_r(ENTRY item, ACTION action, ENTRY **retval,
struct hsearch_data *htab);
void hdestroy_r(struct hsearch_data *htab);

队列:

1
2
3
#include <search.h>
void insque(void *, void *);
void remque(void *);

线性查找:

1
2
3
4
5
#include <search.h>
void *lfind(const void *, const void *, size_t *,
size_t, int (*)(const void *, const void *));
void *lsearch(const void *, void *, size_t *,
size_t, int (*)(const void *, const void *));

查找树:

1
2
3
4
5
6
7
8
9
10
11
#include <search.h>
void *tdelete(const void *restrict, void **restrict,
int(*)(const void *, const void *));
void *tfind(const void *, void *const *,
int(*)(const void *, const void *));
void *tsearch(const void *, void **,
int(*)(const void *, const void *));
void twalk(const void *,
void (*)(const void *, VISIT, int ));
#define _GNU_SOURCE
void tdestroy(void *root, void (*free_node)(void *nodep));

[转]Linux Netcat 命令:网络工具中的瑞士军刀

netcat 是网络工具中的瑞士军刀,它能通过 TCP 和 UDP 在网络中读写数据。通过与其他工具结合和重定向,你可以在脚本中以多种方式使用它。使用 netcat 命令所能完成的事情令人惊讶。

英文原文: Linux Netcat command – The swiss army knife of networking,编译:oschina

netcat 所做的就是在两台电脑之间建立链接并返回两个数据流,在这之后所能做的事就看你的想像力了。你能建立一个服务器,传输文件,与朋友聊天,传输流媒体或者用它作为其它协议的独立客户端。

下面是一些使用 netcat 的例子.

[A(172.31.100.7) B(172.31.100.23)]

Linux netcat 命令实例:

1,端口扫描

端口扫描经常被系统管理员和黑客用来发现在一些机器上开放的端口,帮助他们识别系统中的漏洞。

1
$nc -z -v -n 172.31.100.7 21-25

可以运行在TCP或者UDP模式,默认是TCP,-u参数调整为udp.

z 参数告诉netcat使用0 IO,连接成功后立即关闭连接, 不进行数据交换(谢谢@jxing 指点)

v 参数指使用冗余选项(译者注:即详细输出)

n 参数告诉netcat 不要使用DNS反向查询IP地址的域名

这个命令会打印21到25 所有开放的端口。Banner是一个文本,Banner是一个你连接的服务发送给你的文本信息。当你试图鉴别漏洞或者服务的类型和版本的时候,Banner信息是非常有用的。但是,并不是所有的服务都会发送banner。

一旦你发现开放的端口,你可以容易的使用netcat 连接服务抓取他们的banner。

1
$ nc -v 172.31.100.7 21

netcat 命令会连接开放端口21并且打印运行在这个端口上服务的banner信息。

2,Chat Server

假如你想和你的朋友聊聊,有很多的软件和信息服务可以供你使用。但是,如果你没有这么奢侈的配置,比如你在计算机实验室,所有的对外的连接都是被限制的,你怎样和整天坐在隔壁房间的朋友沟通那?不要郁闷了,netcat提供了这样一种方法,你只需要创建一个Chat服务器,一个预先确定好的端口,这样子他就可以联系到你了。

Server

1
$nc -l 1567

netcat 命令在1567端口启动了一个tcp 服务器,所有的标准输出和输入会输出到该端口。输出和输入都在此shell中展示。

Client

1
$nc 172.31.100.7 1567

不管你在机器B上键入什么都会出现在机器A上。

3,文件传输

大部分时间中,我们都在试图通过网络或者其他工具传输文件。有很多种方法,比如FTP,SCP,SMB等等,但是当你只是需要临时或者一次传输文件,真的值得浪费时间来安装配置一个软件到你的机器上嘛。假设,你想要传一个文件file.txt 从A 到B。A或者B都可以作为服务器或者客户端,以下,让A作为服务器,B为客户端。

Server

1
$nc -l 1567 < file.txt

Client

1
$nc -n 172.31.100.7 1567 > file.txt

这里我们创建了一个服务器在A上并且重定向netcat的输入为文件file.txt,那么当任何成功连接到该端口,netcat会发送file的文件内容。

在客户端我们重定向输出到file.txt,当B连接到A,A发送文件内容,B保存文件内容到file.txt.

没有必要创建文件源作为Server,我们也可以相反的方法使用。像下面的我们发送文件从B到A,但是服务器创建在A上,这次我们仅需要重定向netcat的输出并且重定向B的输入文件。

B作为Server

Server

1
$nc -l 1567 > file.txt

Client

1
nc 172.31.100.23 1567 < file.txt

4,目录传输

发送一个文件很简单,但是如果我们想要发送多个文件,或者整个目录,一样很简单,只需要使用压缩工具tar,压缩后发送压缩包。

如果你想要通过网络传输一个目录从A到B。

Server

1
$tar -cvf – dir_name | nc -l 1567

Client

1
$nc -n 172.31.100.7 1567 | tar -xvf -

这里在A服务器上,我们创建一个tar归档包并且通过-在控制台重定向它,然后使用管道,重定向给netcat,netcat可以通过网络发送它。

在客户端我们下载该压缩包通过netcat 管道然后打开文件。

如果想要节省带宽传输压缩包,我们可以使用bzip2或者其他工具压缩。

Server

1
$tar -cvf – dir_name| bzip2 -z | nc -l 1567

通过bzip2压缩

Client

1
$nc -n 172.31.100.7 1567 | bzip2 -d |tar -xvf -

使用bzip2解压

5. 加密你通过网络发送的数据

如果你担心你在网络上发送数据的安全,你可以在发送你的数据之前用如mcrypt的工具加密。

服务端

1
$nc localhost 1567 | mcrypt –flush –bare -F -q -d -m ecb > file.txt

使用mcrypt工具加密数据。

客户端

1
$mcrypt –flush –bare -F -q -m ecb < file.txt | nc -l 1567

使用mcrypt工具解密数据。

以上两个命令会提示需要密码,确保两端使用相同的密码。

这里我们是使用mcrypt用来加密,使用其它任意加密工具都可以。

6. 流视频

虽然不是生成流视频的最好方法,但如果服务器上没有特定的工具,使用netcat,我们仍然有希望做成这件事。

服务端

1
$cat video.avi | nc -l 1567

这里我们只是从一个视频文件中读入并重定向输出到netcat客户端

1
$nc 172.31.100.7 1567 | mplayer -vo x11 -cache 3000 -

这里我们从socket中读入数据并重定向到mplayer。

7,克隆一个设备

如果你已经安装配置一台Linux机器并且需要重复同样的操作对其他的机器,而你不想在重复配置一遍。不在需要重复配置安装的过程,只启动另一台机器的一些引导可以随身碟和克隆你的机器。

克隆Linux PC很简单,假如你的系统在磁盘/dev/sda上

Server

1
$dd if=/dev/sda | nc -l 1567

Client

1
$nc -n 172.31.100.7 1567 | dd of=/dev/sda

dd是一个从磁盘读取原始数据的工具,我通过netcat服务器重定向它的输出流到其他机器并且写入到磁盘中,它会随着分区表拷贝所有的信息。但是如果我们已经做过分区并且只需要克隆root分区,我们可以根据我们系统root分区的位置,更改sda 为sda1,sda2.等等。

8,打开一个shell

我们已经用过远程shell-使用telnet和ssh,但是如果这两个命令没有安装并且我们没有权限安装他们,我们也可以使用netcat创建远程shell。

假设你的netcat支持 -c -e 参数(默认 netcat)

Server

1
$nc -l 1567 -e /bin/bash -i

Client

1
$nc 172.31.100.7 1567

这里我们已经创建了一个netcat服务器并且表示当它连接成功时执行/bin/bash

假如netcat 不支持-c 或者 -e 参数(openbsd netcat),我们仍然能够创建远程shell

Server

1
2
$mkfifo /tmp/tmp_fifo
$cat /tmp/tmp_fifo | /bin/sh -i 2>&1 | nc -l 1567 > /tmp/tmp_fifo

这里我们创建了一个fifo文件,然后使用管道命令把这个fifo文件内容定向到shell 2>&1中。是用来重定向标准错误输出和标准输出,然后管道到netcat 运行的端口1567上。至此,我们已经把netcat的输出重定向到fifo文件中。

说明:

从网络收到的输入写到fifo文件中

cat 命令读取fifo文件并且其内容发送给sh命令

sh命令进程受到输入并把它写回到netcat。

netcat 通过网络发送输出到client

至于为什么会成功是因为管道使命令平行执行,fifo文件用来替代正常文件,因为fifo使读取等待而如果是一个普通文件,cat命令会尽快结束并开始读取空文件。

在客户端仅仅简单连接到服务器

Client

1
$nc -n 172.31.100.7 1567

你会得到一个shell提示符在客户端

9,反向shell

反向shell是指在客户端打开的shell。反向shell这样命名是因为不同于其他配置,这里服务器使用的是由客户提供的服务。

服务端

1
$nc -l 1567

在客户端,简单地告诉netcat在连接完成后,执行shell。

客户端

1
$nc 172.31.100.7 1567 -e /bin/bash

现在,什么是反向shell的特别之处呢
反向shell经常被用来绕过防火墙的限制,如阻止入站连接。例如,我有一个专用IP地址为172.31.100.7,我使用代理服务器连接到外部网络。如果我想从网络外部访问 这台机器如1.2.3.4的shell,那么我会用反向外壳用于这一目的。

10. 指定源端口

假设你的防火墙过滤除25端口外其它所有端口,你需要使用-p选项指定源端口。

服务器端

1
$nc -l 1567

客户端

1
$nc 172.31.100.7 1567 -p 25

使用1024以内的端口需要root权限。

该命令将在客户端开启25端口用于通讯,否则将使用随机端口。

11. 指定源地址

假设你的机器有多个地址,希望明确指定使用哪个地址用于外部数据通讯。我们可以在netcat中使用-s选项指定ip地址。

服务器端

1
$nc -u -l 1567 < file.txt

客户端

1
$nc -u 172.31.100.7 1567 -s 172.31.100.5 > file.txt

该命令将绑定地址172.31.100.5。

这仅仅是使用netcat的一些示例。

其它用途有:

  • 使用-t选项模拟Telnet客户端,
  • HTTP客户端用于下载文件,
  • 连接到邮件服务器,使用SMTP协议检查邮件,
  • 使用ffmpeg截取屏幕并通过流式传输分享,等等。其它更多用途。

简单来说,只要你了解协议就可以使用netcat作为网络通讯媒介,实现各种客户端。

参考文档

Netcat手册

linux设备驱动程序(3) – 字符设备驱动(设备号 注册设备)

这次我们学习最简单的一种设备, 字符设备驱动的开发. 最终写出一个字符设备, 用户可以进行打开和关闭, 并向他写入数据, 它会始终保存着最后一次写入的数据, 对它进行读取会读出最后一次写入的数据.

1.设备号

在linux中执行ls -l命令, 在日期之前可以看到两个用逗号隔开的数, 这个便是设备号, 逗号之前的是主设备号(major)后面的是次设备号(minor).

在内核中, 设备号使用dev_t来表示(<linux/types.h>). dev_t可以同时保存主设备号和次设备号, 当需要从dev_t中获取主设备号或次设备号时, 可以使用以下:

1
2
MAJOR(dev_t dev);
MINOR(dev_t dev);

相反, 如果需要用设备号构造出dev_t则使用:

1
MKDEV(int major, int minor);

2.分配设备号

建立字符设备前, 驱动程序首先应该分配设备号, 有三个函数用来分配(注册)设备号和释放设备号(在<linux/fs.h>中):

1
2
3
4
5
int register_chrdev_region(dev_t first, unsigned int count, 
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);

register_chrdev_region函数用在已知设备号的情况下向内核进行注册, alloc用在设备号不确定的情况下, 向内核动态分配设备号. first是申请的设备号的第一个, count是要连续申请的个数(次设备号的个数, 比如first是[10, 102], count是4, 则会申请[10,102][10,103][10,104][10,105]这四个). name是设备的名称, 将出现在/proc/devices和sysfs中. 如果出错返回负的错误号.

alloc函数成功后通过dev返回第一个设备号.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DEBUG_LOG("", "allocating device number\n");
/* 申请设备号 */
if (chr_major != 0) /* 如果用户提供了设备号 */
{
dev = MKDEV(chr_major, chr_minor);
ret = register_chrdev_region(dev, 1, DEV_NAME);
}
else
{
ret = alloc_chrdev_region(&dev, chr_minor, 1, DEV_NAME);
chr_major = MAJOR(dev);
}
if (ret < 0)
{
DEBUG_LOG(KERN_WARNING, "cannot get major%d\n", chr_major);
goto err;
}
DEBUG_LOG("", "success\n");
DEBUG_LOG("", "chr_major=%d, chr_minor=%d\n", chr_major, chr_minor);

3.重要的数据结构

关于字符设备驱动, 有三个重要的数据结构, 他们都在<linux/fs.h>中.分别是:

  • struct file_operations
  • struct file
  • struct inode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
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 *, loff_t, loff_t, 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 (*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 **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep_links, f_flags, f_pos vs i_size in lseek SEEK_CUR.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
#ifdef CONFIG_SMP
int f_sb_list_cpu;
#endif
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
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;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
1
2
3
4
5
6
7
8
9
10
struct inode {
...
dev_t i_rdev;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
...
};

对于一个字符设备, 应当满足linux对于字符设备的定义(既它可以进行字符设备的操作, 如打开,读取,写入,ioctl,关闭), 所以他应当自己定义这些操作的函数, 然后把函数指针保存在struct file_operations结构中传递给内核. 这样内核就可以在用户对设备调用这些函数时调用适当的驱动程序. 所以在struct file_operations中可以看到, 大部分的都是函数指针, 而有一个成员例外, struct module *owner成员应给它赋值THIS_MODULE.

例如:

1
2
3
4
5
6
7
8
struct file_operations chr_fops = 
{
.owner = THIS_MODULE,
.open = open,
.release = release,
.read = read,
.write = write,
};

对于file和inode, 我的理解是, 用户每打开一个文件, 将产生一个file结构, 但是一个文件只有一个inode(硬盘上也保存着inode, 用户打开时将会读入内存). 所以看open和release函数的签名都有一个file和inode, 而其他函数只有file. 因为当打开文件时, 应当让file和inode建立联系, 关闭时应当解除联系. 所以内核的接口是这样设计的.

4.注册字符设备

刚刚只是分配了设备号了, 内核其实连你的设备是什么类型都不知道. 所以第二步应该注册设备. 字符设备的注册用到的结构体为struct cdev(<linux/cdev.h>, 刚刚在inode结构体中也看见这个结构了i_cdev)

cdev结构体的分配有两个函数:

1
2
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *cdev, struct file_operations *fops);

第一个函数会分配内存空间, 并进行初始化cdev, 第二个只是会初始化cdev.

cdev有两个重要的字段, owner和ops, owner应该设置成THIS_MODULE, ops应当设置成一个指向struct file_operations的指针.

cdev结构构造好之后通过这两个函数注册和移除字符设备:

1
2
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);

看看通过cdev_add我们传递给内核什么信息:

  • 通过num和count传递了设备号
  • 通过cdev内的ops传递了文件操作的函数指针

这样, 当有用户请求对num指定的设备号的设备进行操作时, 就会通过ops中的指针调用相应的函数了. 这就完成了字符设备的注册.cdev_add会返回错误代码

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DEBUG_LOG("", "register char device\n");
/* 注册字符设备 */
chr_dev = kmalloc(sizeof(chr_dev), GFP_KERNEL);
if (chr_dev == NULL)
{
DEBUG_LOG(KERN_WARNING, "no memory\n");
ret = -ENOMEM;
goto err1;
}
memset(chr_dev, 0, sizeof(*chr_dev));
cdev_init(&chr_dev->cdev, &chr_fops);
chr_dev->cdev.owner = THIS_MODULE;
chr_dev->cdev.ops = &chr_fops;
if ((ret = cdev_add(&chr_dev->cdev, dev, 1)) != 0)
{
DEBUG_LOG(KERN_WARNING, "Error %d adding chr\n", ret);
goto err2;
}
DEBUG_LOG("", "success\n");

我们使用一个自己的结构来保存cdev和相关的信息chr_dev.

5.具体的驱动程序如何写

前面的所有操作讲的都是驱动程序的初始化操作, 都是应当写到module_init函数中的. 下面将驱动真正的事件处理函数应当怎么写.

我们的功能是这个设备可以打开,关闭,读取,写入. 所以我们只需实现这几个函数, 如果用户对设备调用其他的函数, 内核会有相应的默认操作.

1)打开

前面说了, 打开操作就是将inode和file结构体建立联系, 这两个结构体内核都会在用户打开/关闭文件时自动创建/销毁, 驱动程序不用照看他们的生命周期.

open函数会用参数传来inode和file, inode是内核根据用户打开的文件创建的, 因为文件系统保存着设备文件的设备号, 而刚刚我们通过cdev_add函数将设备号和cdev建立了联系所以这个inode中会有一个指向对应cdev的指针. 但是file中没有, 我们要做的就是让每个打开的file也保存起这个指针(通过保存在filp->private_data中).

但是我们的cdev保存在chr_dev结构中, 而且这个结构中还有另外一些我们感兴趣的东西, 所以不如直接保存chr_dev结构.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int open(struct inode *inode, struct file *filp)
{
struct chr_dev *dev;
DEBUG_LOG("", "open from user\n");

dev = container_of(inode->i_cdev, struct chr_dev, cdev);
filp->private_data = dev;

if ((filp->f_flags & O_ACCMODE) == O_WRONLY)
{
dev->last_char = 0;
}

DEBUG_LOG("", "open success\n");
return 0;
}
2)关闭

当用户关闭文件时, 我们应当:

  • 释放filp->private_data中我们分配的数据(如果不再使用)
  • 如果是最后一个文件被关闭, 关闭硬件(如果需要的话[硬件可能会费电])

我们这两个工作都不用做.

3)读写

这个很简单了没什么要说的. 注意一点, 传来的buf指针是用户空间的指针(用__user修饰), 我们不能对这个指针进行解引用, 因为它根本不指向我们这个空间(内核空间)的数据. 而对它进行读写只能通过函数copy_from_user/copy_to_user进行(<linux/uaccess>).

另外, 如果在内核空间需要分配内存, 应使用kmalloc和kfree(<linux/stab.h>)

直接看最终代码吧

6.最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#include <linux/module.h>
#include <linux/init.h> /* module_* */
#include <linux/kernel.h> /* printk */
#include <linux/moduleparam.h> /* module_param */
#include <linux/types.h> /* dev_t */
#include <linux/fs.h> /* reg*_chrdev */
#include <linux/cdev.h> /* cdev_add */
#include <asm/uaccess.h> /* copy_*_user */
#include <linux/string.h> /* memset */
#include <linux/slab.h> /* kmalloc */

#define DEV_NAME "chr"

#define _DEBUG

#ifdef _DEBUG

#define DEBUG_LOG(lvl, fmt, ...) \
printk(lvl "%s: %s:%d <%s>: " fmt, \
DEV_NAME, __FILE__, __LINE__, __func__, ##__VA_ARGS__)

#else

#define DEBUG_LOG(lvl, fmt, ...)

#endif

MODULE_LICENSE("Dual BSD/GPL");

struct chr_dev
{
char last_char;
struct cdev cdev;
};

int open(struct inode *inode, struct file *filp)
{
struct chr_dev *dev;
DEBUG_LOG("", "open from user\n");

dev = container_of(inode->i_cdev, struct chr_dev, cdev);
filp->private_data = dev;

if ((filp->f_flags & O_ACCMODE) == O_WRONLY)
{
dev->last_char = 0;
}

DEBUG_LOG("", "open success\n");
return 0;
}

int release(struct inode *inode, struct file *filp)
{
DEBUG_LOG("", "release from user\n");
DEBUG_LOG("", "release success\n");
return 0;
}

ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
struct chr_dev *dev = filp->private_data;
char* kbuf = kmalloc(count, GFP_KERNEL);
DEBUG_LOG("", "read from user\n");

if (kbuf == NULL)
{
DEBUG_LOG(KERN_WARNING, "no memory\n");
goto err1;
}
memset(kbuf, dev->last_char, count);

if (copy_to_user(buf, kbuf, count) != 0)
{
DEBUG_LOG(KERN_WARNING, "copy_to_user error\n");
goto err;
}

kfree(kbuf);

DEBUG_LOG("", "read success count=%d\n", count);
return count;

err1:
kfree(kbuf);
err:
return -EFAULT;
}

ssize_t write(struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
struct chr_dev *dev = filp->private_data;
DEBUG_LOG("", "write from user\n");
if (copy_from_user(&dev->last_char, buf + count - 1, 1) != 0)
{
DEBUG_LOG(KERN_WARNING, "copy_from_user error\n");
return -EFAULT;
}
DEBUG_LOG("", "write success count=%d\n", count);
return count;
}

struct file_operations chr_fops =
{
.owner = THIS_MODULE,
.open = open,
.release = release,
.read = read,
.write = write,
};

int chr_major = 0;
int chr_minor = 0;
//module_param(chr_major, int, S_IRUGO);

struct chr_dev* chr_dev;

int chr_init(void)
{
int ret;
dev_t dev;

DEBUG_LOG("", "allocating device number\n");

/* 申请设备号 */
if (chr_major != 0) /* 如果用户提供了设备号 */
{
dev = MKDEV(chr_major, chr_minor);
ret = register_chrdev_region(dev, 1, DEV_NAME);
}
else
{
ret = alloc_chrdev_region(&dev, chr_minor, 1, DEV_NAME);
chr_major = MAJOR(dev);
}
if (ret < 0)
{
DEBUG_LOG(KERN_WARNING, "cannot get major%d\n", chr_major);
goto err;
}
DEBUG_LOG("", "success\n");
DEBUG_LOG("", "chr_major=%d, chr_minor=%d\n", chr_major, chr_minor);

DEBUG_LOG("", "register char device\n");
/* 注册字符设备 */
chr_dev = kmalloc(sizeof(chr_dev), GFP_KERNEL);
if (chr_dev == NULL)
{
DEBUG_LOG(KERN_WARNING, "no memory\n");
ret = -ENOMEM;
goto err1;
}
memset(chr_dev, 0, sizeof(*chr_dev));
cdev_init(&chr_dev->cdev, &chr_fops);
chr_dev->cdev.owner = THIS_MODULE;
chr_dev->cdev.ops = &chr_fops;
if ((ret = cdev_add(&chr_dev->cdev, dev, 1)) != 0)
{
DEBUG_LOG(KERN_WARNING, "Error %d adding chr\n", ret);
goto err2;
}
DEBUG_LOG("", "success\n");

return 0;

err2:
kfree(chr_dev);
err1:
unregister_chrdev_region(dev, 1);
err:
return ret;
}

void chr_exit(void)
{
DEBUG_LOG("", "unloading module\n");

cdev_del(&chr_dev->cdev);
kfree(chr_dev);
unregister_chrdev_region(MKDEV(chr_major, chr_minor), 1);

DEBUG_LOG("", "success\n");
}

module_init(chr_init);
module_exit(chr_exit);

linux设备驱动程序(2) – 内核版本 导出符号 模块参数

关于 内核版本 导出符号 模块参数 的内容

1.内核版本

有时模块会针对多个不同版本的内核进行编译, 这时就应该用到预处理命令来实现条件编译. 主要通过测试<linux/version.h>中的宏来完成.

UTS_RELEASE: 内核版本的字符串, 如”2.6.10″

LINUX_VERSION_CODE: 内核版本的二进制表示, 每个版本号对应一个字节, 如2.6.10版的LINUX_VERSION_CODE为0x02060a

KERNEL_VERSION(major, minor, release): 这个宏将major, minor和release扩展成LINUX_VERSION_CODE的形式, 可以直观的进行测试

2.导出模块符号

有时, 模块可能需要被其他模块来调用, 所以需要导出自己模块的符合供别人使用. Linux内核头文件提供了一个方便的方法来到处内核符号:

1
2
EXPORT_SYMBOL(name);		//导出name
EXPORT_SYMBOL_GPL(name); //导出的符号只能被GPL代码使用

3.许可证

通过MODULE_LECENSE(license)宏来声明当前模块的许可证, license是一个字符串, 支持的许可证如下:

“GPL”, “GPL v2”, “GPL and additional rights”, “Dual BSD/GPL”, “Dual MPL/GPL”, “Proprietary”(专用)

如果不声明, 默认为”Proprietary”

4.模块参数

linux内核模块可以声明全局变量为模块参数, 从而可以在加载时赋值.

1
2
3
4
static char* whom = "nobody";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

通过module_param宏可以导出whom和howmany两个模块参数. 这个宏在<linux/moduleparam.h>中定义.
当插入模块时, 使用以下指令来设置模块参数:

1
insmod hello howmany=10 whom="zzZ"

对于module_param宏的第二个参数, 可以取以下值:

bool
invbool
    布尔值, 应关联int型, invbool会翻转布尔值.
charp
    字符指针, 也就是字符串
int, long, short, uint, ulong, ushort

还有一个姐妹宏module_param_array(name, type, num, perm);来设置数组参数.

再来说说第三个参数perm, 这个是成员访问许可, 在<linux/stat.h>中定义可选值. 这个用来控制谁能访问sysfs中对模块参数的表述. 如果perm为0, 则在sysfs中没有对用的入口项(entry), 否则会在/sys/module中出现. 如果使用S_IRUGO, 任何人都可读取, 但不能修改. S_IRUGO | S_IWUSR允许root修改.

记录一下开发过程的小插曲minimad

libmad是一个开源mp3软件解码库(很久以前的东西了, 原来曾用过, 突然有想法把它放我mk808上试试), c+汇编写的, 本身对arm平台有汇编优化, 可是因为年代久远而且是针对armv4优化的, 优化了还不如不优化(优化了反而有杂音, 关了之后很好).

源代码目录里有一个示例用的minimad.c, 是基于这个库的一个简单的mp3播放器. 插上我的号称7.1channel的usb外置声卡(20块淘宝货), 和aplay连用, 就能听歌了!(geek就是爱折腾, 没办法)

首先, 从sourceforge.net上找到libmad并wget到mk808上, cd入目录, ./configure –disable-fpm关掉平台优化.

然后需要改一下Makefile, 打开Makefile查找-fforce-mem, 删掉它(这是gcc老版本才有的选项)

然后make(编译libmad), make minimad(编译minimad).

没什么错误的话就会在当前目录看见一个绿色的minimad了.

这个minimad说是播放器, 不如说是解码器…因为它只会从stdin读入mp3文件, 然后把解码后的pcm数据输出到stdout, 所以还得借助aplay来控制硬件播放.

这是命令:

1
sudo bash -c "minimad <1.mp3 | aplay -f cd"

这个是让1.mp3作为minimad的输入, 然后输出通过管道给aplay. -f是format, 使用CD格式的format, 也就是16 bit little endian, 44100, stereo, 正好是minimad的输出格式. 然后带上耳机, 就能听到美妙的歌声了.

Proudly powered by Hexo and Theme by Hacker
© 2021 wastecat