转自:http://blog.chinaunix.net/uid-20937170-id-3033633.html

学习了驱动程序的设计,感觉在学习驱动的同时学习linux内核,也是很不错的过程哦,做了几个实验,该做一些总结,只有不停的作总结才能印象深刻。

我的平台是虚拟机,fedora14,内核版本为2.6.38.1.其中较之前的版本存在较大的差别,具体的实现已经在上一次总结中给出了。今天主要总结的是ioctl和堵塞读写函数的实现。
 
一、ioctl函数的实现
首先说明在2.6.36以后ioctl函数已经不再存在了,而是用unlocked_ioctl和compat_ioctl两个函数实现以前版本的ioctl函数。同时在参数方面也发生了一定程度的改变,去除了原来ioctl中的struct inode参数,同时改变了返回值。
但是驱动设计过程中存在的问题变化并不是很大,同样在应用程序设计中我们还是采用ioctl实现访问,而并不是unlocked_ioctl函数,因此我们还可以称之为ioctl函数的实现。
ioctl函数的实现主要是用来实现具体的硬件控制,采用相应的命令控制硬件的具体操作,这样就能使得硬件的操作不再是单调的读写操作。使得硬件的使用更加的方便。
ioctl函数实现主要包括两个部分,首先是命令的定义,然后才是ioctl函数的实现,命令的定义是采用一定的规则。
ioctl的命令主要用于应用程序通过该命令操作具体的硬件设备,实现具体的操作,在驱动中主要是对命令进行解析,通过switch-case语句实现不同命令的控制,进而实现不同的硬件操作。
 
ioctl函数的命令定义方法:
int (*unlocked_ioctl)(struct file*filp,unsigned int cmd,unsigned long arg)
虽然其中没有指针的参数,但是通常采用arg传递指针参数。cmd是一个命令。每一个命令由一个整形数据构成(32bits),将一个命令分成四部分,每一部分实现具体的配置,设备类型(幻数)8bits,方向2bits,序号8bits,数据大小13/14bits。命令的实现实质上就是通过简单的移位操作,将各个部分组合起来而已。
一个命令的分布的大概情况如下:
 
|---方向位(31-30)|----数据长度(29-16)----------------|---------设备类型(15-8)------|----------序号(7-0)----------|
|----------------------------------------------------------------------------------------------------------------------------------------|
 
其中方向位主要是表示对设备的操作,比如读设备,写设备等操作以及读写设备等都具有一定的方向,2个bits只有4种方向。
数据长度表示每一次操作(读、写)数据的大小,一般而已每一个命令对应的数据大小都是一个固定的值,不会经常改变,14bits说明可以选择的数据长度最大为16k。
设备类型类似于主设备号(由于8bits,刚好组成一个字节,因此经常采用字符作为幻数,表示某一类设备的命令),用来区别不同的命令类型,也就是特定的设备类型对应特定的设备。序号主要是这一类命令中的具体某一个,类似于次设备号(256个命令),也就是一个设备支持的命令多达256个。
 
同时在内核中也存在具体的宏用来定义命令以及解析命令。
但是大部分的宏都只是定义具体的方向,其他的都需要设计者定义。
主要的宏如下:
#include
 
_IO(type,nr)                    表示定义一个没有方向的命令,
_IOR(type,nr,size)            表示定义一个类型为type,序号为nr,数据大小为size的读命令
_IOW(type,nr,size)           表示定义一个类型为type,序号为nr,数据大小为size的写命令
_IOWR(type,nr,size)         表示定义一个类型为type,序号为nr,数据大小为size的写读命令
 
通常的type可采用某一个字母或者数字作为设备命令类型。
是实际运用中通常采用如下的方法定义一个具体的命令:
  1. //头文件
  2. #include
  3. /*定义一系列的命令*/
  4. /*幻数,主要用于表示类型*/
  5. #define MAGIC_NUM 'k'
  6. /*打印命令*/
  7. #define MEMDEV_PRINTF _IO(MAGIC_NUM,1)
  8. /*从设备读一个int数据*/
  9. #define MEMDEV_READ _IOR(MAGIC_NUM,2,int)
  10. /*往设备写一个int数据*/
  11. #define MEMDEV_WRITE _IOW(MAGIC_NUM,3,int)
  12. /*最大的序列号*/
  13. #define MEM_MAX_CMD 3
还有对命令进行解析的宏,用来确定具体命令的四个部分(方向,大小,类型,序号)具体如下所示:
  1. /*确定命令的方向*/
  2. _IOC_DIR(nr)
  3. /*确定命令的类型*/
  4. _IOC_TYPE(nr)
  5. /*确定命令的序号*/
  6. _IOC_NR(nr)
  7. /*确定命令的大小*/
  8. _IOC_SIZE(nr)
上面的几个宏可以用来命令,实现命令正确性的检查。
 
ioctl的实现过程主要包括如下的过程:
1、命令的检测
2、指针参数的检测
3、命令的控制switch-case语句
 
1、命令的检测主要包括类型的检查,数据大小,序号的检测,通过结合上面的命令解析宏可以快速的确定。
  1. /*检查类型,幻数是否正确*/
  2. if(_IOC_TYPE(cmd)!=MAGIC_NUM)
  3. return -EINVAL;
  4. /*检测命令序号是否大于允许的最大序号*/
  5. if(_IOC_NR(cmd)> MEM_MAX_CMD)
  6. return -EINVAL;
2、主要是指针参数的检测。指针参数主要是因为内核空间和用户空间的差异性导致的,因此需要来自用户空间指针的有效性。使用copy_from_user,copy_to_user,get_user,put_user之类的函数时,由于函数会实现指针参量的检测,因此可以省略,但是采用__get_user(),__put_user()之类的函数时一定要进行检测。具体的检测方法如下所示:
  1. if(_IOC_DIR(cmd) & _IOC_READ)
  2. err = !access_ok(VERIFY_WRITE,(void *)args,_IOC_SIZE(cmd));
  3. else if(_IOC_DIR(cmd) & _IOC_WRITE)
  4.  err = !access_ok(VERIFY_READ,(void *)args,_IOC_SIZE(cmd));
  5. if(err)/*返回错误*/
  6. return -EFAULT;
当方向是读时,说明是从设备读数据到用户空间,因此要检测用户空间的指针是否可写,采用VERIFY_WRITE,而当方向是写时,说明是往设备中写数据,因此需要检测用户空间中的指针的可读性VERIFY_READ。检查通常采用access_ok()实现检测,第一个参数为读写,第二个为检测的指针,第三个为数据的大小。
3、命名的控制:
命令的控制主要是采用switch和case相结合实现的,这于window编程中的检测各种消息的实现方式是相同的。
  1. /*根据命令执行相应的操作*/
  2. switch(cmd)
  3. {
  4. case MEMDEV_PRINTF:
  5. printk("<--------CMD MEMDEV_PRINTF Done------------>\n\n");
  6. ...
  7. break;
  8. case MEMDEV_READ:
  9. ioarg = &mem_devp->data;
  10. ...
  11. ret = __put_user(ioarg,(int *)args);
  12. ioarg = 0;
  13. ...
  14. break;
  15. case MEMDEV_WRITE:
  16. ...
  17. ret = __get_user(ioarg,(int *)args);
  18. printk("<--------CMD MEMDEV_WRITE Done ioarg = %d--------->\n\n",ioarg);
  19. ioarg = 0;
  20. ...
  21. break;
  22. default:
  23. ret = -EINVAL;
  24. printk("<-------INVAL CMD--------->\n\n");
  25. break;
  26. }
这只是基本的框架结构,实际中根据具体的情况进行修改。这样就实现了基本的命令控制。
文件操作支持的集合如下:
  1. /*添加该模块的基本文件操作支持*/
  2. static const struct file_operations mem_fops =
  3. {
  4. /*结尾不是分号,注意其中的差别*/
  5. .owner = THIS_MODULE,
  6. .llseek = mem_llseek,
  7. .read = mem_read,
  8. .write = mem_write,
  9. .open = mem_open,
  10. .release = mem_release,
  11. /*添加新的操作支持*/
  12. .unlocked_ioctl = mem_ioctl,
  13. };
需要注意不是ioctl,而是unlocked_ioctl。
 
 
二、设备的堵塞读写方式实现,通常采用等待队列。
设备的堵塞读写方式,默认情况下的读写操作都是堵塞型的,具体的就是如果需要读数据,当设备中没有数据可读的时候应该等待设备中有设备再读,当往设备中写数据时,如果上一次的数据还没有被读完成,则不应该写入数据,就会导致进程的堵塞,等待数据可读写。但是在应用程序中也可以采用非堵塞型的方式进行读写。只要在打开文件的时候添加一个O_NONBLOCK,这样在不能读写的时候就会直接返回,而不会等待。
因此我们在实际设计驱动设备的同时需要考虑读写操作的堵塞方式。堵塞方式的设计主要是通过等待队列实现,通常是将等待队列(实质就是一个链表)的头作为设备数据结构的一部分。在设备初始化过程中初始化等待队列的头。最后在设备读写操作的实现添加相应的等待队列节点,并进行相应的控制。
 
等待队列的操作基本如下:
1、等待队列的头定义并初始化的过程如下:
方法一:
struct wait_queue_head_t mywaitqueue;
init_waitqueue_head(&mywaitqueue);
方法二:
DECLARE_WAIT_QUEUE_HEAD(mywaitqueue);
以上的两种都能实现定义和初始化等待队列头。
 
2、创建、移除一个等待队列的节点,并添加、移除相应的队列。
定义一个等待队列的节点:DECLARE_WAITQUEUE(wait,tsk)
其中tsk表示一个进程,可以采用current当前的进程。
添加到定义好的等待队列头中。
add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);
即:add_wait_queue(&mywaitqueue,&wait);
 
移除等待节点
remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);
即:remove_wait_queue(&mywaitqueue,&wait);
 
3、等待事件
wait_event(queue,condition);当condition为真时,等待队列头queue对应的队列被唤醒,否则继续堵塞。这种情况下不能被信号打断。
wait_event_interruptible(queue,condition);当condition为真时,等待队列头queue对应的队列被唤醒,否则继续堵塞。这种情况下能被信号打断。
4、唤醒等待队列
wait_up(wait_queue_head_t *q),唤醒该等待队列头对应的所有等待。
wait_up_interruptible(wait_queue_head_t *q)唤醒处于TASK_INTERRUPTIBLE的等待进程。
应该成对的使用。即wait_event于wait_up,而wait_event_interruptible与wait_up_interruptible。
  1. wait_event和wait_event_interruptible的实现都是采用宏的方式,都是一个重新调度的过程,如下所示:
  1. #define wait_event_interruptible(wq, condition)                \
  2. ({                                    \
  3. int __ret = 0;                            \
  4. if (!(condition))                        \
  5. __wait_event_interruptible(wq, condition, __ret);    \
  6. __ret;                                \
  7. })
  1. #define __wait_event_interruptible(wq, condition, ret)            \
  2. do {                                    \
  3. /*此处存在一个声明等待队列的语句,因此不需要再重新定义一个等待队列节点*/
  4. DEFINE_WAIT(__wait);                        \
  5. \
  6. for (;;) {                            \
  7. /*此处就相当于add_wait_queue()操作,具体参看代码如下所示*/
  8. prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);    \
  9. if (condition)                        \
  10. break;                        \
  11. if (!signal_pending(current)) {                \
  12. /*此处是调度,丢失CPU,因此需要wake_up函数唤醒当前的进程
  13. 根据定义可知,如果条件不满足,进程就失去CPU,能够跳出for循环的出口只有
  14. 1、当条件满足时2、当signal_pending(current)=1时。
  15. 1、就是满足条件,也就是说wake_up函数只是退出了schedule函数,
  16. 而真正退出函数还需要满足条件
  17. 2、说明进程可以被信号唤醒。也就是信号可能导致没有满足条件时就唤醒当前的进程。
  18. 这也是后面的代码采用while判断的原因.防止被信号唤醒。
  19. */
  20. schedule();                    \
  21. continue;                    \
  22. }                            \
  23. ret = -ERESTARTSYS;                    \
  24. break;                            \
  25. }                                \
  26. finish_wait(&wq, &__wait);                    \
  27. } while (0)
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
#define DEFINE_WAIT_FUNC(name, function) \
wait_queue_t name = { \
.private = current, \
.func = function, \
.task_list = LIST_HEAD_INIT((name).task_list), \
}
 
  1. void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
  2. {
  3. unsigned long flags;
  4. wait->flags &= ~WQ_FLAG_EXCLUSIVE;
  5. spin_lock_irqsave(&q->lock, flags);
  6. if (list_empty(&wait->task_list))
  7. /*添加节点到等待队列*/
  8. __add_wait_queue(q, wait);
  9. set_current_state(state);
  10. spin_unlock_irqrestore(&q->lock, flags);
  11. }
  12. 唤醒的操作也是类似的。
  13. #define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
 
  void __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;
 
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock, flags);
}
 
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
 
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
 
if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
等待队列通常用在驱动程序设计中的堵塞读写操作,并不需要手动的添加节点到队列中,直接调用即可实现,具体的实现方法如下:
1、在设备结构体中添加等待队列头,由于读写都需要堵塞,所以添加两个队列头,分别用来堵塞写操作,写操作。
  1. #include<linux/wait.h>
  2. struct mem_dev
  3. {
  4. char *data;
  5. unsigned long size;
  6. /*添加一个并行机制*/
  7. spinlock_t lock;
  8. /*添加一个等待队列t头*/
  9. wait_queue_head_t rdqueue;
  10. wait_queue_head_t wrqueue;
  11. };
2、然后在模块初始化中初始化队列头:
  1. /*初始化函数*/
  2. static int memdev_init(void)
  3. {
  4. ....
  5. for(i = 0; i < MEMDEV_NR_DEVS; i)
  6. {
  7. mem_devp[i].size = MEMDEV_SIZE;
  8. /*对设备的数据空间分配空间*/
  9. mem_devp[i].data = kmalloc(MEMDEV_SIZE,GFP_KERNEL);
  10. /*问题,没有进行错误的控制*/
  11. memset(mem_devp[i].data,0,MEMDEV_SIZE);
  12. /*初始化定义的互信息量*/
  13. //初始化定义的自旋锁ua
  14. spin_lock_init(&(mem_devp[i].lock));
  15. /*初始化两个等待队列头,需要注意必须用括号包含起来,使得优先级正确*/
  16. init_waitqueue_head(&(mem_devp[i].rdqueue));
  17. init_waitqueue_head(&(mem_devp[i].wrqueue));
  18. }
  19. ...
  20. }
3、确定一个具体的条件,比如数据有无,具体的条件根据实际的情况设计。
/*等待条件*/
static bool havedata = false;
 
4、在需要堵塞的读函数,写函数中分别实现堵塞,首先定义等待队列的节点,并添加到队列中去,然后等待事件的唤醒进程。但是由于读写操作的两个等待队列都是基于条件havedata的,所以在读完成以后需要唤醒写,写完成以后需要唤醒读操作,同时更新条件havedata,最后还要移除添加的等待队列节点。
  1. /*read函数的实现*/
  2. static ssize_t mem_read(struct file *filp,char __user *buf, size_t size,loff_t *ppos)
  3. {
  4. unsigned long p = *ppos;
  5. unsigned int count = size;
  6. int ret = 0;
  7. struct mem_dev *dev = filp->private_data;
  8. /*参数的检查,首先判断文件位置*/
  9. if(p >= MEMDEV_SIZE)
  10. return 0;
  11. /*改正文件大小*/
  12. if(count > MEMDEV_SIZE - p)
  13. count = MEMDEV_SIZE - p;
         #if 0
  1. /*添加一个等待队列节点到当前进程中*/
  2. DECLARE_WAITQUEUE(wait_r,current);
  3. /*将节点添加到等待队列中*/
  4. add_wait_queue(&dev->rdqueue,&wait_r);
  5. /*添加等待队列,本来采用if即可,但是由于信号等可能导致等待队列的唤醒,因此采用循环,确保不会出现误判*/
  6. #endif
  7. while(!havedata)
  8. {
  9. /*判断用户是否设置为非堵塞模式读,告诉用户再读*/
  10. if(filp->f_flags & O_NONBLOCK)
  11. return -EAGAIN;
  12. /*依据条件havedata判断队列的状态,防止进程被信号唤醒*/
  13. wait_event_interruptible(dev->rdqueue,havedata);
  14. }
  15. spin_lock(&dev->lock);
  16. /*从内核读数据到用户空间,实质就通过private_data访问设备*/
  17. if(copy_to_user(buf,(void *)(dev->data p),count))
  18. {
  19. /*出错误*/
  20. ret = -EFAULT;
  21. }
  22. else
  23. {
  24. /*移动当前文件光标的位置*/
  25. *ppos = count;
  26. ret = count;
  27. printk(KERN_INFO "read %d bytes(s) from %d\n",count,p);
  28. }
  29. spin_unlock(&dev->lock);
 #if 0
  1. /*将等待队列节点从读等待队列中移除*/
  2. remove_wait_queue(&dev->rdqueue,&wait_r);
  3. #endif
  4. /*更新条件havedate*/
  5. havedata = false;
  6. /*唤醒写等待队列*/
  7. wake_up_interruptible(&dev->wrqueue);
  8. return ret;
  9. }
 
  1. /*write函数的实现*/
  2. static ssize_t mem_write(struct file *filp,const char __user *buf,size_t size,loff_t *ppos)
  3. {
  4. unsigned long p = *ppos;
  5. unsigned int count = size;
  6. int ret = 0;
  7. /*获得设备结构体的指针*/
  8. struct mem_dev *dev = filp->private_data;
  9. /*检查参数的长度*/
  10. if(p >= MEMDEV_SIZE)
  11. return 0;
  12. if(count > MEMDEV_SIZE - p)
  13. count = MEMDEV_SIZE - p;
  #if 0
  1. /*定义并初始化一个等待队列节点,添加到当前进程中*/
  2. DECLARE_WAITQUEUE(wait_w,current);
  3. /*将等待队列节点添加到等待队列中*/
  4. add_wait_queue(&dev->wrqueue,&wait_w);
  5. #endif
  6. /*添加写堵塞判断*/
  7. /*为何采用循环是为了防止信号等其他原因导致唤醒*/
  8. while(havedata)
  9. {
  10. /*如果是以非堵塞方式*/
  11. if(filp->f_flags & O_NONBLOCK)
  12. return -EAGAIN;
  13. /*分析源码发现,wait_event_interruptible 中存在DECLARE_WAITQUEUE和add_wait_queue的操作,因此不需要手动添加等待队列节点*/
  14. wait_event_interruptible(&dev->wrqueue,(!havedata));
  15. }
  16. spin_lock(&dev->lock);
  17. if(copy_from_user(dev->data p,buf,count))
  18. ret = -EFAULT;
  19. else
  20. {
  21. /*改变文件位置*/
  22. *ppos = count;
  23. ret = count;
  24. printk(KERN_INFO "writted %d bytes(s) from %d\n",count,p);
  25. }
  26. spin_unlock(&dev->lock);
  27. #if 0
  28. /*将该等待节点移除*/
  29. remove_wait_queue(&dev->wrqueue,&wait_w);
  30. #endif
  31. /*更新条件*/
  32. havedata = true;
  33. /*唤醒读等待队列*/
  34. wake_up_interruptible(&dev->rdqueue);
  35. return ret;
  36. }
5、应用程序采用两个不同的进程分别进行读、写,然后检测顺序是否可以调换,检查等待是否正常。

Linux驱动总结3- unlocked_ioctl和堵塞(waitqueue)读写函数的实现 【转】的更多相关文章

  1. Linux代码的重用与强行卸载Linux驱动

    (一)Linux代码的重用 重用=静态重用(将要重用的代码放到其他的文件的头文件中声明)+动态重用(使用另外一个Linux驱动中的资源,例如函数.变量.宏等) 1.编译是由多个文件组成的Linux驱动 ...

  2. Linux 驱动学习笔记05--字符驱动实例,实现一个共享内存设备的驱动

    断断续续学驱动,好不容易有空,做了段字符驱动的例子.主要还是跟书上学习在此记录下来,以后说不定能回过头来温故知新. 首先上驱动源码 gmem.c: /************************* ...

  3. 嵌入式Linux驱动开发日记

    嵌入式Linux驱动开发日记 主机硬件环境 开发机:虚拟机Ubuntu12.04 内存: 1G 硬盘:80GB 目标板硬件环境 CPU: SP5V210 (开发板:QT210) SDRAM: 512M ...

  4. linux驱动初探之杂项设备(控制两个GPIO口)

    关键字:linux驱动.杂项设备.GPIO 此驱动程序控制了外接的两个二极管,二极管是低电平有效. 上一篇博客中已经介绍了linux驱动程序的编写流程,这篇博客算是前一篇的提高篇,也是下一篇博客(JN ...

  5. linux驱动初探之字符驱动

    关键字:字符驱动.动态生成设备节点.helloworld linux驱动编程,个人觉得第一件事就是配置好平台文件,这里以字符设备,也就是传说中的helloworld为例~ 此驱动程序基于linux3. ...

  6. 嵌入式linux驱动开发之点亮led(驱动编程思想之初体验)

    这节我们就开始开始进行实战啦!这里顺便说一下啊,出来做开发的基础很重要啊,基础不好,迟早是要恶补的.个人深刻觉得像这种嵌入式的开发对C语言和微机接口与原理是非常依赖的,必须要有深厚的基础才能hold的 ...

  7. Linux 驱动开发

    linux驱动开发总结(一) 基础性总结 1, linux驱动一般分为3大类: * 字符设备 * 块设备 * 网络设备 2, 开发环境构建: * 交叉工具链构建 * NFS和tftp服务器安装 3, ...

  8. 嵌入式Linux驱动笔记(十八)------浅析V4L2框架之ioctl【转】

    转自:https://blog.csdn.net/Guet_Kite/article/details/78574781 权声明:本文为 风筝 博主原创文章,未经博主允许不得转载!!!!!!谢谢合作 h ...

  9. linux驱动---等待队列、工作队列、Tasklets【转】

    转自:https://blog.csdn.net/ezimu/article/details/54851148 概述: 等待队列.工作队列.Tasklet都是linux驱动很重要的API,下面主要从用 ...

随机推荐

  1. 用ip代替机器名访问sharepoint 站点

    1. aam 里加入一个ip的internet 2. iis里不用加上ip,但不要有host name   出现的问题: 1. 当打开站点里会出现这个错误 file not found 2. 当加授予 ...

  2. POJ 1958 Strange Towers of Hanoi 解题报告

    Strange Towers of Hanoi 大体意思是要求\(n\)盘4的的hanoi tower问题. 总所周知,\(n\)盘3塔有递推公式\(d[i]=dp[i-1]*2+1\) 令\(f[i ...

  3. 使用fiddler模拟http请求

    概述  与httpwath相比,fiddler能模拟http请求.能断点调试.http分析统计吸引了我,使用之后感觉这个工具非常不错,这篇文章只单介绍一下fiddler工作原理,简单介绍一下它的重要功 ...

  4. android sqlite批量插入数据速度解决方案

    转自 http://hi.baidu.com/hfutonline/blog/item/62b1e4de8bdf4b2e5882dd28.html 最近在做android项目的时候遇到一个问题,应用程 ...

  5. centos6.5 开机自动挂载硬盘

    1. 查看硬盘信息 输入命令查询 blkid 查找新添加的硬盘的UUID信息,并且拷贝. 2.编辑系统分区表,加入硬盘自动挂载信息 2.1 打开系统分区表 vim /etc/fstab 进入文件编辑模 ...

  6. EXGCD 扩展欧几里得

    推荐:https://www.zybuluo.com/samzhang/note/541890 扩展欧几里得,就是求出来ax+by=gcd(x,y)的x,y 为什么有解? 根据裴蜀定理,存在u,v使得 ...

  7. A1012. The Best Rank

    To evaluate the performance of our first year CS majored students, we consider their grades of three ...

  8. 基本数据类型对象包装(Integer等)

    基本数据类型 包装类 byte Byte short             Short int   Integer long Long boolean Boolean float          ...

  9. CRC-16的原理和实现

    CRC的全称为Cyclic Redundancy Check,中文名称为循环冗余校验.它是一类重要的线性分组码,编码和解码方法简单,检错和纠错能力强,在通信领域广泛地用于实现差错控制.实际上,除 数据 ...

  10. SQL语句汇总(三)——聚合函数、分组、子查询及组合查询

    拖了一个星期,终于开始写第三篇了.走起! 聚合函数: SQL中提供的聚合函数可以用来统计.求和.求最值等等. 分类: –COUNT:统计行数量 –SUM:获取单个列的合计值 –AVG:计算某个列的平均 ...