进程间通信组件,大家应该见得多了。shm、mmap、pipe、fifo……这些就不介绍了。今天介绍一种新的方式。
在内核上添加一个设备文件,一个进程向这个文件里写数据,另一个进程从这个文件读数据,这就是原理。(好水……)为什么要用设备文件而不是普通文件呢?因为普通文件无法在文件有数据时立即通知读进程,只能靠轮询,也就是说,实时性会差一些。
当然,内核模块里还有块设备文件模块、网卡设备模块,今天选择字符设备模块。
这里一点一点贴代码。代码整体流程不难,主要是有很多涉及内核编程相关的代码需要解释。
先是模块初始化的代码,这几行记住就好了。
MODULE_LICENSE("GPL");
模块应当指定它的代码使用哪个许可。做到这一点只需包含上面一行代码即可。
内核认识的特定许可有“GPL”( 适用 GNU 通用公共许可的任何版本 )、“GPL v2”( 只适用 GPL 版本 2 )、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和 “Proprietary”。除非你的模块明确标识是在内核认识的一个自由许可下, 否则就假定它是私有的,内核在模块加载时被"弄污浊"了。内核开发者不会热心帮助在加载了私有模块后遇到问题的用户。
展开宏定义
static const char __mod_licenseXX[] \
__used __attribute__((section(".modinfo"),unused,aligned(1))) \
= "license= GPL"
第二行的意思是把__mod_licenseXX变量放在.modinfo对应的段中。关于内核编译的代码就不再详细介绍了,不然篇幅又打不住了。
module_init(channel_init);
module_exit(channel_exit);
这两个函数是在insmod和rmmod时调用的函数,其实是调用channel_init和channel_exit。
定义了一些全局变量、宏定义和一个结构体
#ifndef CHANNEL_MAJOR
#define CHANNEL_MAJOR 96 //主设备号
#endif
#ifndef CHANNEL_NR_DEVS
#define CHANNEL_NR_DEVS 2 //允许设备数
#endif
#ifndef CHANNEL_SIZE
#define CHANNEL_SIZE 4096
#endif
#define ENABLE_POLL 1
//每个channel的数据
struct channel {
char *data;
unsigned long size;
#if ENABLE_POLL
wait_queue_head_t inq;
#endif
};
static int channel_major = CHANNEL_MAJOR;
/*
使用module_param指定模块的参数
insmod时可以传入参数,如 insmod channel.ko channel_major=96
如果不传,则为代码中原有的值
*/
module_param(channel_major, int, S_IRUGO); //S_IRUGO为0444,即都是只给了读权限
struct channel *channel_devp;
//cdev结构体用来描述一个字符设备
struct cdev cdev; //代码后面贴
char have_data = 0;
cdev结构体
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
主要实现这么几个操作
//正是因为这个结构体,才有了linux一切皆文件
static const struct file_operations channel_fops = {
.owner = THIS_MODULE, //THIS_MODULE是宏定义,代表当前模块
.llseek = channel_llseek,
.read = channel_read,
.write = channel_write,
.open = channel_open,
.release = channel_release,
.poll = channel_poll,
.mmap = channel_mmap,
};
这个结构体应该比较熟悉,是之前介绍过的关于文件操作的集合。下面是具体的各个操作
int channel_open(struct inode *inode, struct file *filp) {
struct channel *channel;
//MINOR宏,取设备号低20位,即次设备号
int num = MINOR(inode->i_rdev);
if (num >= CHANNEL_NR_DEVS)
return -ENODEV;
channel = &channel_devp[num];
//文件私有域,用来存储数据
filp->private_data = channel;
return 0;
}
//关闭文件,这里不需要额外做什么,vfs会帮我们回收fd
int channel_release(struct inode *inode, struct file *filp) {
return 0;
}
//__user告诉内核,buffer值是一个用户空间的地址
ssize_t channel_read(struct file *filp, char __user *buffer, size_t size, loff_t *ppos) {
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct channel *channel = filp->private_data;
if (p >= CHANNEL_SIZE) {
return 0;
}
if (count > CHANNEL_SIZE - p) {
count = CHANNEL_SIZE - p;
}
#if ENABLE_POLL
while (!have_data) {
if (filp->f_flags & O_NONBLOCK) return -EAGAIN;
//阻塞当前进程,当调用wake_up且have_data为1时唤醒
wait_event_interruptible(channel->inq, have_data);
}
#endif
//从内核空间拷贝到用户空间,返回值是没有拷贝成功的数据字节数,成功就是0
if (copy_to_user(buffer, (void*)(channel->data+p), count)) {
ret = -EFAULT;
} else {
ret = strlen(buffer);
channel->size -= ret;
printk(KERN_INFO "read %d byte(s) from %ld\n", ret, p);
}
have_data = 0;
return ret;
}
//loff_t 为 long long 类型,表示文件指针的偏移量
ssize_t channel_write(struct file *filp, const char __user *buffer, size_t size, loff_t *ppos) {
int ret = 0;
unsigned long p = *ppos;
unsigned int count = size;
struct channel *channel = filp->private_data;
if (p >= CHANNEL_SIZE) {
return 0;
}
if (count > CHANNEL_SIZE - p) {
count = CHANNEL_SIZE - p;
}
if (copy_from_user(channel->data+p, buffer, count)) {
return -EFAULT;
} else {
*ppos += count;
ret = count;
channel->size += count;
*(channel->data+p + count) = '\0';
printk(KERN_INFO "written %d byte(s) from %ld\n", count, p);
}
#if ENABLE_POLL
have_data = 1;
wake_up(&channel->inq);
#endif
return ret;
}
#if ENABLE_POLL
unsigned int channel_poll(struct file *filp, struct poll_table_struct *wait) {
struct channel *channel = filp->private_data;
unsigned int mask = 0;
poll_wait(filp, &channel->inq, wait);
if (have_data)
mask |= (POLLIN | POLLRDNORM); //POLLRDNORM可以不阻塞的读普通数据
return mask;
}
#endif
loff_t channel_llseek(struct file *filp, loff_t offset, int whence) {
loff_t newpos;
switch (whence) {
case 0: {
newpos = offset;
break;
}
case 1: {
newpos = filp->f_pos + offset;
break;
}
case 2: {
newpos = CHANNEL_SIZE - 1 + offset;
break;
}
default: {
return -EINVAL;
}
}
if (newpos < 0 || newpos > CHANNEL_SIZE) {
return -EINVAL;
}
filp->f_pos = newpos;
return newpos;
}
//vfs已经做好了从文件到内核空间的映射
//我们需要做的就是从内核空间到用户空间的映射,也就是remap_pfn_range
int channel_mmap(struct file *filp, struct vm_area_struct *vma) {
struct channel *channel = filp->private_data;
vma->vm_flags |= VM_IO;
vma->vm_flags |= (VM_DONTEXPAND | VM_DONTDUMP);
//remap_pfn_range映射内核内存到用户空间
//第二个参数是目标用户空间起始地址,第三个参数是内核内存的物理地址
if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(channel->data) >> PAGE_SHIFT,
vma->vm_end-vma->vm_start, vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
}
执行命令insmod、rmmod会最终执行这两个函数。
static int channel_init(void) {
int result;
int i;
//dev_t数据类型存储设备号
//dev_t是一个32位的数,12位表示主设备号,另外20位表示次设备号
dev_t devno = MKDEV(channel_major, 0); //根据主次设备号得到devno
if (channel_major) {
//注册字符设备,需要指定主设备号
//事先知道要使用的主设备号时使用的,要先查看cat /proc/devices去查看没有使用的
result = register_chrdev_region(devno, CHANNEL_NR_DEVS, "channel");
} else {
//和上面的函数在底层其实调用的是同一个函数,区别在于是否给定主设备号
//由内核自动分配主设备号
result = alloc_chrdev_region(&devno, 0, CHANNEL_NR_DEVS, "channel");
channel_major = MAJOR(devno);
}
if (result < 0) return result;
cdev_init(&cdev, &channel_fops);
cdev.owner = THIS_MODULE;
//将cdev 添加到cdev_map中,也就是添加到内核中,注册这个字符设备
cdev_add(&cdev, MKDEV(channel_major, 0), CHANNEL_NR_DEVS);
channel_devp = kmalloc(CHANNEL_NR_DEVS * sizeof(struct channel), GFP_KERNEL);
if (!channel_devp) {
result = -ENOMEM;
goto fail_malloc;
}
memset(channel_devp, 0, sizeof(struct channel));
for (i = 0;i < CHANNEL_NR_DEVS;i ++) {
channel_devp[i].size = CHANNEL_SIZE;
channel_devp[i].data = kmalloc(CHANNEL_SIZE, GFP_KERNEL);
memset(channel_devp[i].data, 0, CHANNEL_SIZE);
#if ENABLE_POLL
init_waitqueue_head(&(channel_devp[i].inq));
#endif
}
printk(KERN_INFO "channel_init");
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
static void channel_exit(void) {
printk(KERN_INFO "channel_exit");
cdev_del(&cdev);
kfree(channel_devp);
unregister_chrdev_region(MKDEV(channel_major, 0), 2);
}
第一步,先向内核中插入这个模块,使用编译好的.ko文件
insmod channel.ko
insmod与module_init()就做到了/dev/channel与.c文件的关联。
第二步,使用mknod命令。这个命令指定名称产生一个FIFO(命名管道),字符专用或块专用文件。通常,一个专用文件并不在磁盘上占用空间,仅仅是为操作系统提供交流,而不是为数据存贮服务。一般地,专用文件会指向一个硬件设备(如:磁盘、磁带、打印机、虚拟控制台)或者操作系统提供的服务(如:/dev/null, /dev/random)。
mknod /dev/channel c 96 0
mknod 命令建立一个目录项和一个特殊文件的对应索引节点。第一个参数是设备文件的路径。mknod 命令有两种形式,它们有不同的标志。第一种形式中,使用了b 或 c 标志。b 标志表示这个特殊文件是面向块的设备(磁盘、软盘或磁带)。c 标志表示这个特殊文件是面向字符的设备(其他设备)。最后两个参数是指定主设备的数目,它帮助操作系统查找设备驱动程序代码,和指定次设备的数目,也就是单元驱动器或行号,它们是十进制或八进制的。一个设备的主要和次要编号由该设备的配置方法分配,它们保存在 ODM 中的 CuDvDr 类里。在这个对象类中定义了主要和次要编号以确保整个系统设备定义的一致性,这是很重要的。
在 mknod 命令的第二种形式中,使用了 p 标志来创建 FIFO(已命名的管道)。与今天主题无关,暂不介绍。
最后,在前两步准备好了之后,就可以通信了。一个进程往里写,一个进程往外读。读进程执行应用程序
int main() {
int fd = open("/dev/channel", O_RDWR);
if (fd < 0) {
printf("open failed : errno : %d\n", errno);
return -1;
}
char *buffer = (char *)malloc(BUFFER_LENGTH);
memset(buffer, 0, BUFFER_LENGTH);
char *start = mmap(NULL, BUFFER_LENGTH, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
fd_set rds;
FD_ZERO(&rds);
FD_SET(fd, &rds);
while (1) {
int ret = select(fd+1, &rds, NULL, NULL, NULL);
if (ret < 0) {
printf("select error\n");
exit(1);
}
if (FD_ISSET(fd, &rds)) {
#if 0
strcpy(buffer, start);
printf("channel: %s\n", buffer);
#else
read(fd, buffer, BUFFER_LENGTH);
printf("channel: %s\n", buffer);
#endif
}
}
//printf("buf 2 = %s\n", buffer);
munmap(start, BUFFER_LENGTH);
free(buffer);
close(fd);
return 0;
}
再介绍一下poll/select什么时候效率比epoll高。poll/select每次调用时需要将全部fd都从用户态到内核态,不管有没有数据。如果管理的fd数有几十个,这个时候就建议用epoll了,而不是1024个。当然,如果管理的fd都有数据,还是建议用poll,因为epoll建立红黑树还有所消耗。
对于写进程,可以使用echo等方式写入文件/dev/channel,读端就可以读出数据了。