![]() |
逆袭的电梯 · 通过 Ocelot 实现 API 网关 | ...· 1 年前 · |
![]() |
深情的香槟 · Android ...· 1 年前 · |
![]() |
曾深爱过的蛋挞 · python - raise ...· 1 年前 · |
![]() |
没有腹肌的鞭炮 · 随笔之如何实现一个线程池_如何自己实现一个一 ...· 2 年前 · |
一、linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序: 1、字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。 2、块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。 每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。 3、字符设备驱动模型 二、字符设备驱动程序基础: 1、主设备号和次设备号(二者一起为设备号): 一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。 linux内核中,设备号用dev_t来描述,2.6.28中定义如下: typedef u_long dev_t; 在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。 可以使用下列宏从dev_t中获得主次设备号: 也可以使用下列宏通过主次设备号生成dev_t: MAJOR(dev_t dev); MKDEV(int major,int minor); MINOR(dev_t dev); //宏定义: #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) * register_chrdev_region() - register a range of device numbers * @from: the first in the desired range of device numbers; must include * the major number. * @count: the number of consecutive device numbers required * @name: the name of the device or driver. * Return value is zero on success, a negative error code on failure. (2)动态分配: int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); * alloc_chrdev_region() - register a range of char device numbers * @dev: output parameter for first assigned number * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: the name of the associated device or driver * Allocates a range of char device numbers. The major number will be * chosen dynamically, and returned (along with the first minor number) * in @dev. Returns zero or a negative error code. 注销设备号: void unregister_chrdev_region(dev_t from, unsigned count); 创建设备文件: 利用cat /proc/devices查看申请到的设备名,设备号。 (1)使用mknod手工创建:mknod filename type major minor (2)自动创建; 利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。 3、字符设备驱动程序重要的数据结构: (1)struct file:代表一个打开的文件描述符,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。 //重要成员: const struct file_operations *f_op; //该操作是定义文件关联的操作的。内核在执行open时对这个指针赋值。 off_t f_pos; //该文件读写位置。 void *private_data;//该成员是系统调用时保存状态信息非常有用的资源。 (2)struct inode:用来记录文件的物理信息。它和代表打开的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构。inode一般作为file_operations结构中函数的参数传递过来。 inode译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ... )被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。 dev_t i_rdev; //对表示设备文件的inode结构,该字段包含了真正的设备编号。 struct cdev *i_cdev; //是表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。 //我们也可以使用下边两个宏从inode中获得主设备号和此设备号: unsigned int iminor(struct inode *inode); unsigned int imajor(struct inode *inode); (3)struct file_operations 本部分来源于:http://blog.chinaunix.net/space.php?uid=20729583&do=blog&id=1884550,感谢chinahhucai的分享。 struct file_operations ***_ops={ .owner = THIS_MODULE, .llseek = ***_llseek, .read = ***_read, .write = ***_write, .ioctl = ***_ioctl, .open = ***_open, .release = ***_release, 。。。 。。。 struct module *owner; /*第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。*/ loff_t (*llseek) (struct file * filp , loff_t p, int orig); /*(指针参数filp为进行读取信息的目标文件结构体指针;参数 p 为文件定位的目标偏移量;参数orig为对文件定位 的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2)) llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).*/ ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p); /*(指针参数 filp 为进行读取信息的目标文件,指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址), 参数size为要读取的信息长度,参数 p 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值) 这个函数用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).*/ ssize_t (*aio_read)(struct kiocb * , char __user * buffer, size_t size , loff_t p); /*可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同 的, 异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。 异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体); 初始化一个异步读 -- 可能在函数返回前不结束的读操作.如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地). (有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/ ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos); /*(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度, ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界) 发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数. (注:这个操作和上面的对文件进行读的操作均为阻塞操作)*/ ssize_t (*aio_write)(struct kiocb *, const char __user * buffer, size_t count, loff_t * ppos); /*初始化设备上的一个异步写.参数类型同aio_read()函数;*/ int (*readdir) (struct file * filp, void *, filldir_t); /*对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.*/ unsigned int (*poll) (struct file *, struct poll_table_struct *); /*(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针) 这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。 每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。 (poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写. (这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果)*/ int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg); /*(inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针. 如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的. 因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.) ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.*/ int (*mmap) (struct file *, struct vm_area_struct *); /*mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV. (如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/ int (*open) (struct inode * inode , struct file * filp ) ; /*(inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构; 但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息) 尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知. 与open()函数对应的是release()函数。*/ int (*flush) (struct file *); /*flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.*/ int (*release) (struct inode *, struct file *); /*release ()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数: void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。 在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.*/ int(*synch)(struct file *,struct dentry *,int datasync); //刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。 int (*aio_fsync)(struct kiocb *, int); /*这是 fsync 方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync 把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。 相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束, 这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘。*/ int (*fasync) (int, struct file *, int); //这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板: static int ***_fasync(int fd,struct file *filp,int mode) struct ***_dev * dev=filp->private_data; return fasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为 fasync_struct结构体指针的指针。 //这个函数是用来处理FASYNC标志的函数。(FASYNC:表示兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执行。 /*此操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知.*/ int (*lock) (struct file *, int, struct file_lock *); //lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它. ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); /*这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).*/ ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *); /*这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL.*/ ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /*sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.*/ unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /*这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.[10]*/ int (*check_flags)(int) //这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志. int (*dir_notify)(struct file *, unsigned long); //这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify. struct kobject kobj;//内嵌的kobject对象 struct module *owner;//所属模块 struct file_operations *ops;//文件操作结构体 struct list_head list; dev_t dev;//设备号,长度为32位,其中高12为主设备号,低20位为此设备号 unsigned int count; 字符设备的注册分为三个步骤: (1)分配cdev: struct cdev *cdev_alloc(void); (2)初始化cdev: void cdev_init(struct cdev *cdev, const struct file_operations *fops); (3)添加cdev: int cdev_add(struct cdev *p, dev_t dev, unsigned count) * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this * device * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. 2.设备操作的实现:file_operations函数集的实现(要明确某个函数什么时候被调用?调用来做什么操作?)特别注意:驱动程序应用程序的数据交换: 驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有: unsigned long copy_to_user(void __user *to, const void *from, unsigned long n); unsigned long copy_from_user(void *to, const void __user *from, unsigned long n); put_user(local,user); get_user(local,user); 3.设备注销:void cdev_del(struct cdev *p); 四、字符设备驱动小结: 字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。 五:字符设备驱动程序分析: (1)memdev.h #ifndef _MEMDEV_H_ #define _MEMDEV_H_ #ifndef MEMDEV_MAJOR #define MEMDEV_MAJOR 251 /*预设的mem的主设备号*/ #endif #ifndef MEMDEV_NR_DEVS #define MEMDEV_NR_DEVS 2 /*设备数*/ #endif #ifndef MEMDEV_SIZE #define MEMDEV_SIZE 4096 #endif /*mem设备描述结构体*/ struct mem_dev char *data; unsigned long size; #endif /* _MEMDEV_H_ */ /*读函数*/ static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) unsigned long p = *ppos; /*记录文件指针偏移位置*/ unsigned int count = size; /*记录需要读取的字节数*/ int ret = 0; /*返回值*/ struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/ /*判断读位置是否有效*/ if (p >= MEMDEV_SIZE) /*要读取的偏移大于设备的内存空间*/ return 0; if (count > MEMDEV_SIZE - p) /*要读取的字节大于设备的内存空间*/ count = MEMDEV_SIZE - p; /*读数据到用户空间:内核空间->用户空间交换数据*/ if (copy_to_user(buf, (void*)(dev->data + p), count)) ret = - EFAULT; *ppos += count; ret = count; printk(KERN_INFO "read %d bytes(s) from %d\n", count, p); return ret; /*写函数*/ static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos) unsigned long p = *ppos; unsigned int count = size; int ret = 0; struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/ /*分析和获取有效的写长度*/ if (p >= MEMDEV_SIZE) return 0; if (count > MEMDEV_SIZE - p) /*要写入的字节大于设备的内存空间*/ count = MEMDEV_SIZE - p; /*从用户空间写入数据*/ if (copy_from_user(dev->data + p, buf, count)) ret = - EFAULT; *ppos += count; /*增加偏移位置*/ ret = count; /*返回实际的写入字节数*/ printk(KERN_INFO "written %d bytes(s) from %d\n", count, p); return ret; /* seek文件定位函数 */ static loff_t mem_llseek(struct file *filp, loff_t offset, int whence) loff_t newpos; switch(whence) { case 0: /* SEEK_SET */ /*相对文件开始位置偏移*/ newpos = offset; /*更新文件指针位置*/ break; case 1: /* SEEK_CUR */ newpos = filp->f_pos + offset; break; case 2: /* SEEK_END */ newpos = MEMDEV_SIZE -1 + offset; break; default: /* can't happen */ return -EINVAL; if ((newpos<0) || (newpos>MEMDEV_SIZE)) return -EINVAL; filp->f_pos = newpos; return newpos; /*文件操作结构体*/ static const struct file_operations mem_fops = .owner = THIS_MODULE, .llseek = mem_llseek, .read = mem_read, .write = mem_write, .open = mem_open, .release = mem_release, /*设备驱动模块加载函数*/ static int memdev_init(void) int result; int i; dev_t devno = MKDEV(mem_major, 0); /* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/ /* 静态申请设备号*/ if (mem_major) result = register_chrdev_region(devno, 2, "memdev"); else /* 动态分配设备号 */ result = alloc_chrdev_region(&devno, 0, 2, "memdev"); mem_major = MAJOR(devno); /*获得申请的主设备号*/ if (result < 0) return result; /*初始化cdev结构,并传递file_operations结构指针*/ cdev_init(&cdev, &mem_fops); cdev.owner = THIS_MODULE; /*指定所属模块*/ cdev.ops = &mem_fops; /* 注册字符设备 */ cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS); /* 为设备描述结构分配内存*/ mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL); if (!mem_devp) /*申请失败*/ result = - ENOMEM; goto fail_malloc; memset(mem_devp, 0, sizeof(struct mem_dev)); /*为设备分配内存*/ for (i=0; i < MEMDEV_NR_DEVS; i++) mem_devp[i].size = MEMDEV_SIZE; mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL); memset(mem_devp[i].data, 0, MEMDEV_SIZE); return 0; fail_malloc: unregister_chrdev_region(devno, 1); return result; /*模块卸载函数*/ static void memdev_exit(void) cdev_del(&cdev); /*注销设备*/ kfree(mem_devp); /*释放设备结构体内存*/ unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/ MODULE_AUTHOR("David Xie"); MODULE_LICENSE("GPL"); module_init(memdev_init); module_exit(memdev_exit); (3)应用程序(测试文件):app-mem.c #include <stdio.h> int main() FILE *fp0 = NULL; char Buf[4096]; /*初始化Buf*/ strcpy(Buf,"Mem is char dev!"); printf("BUF: %s\n",Buf); /*打开设备文件*/ fp0 = fopen("/dev/memdev0","r+"); if (fp0 == NULL) printf("Open Memdev0 Error!\n"); return -1; /*写入设备*/ fwrite(Buf, sizeof(Buf), 1, fp0); /*重新定位文件位置(思考没有该指令,会有何后果)*/ fseek(fp0,0,SEEK_SET); /*清除Buf*/ strcpy(Buf,"Buf is NULL!"); printf("BUF: %s\n",Buf); /*读出设备*/ fread(Buf, sizeof(Buf), 1, fp0); /*检测结果*/ printf("BUF: %s\n",Buf); return 0; 测试步骤: 1)cat /proc/devices看看有哪些编号已经被使用,我们选一个没有使用的XXX。 2)insmod memdev.ko 3)通过"mknod /dev/memdev0 c XXX 0"命令创建"/dev/memdev0"设备节点。 4)交叉编译app-mem.c文件,下载并执行: #./app-mem,显示: Mem is char dev! 代码目录结构 在阅读源码之前,还应知道Linux内核源码的整体分布情况。现代的操作系统一般由进程管理、内存管理、文件系统、驱动程序和网络等组成。Linux内核源码的各个目录大致与此相对应,其组成如下(假设相对于Linux-2.4.23目录): arch目录包括了所有和体系结构相关的核心代码。它下面的每一个子目录都代表一种Linux支持的体系结构,例如i386就是Intel CPU及与之相兼容体系结构的子目录。PC机一般都基于此目录。 include目录包括编译核心所需要的大部分头文件,例如与平台无关的头文件在include/linux子目录下。 init目录包含核心的初始化代码(不是系统的引导代码),有main.c和Version.c两个文件。这是研究核心如何工作的好起点。 mm目录包含了所有的内存管理代码。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下。 drivers目录中是系统中所有的设备驱动程序。它又进一步划分成几类设备驱动,每一种有对应的子目录,如声卡的驱动对应于drivers/sound。 ipc目录包含了核心进程间的通信代码。 modules目录存放了已建好的、可动态加载的模块。 fs目录存放Linux支持的文件系统代码。不同的文件系统有不同的子目录对应,如ext3文件系统对应的就是ext3子目录。 Kernel内核管理的核心代码放在这里。同时与处理器结构相关代码都放在arch/*/kernel目录下。 net目录里是核心的网络部分代码,其每个子目录对应于网络的一个方面。 lib目录包含了核心的库代码,不过与处理器结构相关的库代码被放在arch/*/lib/目录下。 scripts目录包含用于配置核心的脚本文件。 documentation目录下是一些文档,是对每个目录作用的具体说明。 一般在每个目录下都有一个Makefile文件。这两个文件都是编译时使用的辅助文件。仔细阅读这两个文件对弄清各个文件之间的联系和依托关系很有帮助。另外有的目录下还有Readme文件,它是对该目录下文件的一些说明,同样有利于对内核源码的理解。
搞了一天了,编译的时候,总是出现 fatal: Unable to find remote helper for 'https'这样的错误。今天终于解决了。 今天知道问题的原因了,是因为/usr/local/libexec/git-core路径没在 PATH 环境变量中。 deng@cecport:/usr/local/libexec/git-core$ pwd /usr/local/libexec/git-core deng@cecport:/usr/local/libexec/git-core$ ls git git-count-objects git-gui git-merge-subtree git-remote git-sh-setup git-add git-credential git-gui--askpass git-mergetool git-remote-ext git-stage git-add--interactive git-credential-cache git-hash-object git-mergetool--lib git-remote-fd git-stash git-am git-credential-cache--daemon git-help git-merge-tree git-remote-ftp git-status git-annotate git-credential-store git-http-backend git-mktag git-remote-ftps git-stripspace git-apply git-cvsexportcommit git-http-fetch git-mktree git-remote-http git-submodule git-archimport git-cvsimport git-http-push git-mv git-remote-https git-submodule--helper git-archive git-cvsserver git-imap-send git-name-rev git-remote-testpy git-svn git-bisect git-daemon git-index-pack git-notes git-remote-testsvn git-symbolic-ref git-bisect--helper git-describe git-init git-p4 git-repack git-tag git-blame git-diff git-init-db git-pack-objects git-replace git-tar-tree git-branch git-diff-files git-instaweb git-pack-redundant git-repo-config git-unpack-file git-bundle git-diff-index git-interpret-trailers git-pack-refs git-request-pull git-unpack-objects git-cat-file git-difftool git-log git-parse-remote git-rerere git-update-index git-check-attr git-difftool--helper git-lost-found git-patch-id git-reset git-update-ref git-check-ignore git-diff-tree git-ls-files git-peek-remote git-revert git-update-server-info git-check-mailmap git-fast-export git-ls-remote git-prune git-rev-list git-upload-archive git-checkout git-fast-import git-ls-tree git-prune-packed git-rev-parse git-upload-pack git-checkout-index git-fetch git-mailinfo git-pull git-rm git-var git-check-ref-format git-fetch-pack git-mailsplit git-push git-send-email git-verify-commit git-cherry git-filter-branch git-merge git-quiltimport git-send-pack git-verify-pack git-cherry-pick git-fmt-merge-msg git-merge-base git-read-tree git-shell git-verify-tag git-citool git-for-each-ref git-merge-file git-rebase git-sh-i18n git-web--browse git-clean git-format-patch git-merge-index git-rebase--am git-sh-i18n--envsubst git-whatchanged git-clone git-fsck git-merge-octopus git-rebase--interactive git-shortlog git-worktree git-column git-fsck-objects git-merge-one-file git-rebase--merge git-show git-write-tree git-commit git-gc git-merge-ours git-receive-pack git-show-branch mergetools git-commit-tree git-get-tar-commit-id git-merge-recursive git-reflog git-show-index git-config git-grep git-merge-resolve git-relink git-show-ref deng@cecport:/usr/local/libexec/git-core$
更多资料,请参考:中电港论坛 http://bbs.cecport.com/forum.php?mod=forumdisplay&fid=64&page=1 感谢中电港,低价QCA4010开发板入手,终于腾出时间来研究一下这个高大上的开发板 4010与4004一样,内部集成了Tensilica Xtensa处理器,主频可达130MHz。Xtensa处理器是Tensilica公司推出的一个可自由装组、可弹性扩张,并可以自动合成的处理器核心,它的指令集构架 (ISA) 拥有专利权,32位处理器的结构特色是有一套专门为嵌入式系统设计、精简而高效能的16与24位指令集。如要更进一步了解 Xtensa 处理器的功能,请访问该公司的官网。开发板提供的资料不多,仅有的一些资料也是分散得很,得自己爬论坛一个一个的找。经过几天的摸索,终于把开发环境搭建好了。开发环境的搭建包括两个方面的内容,第一是ART工具的使用环境准备,第二是编译环境的准备。ART工具的全称是Qualcomm Atheros Radio Tool 2 for Internet of Everything的简称,实际上这就是个测试与烧录固件的程序。当然这个工具还有其它的一些功能, 如读取及修改硬件的一些工作参数。ART只能在WINDOWS下执行,而且似乎只能在WIN7下正常工作。不过WIN10良好的兼容性,使得我们也可以在WIN10下工作,下面会介绍具体的使用方法。编译用的工具链则是基于LINUX系统的,所以还得准备一个LINUX系统。我的建议是在WINDOWS下安装一个LINUX虚拟机,这样二者就兼顾了。本文在WINDOWS 10下,使用VBOX搭建了一个UBUNTU来配合使用。一、 ART工具准备先从这里下载ART工具的最新版,http://bbs.cecport.com/forum.php?mod=viewthread&tid=299&extra=page%3D1。解压缩,得到如下目录结构├─bin├─command│ ├─common│ │ ├─calibrationMemory│ │ └─summary│ ├─refDesigns│ │ ├─AR6004│ │ ├─DV144│ │ ├─SP141│ │ ├─SP143│ │ ├─SP144│ │ ├─SP241│ │ └─SP242│ ├─test_bt│ ├─test_rx│ │ └─rate│ ├─test_setup│ │ └─regression│ └─test_tx│ └─Rate├─driver│ ├─boardData│ ├─Firmware│ │ └─AR6006│ ├─win7│ │ ├─USB│ │ └─USB_64bit│ │ └─SP242│ └─winxp│ ├─SDIO│ └─USB├─log├─report├─support│ └─EepromUtil└─tcmd ├─ath6kl_driver └─athtestcmd要注意的是BIN和DRIVER文件夹,BIN包括一些执行的程序,DRIVER是驱动程序。别的东西暂时不用管,用到的时候再来研究。先将开发板上HOST0的跳线跳到如图所示位置 然后用USB线连接到PC机,此时,系统会发现硬件,但是驱动不会成功,因为这个驱动不是通用的,WIN10是找不到的,注意上面的ART工具目录下有个DRIVER文件夹,不过只有WIN7和WINXP的驱动。没关系,WIN7的驱动WIN10也是可以用的。不过在安装驱动之前,先要将WIN10的硬件驱动签名禁用。怎么禁用WIN10的硬件驱动签名,请自行GOOGLE。重启系统且禁用硬件驱动签名后,打开设备管理器,安装设备驱动程序。出现下面的画面,就表示驱动成功了! 下面来测试ART工具,试着烧写一个固件文件。打开BIN目录下,依次找到下面三个文件, 依次在各文件上右击,更改程序的兼容性设置,如下图所示 都设置好之后,双击artgui.exe,启动。出现如下画面 点击LOAD CARD,会发现加载成功。如下图 下载测试用的固件文件, raw_flashimage_AR401X_REV6_IOT_hostless_unidev_dualband.zip (179.72 KB, 下载次数: 0) 将下载好的测试用固件解压,放到BIN目录下打开ARTGUI菜单TOOLS/SEND CMD命令,在弹出的对话框中输入"xp file=raw_flashimage_AR401X_REV6_IOT_hostless_unidev_dualband.bin"。点击OK,开始烧写。烧写成功后,会出现如下信息, 如果连接了串口,终端会出现类似信息。注意先要将HOST0的跳线复位,然后可能需要复位开发板。PUTTY串口设置能让信息如下 第一步工作就算完成了。二、LINUX编译环境需要注意的官方的工具链是32位的,在UBUNTU14 X64下是没有办法运行的,不得已,再装个UBUNTU14 X86 LTS版。1. 将工具包 cad_kf_RD2012_4.tar.gz 解压在Linux系统根目录cd /tar xvzf cad_kf_RD2012_4.tar.gz 注意:需解压至根目录,因为脚本里使用的绝对路径2. 单机版license安装将 license.dat 文件拷贝至/cad/tensilica目录下。编译代码时需使用license,与电脑MAC地址绑定3. 解压SDKtar xvzf qca4010-tx-1-0_qca_odm_ext.tar.gz得到qca4010-tx-1-0_qca_odm_ext.git目录修改sdkenv.sh脚本,添加工具链定义: XTENSA_CORE=KF1_prod_rel_2012_4 XTENSA_TOOLS_ROOT=/cad/tensilica/xtensa/XtDevTools/install/tools/RD-2012.4-linux/XtensaTools XTENSA_ROOT=/cad/tensilica/chips/kingfisher/RD-2012.4-linux/${XTENSA_CORE} XTENSA_SYSTEM=${XTENSA_ROOT}/config LM_LICENSE_FILE=/cad/tensilica/license.dat PATH=${PATH}:${XTENSA_TOOLS_ROOT}/bin export LM_LICENSE_FILE XTENSA_TOOLS_ROOT XTENSA_ROOT XTENSA_SYSTEM XTENSA_CORE PATH export XTENSA_PREFER_LICENSE=XT-GENERIC 4. 编译demosource sdkenv.sh; make -C demo/sdk_shell注意:需修改MAC地址为: 00:0c:29:01:02:03,如 ifconfig eth0 hw ether 00:0c:29:01:02:035. 复制配置文件,并用qonstruct工具生成可烧录bin文件cp ./tool/tunable/tunable_input_sp24X_hostless_4bitflash.txt ./tool/tunable/tunable_input.txt./tool/qonstruct.sh --qons ./tool/tunable/在bin目录下生成烧录用bin文件:raw_flashimage_AR401X_REV6_IOT_hostless_unidev_dualband.bin呼,大功告成!上一张编译成功后的图片。 现在可以将这个BIN文件用上面介绍的方法刷入到QCA4010中去了。总结几点,要想在WIN10下成功使用开发板,注意以下几点:1. 安装WINDOWS驱动时,先禁用驱动签名,然后再安装驱动2. 将几个重要的文件兼容性模式指定为WINDOWS 73. 最好在UBUNTU X86下编译固件接下来才是正式体验QCA4010的强大时候!
这篇文章主要对BootLoader(UBoot)的源码进行了分析,并对UBoot的移植略作提及。 BootLoader的总目标是正确调用内核的执行,由于大部分的BoorLoader都依赖于CPU的体系结构。因此大部分的BootLoader都分为两个步骤启动。依赖于CPU体系结构(如设备初始化等)的代码都放在stage1。而stage2一般使用C语言实现,能够实现更加复杂的功能,代码的可移植性也提高。 二.本文提纲 1. 摘要 2. 本文提纲 3. UBoot启动过程 4. Stage1(汇编语言实现)代码分析 5. Stage2(C语言实现)代码分析 6. UBoot移植过程中串口没有显示或者显示乱码的原因 7. 总结 三.UBoot启动过程 UBoot其启动过程主要可以分为两个部分,Stage1和Stage2 。其中Stage1是用汇编语言实现的,主要完成硬件资源的初始化。而Stage2则是用C语言实现。主要完成内核程序的调用。这两个部分的主要执行流程如下: stage1包含以下步骤: 1. 硬件设备初始化 2. 为加载stage2准备RAM空间 3. 拷贝stage2的代码到RAM空间 4. 设置好堆栈 5. 跳转到stage2的C语言入口点 stage2一般包括以下步骤: 1. 初始化本阶段要使用的硬件设备 2. 检测系统内存映射 3. 将kernel映射和根文件系统映射从Flash读到RAM空间中 4. 为内核设置启动参数 5. 调用内核 四. Stage1(汇编语言实现)代码分析 该阶段主要是在cpu/arm920t/start.S文件中执行,这个汇编程序是U-Boot的入口程序,程序的开头就是复位向量的代码,主要的执行流程见下图。 U-Boot启动代码流程图 start.S代码分析: (1)主要实现复位向量,设置异常向量表。 _start: b reset //复位向量 ;;设置异常向量表 ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq //中断向量 ldr pc, _fiq //中断向量 (2)复位启动子程序,将CPU设置到SVC模式 /* the actual reset code */ reset: //复位启动子程序 /* 设置CPU为SVC32模式 */ mrs r0,cpsr bic r0,r0,#0x1f ;;位清除,将某些位的值置0:r0 = r0 AND ( !0x1f) orr r0,r0,#0xd3 ;;逻辑或,将r0与立即数进行逻辑或,放在r0中(第一个) msr cpsr,r0 #if defined(CONFIG_S3C2400) # define pWTCON 0x15300000 # define INTMSK 0x14400008 /* Interupt-Controller base addresses */ # define CLKDIVN 0x14800014 /* clock divisor register */ #elif defined(CONFIG_S3C2410) # define pWTCON 0x53000000 # define INTMSK 0x4A000008 /* Interupt-Controller base addresses */ # define INTSUBMSK 0x4A00001C # define CLKDIVN 0x4C000014 /* clock divisor register */ #endif #if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410) ldr r0, =pWTCON mov r1, #0x0 str r1, [r0] (4)禁止所有中断,设置CPU频率 /* 禁止所有中断和设置CPU频率 */ * mask all IRQs by setting all bits in the INTMR - default mov r1, #0xffffffff ldr r0, =INTMSK str r1, [r0] # if defined(CONFIG_S3C2410) ldr r1, =0x3ff ldr r0, =INTSUBMSK str r1, [r0] # endif /* FCLK:HCLK:PCLK = 1:2:4 */ ;;FCLK用于CPU,HCLK用于AHB,PCLK用于APB /* default FCLK is 120 MHz ! */ ldr r0, =CLKDIVN ;;根据硬件手册来设置CLKDIVN寄存器 mov r1, #3 ;;用户手册的推荐值 str r1, [r0] #endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */ mov r0, #0 mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */ ;;使I/D cache失效:将寄存器r0的数据传送到协处理器p15的c7中。C7寄存器位对应cp15中的cache控制寄存器 mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */ ;;使TLB操作寄存器失效:将r0数据送到cp15的c8、c7中。C8对应TLB操作寄存器 * disable MMU stuff and caches 禁止MMU和caches mrc p15, 0, r0, c1, c0, 0 ;;先把c1和c0寄存器的各位置0(r0 = 0) bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS) bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM) ;;这里我本来有个疑问:为什么要分开设置。因为arm汇编要求的立即数格式所决定的 orr r0, r0, #0x00000002 @ set bit 2(??) (A) Align ;;上一条已经设置bit1为0,这一条又设置为1?? orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache mcr p15, 0, r0, c1, c0, 0 ;;用上面(见下面)设定的r0的值设置c1??(cache类型寄存器)和c0(control字寄存器),以下为c0的位定义 ;;bit8: 0 = Disable System protection ;;bit9: 0 = Disable ROM protection ;;bit0: 0 = MMU disabled ;;bit1: 0 = Fault checking disabled 禁止纠错 ;;bit2: 0 = Data cache disabled ;;bit7: 0 = Little-endian operation ;;bit12: 1 = Instruction cache enabled /* 配置内存区控制寄存器 ??有待分析,是1.1.4版本的 * before relocating, we have to setup RAM timing * because memory timing is board-dependend, you will * find a lowlevel_init.S in your board directory. mov ip, lr bl lowlevel_init ;;位于board/smdk2410/lowlevel_init.S:用于完成芯片存储器的初始化,执行完成后返回 mov lr, ip mov pc, lr relocate: adr r0, _start /* r0是代码的当前位置 */ ;;adr伪指令,汇编器自动通过当前PC的值算出 如果执行到_start时PC的值,放到r0中: 当此段在flash中执行时r0 = _start = 0;当此段在RAM中执行时_start = _TEXT_BASE(在board/smdk2410/config.mk中指定的值为0x33F80000,即u-boot在把代码拷贝到RAM中去执行的代码段的开始) ldr r1, _TEXT_BASE /* 测试判断是从Flash启动,还是RAM */ ;;此句执行的结果r1始终是0x33FF80000,因为此值是又编译器指定的(ads中设置,或-D设置编译器参数) cmp r0, r1 /* 比较r0和r1,调试的时候不要执行重定位 */ beq stack_setup /* 如果r0等于r1,跳过重定位代码 */ /* 准备重新定位代码 */ ;;以上确定了复位启动代码是在flash中执行的(是系统重启,而不是软复位),就需要把代码拷贝到RAM中去执行,以下为计算即将拷贝的代码的长度 ldr r2, _armboot_start ;;前面定义了,就是_start ldr r3, _bss_start ;;所谓bss段,就是未被初始化的静态变量存放的地方,这个地址是如何的出来的?根据board/smsk2410/u-boot.lds内容? sub r2, r3, r2 /* r2 得到armboot的大小 */ add r2, r0, r2 /* r2 得到要复制代码的末尾地址 */ (7)重新定位代码,循环拷贝启动的代码到RAM中 copy_loop: ldmia {r3-r10} /*从源地址[r0]复制 */ ;;r0指向_start(=0) stmia {r3-r10} /* 复制到目的地址[r1] */ ;;r1指向_TEXT_BASE(=0x33F80000) cmp r0, r2 /* 复制数据块直到源数据末尾地址[r2] */ ble copy_loop (8)初始化堆栈等 stack_setup: ldr r0, _TEXT_BASE /* 上面是128 KiB重定位的u-boot */ sub r0, r0, #CFG_MALLOC_LEN /* 向下是内存分配空间 */ sub r0, r0, #CFG_GBL_DATA_SIZE /* 然后是bdinfo结构体地址空间 */ #ifdef CONFIG_USE_IRQ sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) #endif ;;这些宏定义在/include/configs/smdk2410.h中: #define CFG_MALLOC_LEN (CFG_ENV_SIZE + 128*1024) ;;64K+128K=0xC0 #define CFG_ENV_SIZE 0x10000 /* Total Size of Environment Sector 64k*/ #define CONFIG_STACKSIZE (128*1024) /* regular stack 128k */ #define CFG_GBL_DATA_SIZE 128 /* size in bytes reserved for initial data */ 用0x33F8000 – 0xC0 – 0x80得到_TEXT_BASE向下(低地址)的堆栈指针sp的起点地址 sub sp, r0, #12 /* 为abort-stack预留3个字 */ ;;得到最终sp指针初始值 clear_bss: ldr r0, _bss_start /* 找到bss段起始地址 */ ldr r1, _bss_end /* bss段末尾地址 */ mov r2, #0x00000000 /* 清零 */ clbss_l:str r2, [r0] /* bss段地址空间清零循环... */ add r0, r0, #4 cmp r0, r1 bne clbss_l (9)跳转到start_armboot函数入口,_start_armboot字保存函数的入口指针 ldr pc, _start_armboot _start_armboot: .word start_armboot ;;start_armboot函数在lib_arm/board.c中实现 五. Stage2(C语言实现)代码分析 这个文件是bootloader的stage2部分,这个文件中的start_armboot函数是U-Boot执行的第一个C语言函数,主要完成系统的初始化工作,然后进入主循环,等待并处理用户输入的命令。 在编译和链接BootLoader这样的程序的时候,不能使用glibc库中的任何支持函数,这就带来了一个问题:从何处跳入Main函数,最直接的想法是直接把Main函数的起始地址作为整个Stage2执行映像的入口。但是这样做有两个缺点: a: 无法通过Main函数传递参数 b: 无法处理Main函数返回的情况 一种更好的解决方案是利用trampoline(弹簧床)的概念:用汇编写一段trampoline小程序,并将这段trampoline小程序作为Stage2可执行映像的入口点,然后就可以在trampoline小程序中用CPU跳转指令跳入Main函数去执行,当Main函数执行结束以后CPU执行路径显然再次回到trampoline程序。其核心思想就是用这段trampoline程序作为Main函数的外部包裹。 (1). 初始化本阶段要使用到的硬件设备,一般包括: a:点亮LED,表示已经进入main函数执行(可选) b: 至少一个串口,以便和终端用户进行IO信息交换 c: 初始化定时器等 d: 输出一些打印信息,程序名称,版本号等 (2). 检测系统的内存映射 所谓内存映射就是指在整个 4GB 物理地址空间中有哪些地址范围被分配用来寻址系统的 RAM 单元。比如,在 SA-1100 CPU 中,从 0xC000,0000 开始的 512M 地址空间被用作系统的 RAM 地址空间,而在 Samsung S3C44B0X CPU 中,从 0x0c00,0000 到 0x1000,0000 之间的 64M 地址空间被用作系统的 RAM 地址空间。虽然 CPU 通常预留出一大段足够的地址空间给系统 RAM,但是在搭建具体的嵌入式系统时却不一定会实现 CPU 预留的全部 RAM 地址空间。也就是说,具体的嵌入式系统往往只把 CPU 预留的全部 RAM 地址空间中的一部分映射到 RAM 单元上,而让剩下的那部分预留 RAM 地址空间处于未使用状态。 由于上述这个事实,因此 Boot Loader 的 stage2 必须在它想干点什么 (比如,将存储在 flash 上的内核映像读到 RAM 空间中) 之前检测整个系统的内存映射情况,也即它必须知道 CPU 预留的全部 RAM 地址空间中的哪些被真正映射到 RAM 地址单元,哪些是处于 "unused" 状态的。 (3). 加载内核映像和根文件系统映像 a:规划内存占用的布局:主要包括基地址和映像大小两个方面。对于内核映像,一般将其拷贝到从(MEM_START+0x8000) 这个基地址开始的大约 1MB大小的内存范围内(嵌入式 Linux 的内核一般都不操过 1MB)。为什么要把从 MEM_START到MEM_START+0x 8000 这段 32KB 大小的内存空出 来呢?这是因为 Linux 内核要在这段内存中放置一些全局数据结构,如:启动参数和内核页表等信息。而对于根文件系统映像,则一般将其拷贝到 MEM_START+0x0010,0000 开始的地方。如果用 Ramdisk 作为根文件系统映像,则其解压后的大小一般是 1MB。 b:从Flash中拷贝映像 while(count) { *dest++ = *src++; /* they are all aligned with word boundary */ count -= 4; /* byte number */ (4). 设置内核的启动参数 将内核映像拷贝到RAM中之后就可以启动了,但是一般都需要先设定Linux内核的启动参数。Linux2.4以后的内核都以标记列表(tagged list)的形式来传递启动参数。启动参数列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。每个标记由标示被传递参数的tag_header结构以及随后的参数数据结构来组成。数据结构tag和tag_header定义在Linux内核源码的include/asm/setup.h头文件中。在嵌入式Linux系统中,通常需要由BootLoader设定的参数有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD。 比如,设置 ATAG_CORE 的代码如下: params = (struct tag *)BOOT_PARAMS; params->hdr.tag = ATAG_CORE; params->hdr.size = tag_size(tag_core); params->u.core.flags = 0; params->u.core.pagesize = 0; params->u.core.rootdev = 0; params = tag_next(params); 其中,BOOT_PARAMS 表示内核启动参数在内存中的起始基地址,指针 params 是一个 struct tag 类型的指针。宏 tag_next() 将以指向当前标记的指针为参数,计算紧临当前标记的下一个标记的起始地址。注意,内核的根文件系统所在的设备 ID 就是在这里设置的。 (5). 调用内核 BootLoader调用内核的方法是直接跳转到内核的第一条指令处,即直接跳到MEM_START+0x8000处。在跳转的时候要满足下面的条件: a: CPU寄存器的设置 R0 = 0; R1 = 机器类型ID, b: CPU必须在SVC模式 c: Cache和MMU的设置: MMU必须关闭 指令Cache可以打开也可以关闭 数据Cache必须关闭 说明:如果用 C 语言,可以像下列示例代码这样来调用内核: void (*theKernel)(int zero, int arch, u32 params_addr) = (void (*)(int, int, u32))KERNEL_RAM_BASE; theKernel(0, ARCH_NUMBER, (u32) kernel_params_start); (1). boot loader 对串口的初始化设置不正确。 (2). 运行在 host 端的终端仿真程序对串口的设置不正确, 这包括:波特率、奇偶校验、数据位和停止位等方面的设置。 关于BootLoader启动时串口能输出,但是启动内核后不能正确显示的原因: (1). 内核编译时缺少配置对串口驱动的支持,或配置正确的串口驱动 (2). BootLoader的串口配置和内核的不一致 (3). 内核没有正确启动 U-Boot,全称 Universal Boot Loader,是遵循GPL条款的开放源码项目。从FADSROM、8xxROM、PPCBOOT逐步发展演化而来。其源码目录、编译形式与Linux内核很相似,事实上,不少U-Boot源码就是相应的Linux内核源程序的简化,尤其是一些设备的驱动程序,这从U-Boot源码的注释中能体现这一点。 参考网址:http://www.cnblogs.com/tianyou/archive/2013/03/23/2977781.html
一. 摘要 这篇文章主要介绍了Linux内核模块的相关概念,以及简单的模块开发过程。主要从模块开发中的常用指令、内核模块程序的结构、模块使用计数以及模块的编译等角度对内核模块进行介绍。在Linux系统开发过程中,以模块的形式开发其重要性不言自明,而在嵌入式设备驱动开发中将驱动程序以模块的形式发布,更是极大地提高了设备使用的灵活性——用户只需要拿到相关驱动模块,再插入到用户的内核中,即可灵活地使用你的设备。 二. 文章提纲 1. 摘要 2. 文章提纲 3. 概述 4. 模块开发常用的指令 5. 内核模块程序结构 6. 模块使用计数 7. 模块的编译 8. 使用模块绕开GPL 9. 总结 Linux内核整体结构已经很庞大,包含了很多的组件,而对于我们工程师而言,有两种方法将需要的功能包含进内核当中。 一:将所有的功能都编译进Linux内核。 二:将需要的功能编译成模块,在需要的时候动态地添加。 上述两种方式优缺点分析: 优点:不会有版本不兼容的问题,不需要进行严格的版本检查 缺点:生成的内核会很大;要在现有的内核中添加新的功能,则要编译整个内核 优点:模块本身不编译进内核,从而控制了内核的大小;模块一旦被加载,将和其它的部分完全一样。 缺点:可能会有内核与模块版本不兼容的问题,导致内核崩溃;会造成内存的利用率比较低。 四.模块开发常用的指令 在内核模块开发的过程中常用的有以下指令。 1) insmod: 将模块插入内核中,使用方法:#insmod XXX.ko 2) rmmod: 将模块从内核中删除,使用方法:#rmmod XXX.ko 3) lsmod: 列表显示所有的内核模块,可以和grep指令结合使用。使用方法:#lsmod | grep XXX 4) modprobe: modprobe可载入指定的个别模块,或是载入一组相依赖的模块。modprobe会根据depmod所产生的依赖关系,决定要载入哪些模块。若在载入过程中发生错误,在modprobe会卸载整组的模块。依赖关系是通过读取 /lib/modules/2.6.xx/modules.dep得到的。而该文件是通过depmod 所建立。 5) modinfo: 查看模块信息。使用方法:#modinfo XXX.ko 6) tree –a: 查看当前目录的整个树结构。使用方法:#tree -a 五.内核模块程序结构 1) 模块加载函数(一般需要) 在用insmod或modprobe命令加载模块时,该函数被执行。完成模块的初始化工作。 Linux内核的模块加载函数一般用__init标识声明,模块加载函数必须以module_init(函数名)的形式被指定。该函数返回整型值,如果执行成功,则返回0,初始化失败时则返回错误编码,Linux内核当中的错误编码是负值,在<linux/errno.h>中定义。 在Linux中,标识__init的函数在连接时放在.init.text这个区段,而且在.initcall.init中保留一份函数指针,初始化的时候内核会根据这些指针调用初始化函数,初始化结束后释放这些init区段(包括前两者)。 代码清单: 1 static int __init XXX_init(void) 5 return 0; 10 moudle_init(XXX_init); 若模块加载函数动态分配了内存,则模块卸载函数释放这些内存 若模块加载函数申请了硬件资源,则模块卸载函数释放这些硬件资源 若模块加载函数开启了硬件资源,则模块卸载函数一定要关闭这些资源 代码清单: 1 static void __exit XXX_exit(void) 9 moudle_exit(XXX_exit); 17 printk(KERN_INFO "Book name is %s\n", bookName); 19 printk(KERN_INFO "Book number is %d\n", bookNumber); 21 return 0; 27 static void __exit book_exit(void) 31 printk(KERN_INFO "Book module exit.\n"); 37 module_init(book_init); 39 module_exit(book_exit); 41 module_param(bookName, charp, S_IRUGO); 43 module_param(bookNumber, int, S_IRUGO); 47 MODULE_LICENSE("GPL"); 使用模块导出符号,方便其它模块依赖于该模块,并使用模块中的变量和函数等。 在Linux2.6的内核中,/proc/kallsyms文件对应着符号表,它记录了符号和符号对应的内存地址。对于模块而言,使用下面的宏可以导出符号。 1 EXPORT_SYMBOL(符号名); 1 EXPORT_GPL_SYMBOL(符号名); 如果功能不编译成模块,则无法绕开GPL,编译成模块后公司发布产品则只需要发布模块,而不需要发布源码。为了Linux系统能够支持模块,需要做以下的工作: 内核编译时选择“可以加载模块”,嵌入式产品一般都不需要卸载模块,则可以不选择“可卸载模块” 将我们的ko文件放在文件系统中 Linux系统实现了insmod、rmmod等工具 使用时可以用insmod手动加载模块,也可以修改/etc/init.d/rcS文件,从而在系统启动的时候就加载模块。 学习内核,避免不了要和模块打交道,而模块的编译一直都是用以前写好的makefile文件,没有研究过模块是如何编译的,后来在内核里面找到了一份文档,才发现模块的编译的方式很“丰富” linux/Document/kbuild/module.txt文件中有详细的讲解。 ---------------------------------------------------------------------------------------一,一般的模块的编译。 我喜欢用的一种格式如下: obj-m := find_mod.o kernel_path=/home/linux/kernel/linux-3.5 2,模块所在的位置,一般都是当前目录,PWD变量指定。 3,产生的模块的目标文件。这文件名要和模块.c文件名一致。 ------------------------------------------------------------------------------------------二,多个C文件的模块编译。 在用户态编程时,很多时候都是多个文件编译,这么多文件组成一个项目。 这么多文件可以由makefile组织到一起,同样模块中也可以编译多个文件。 而且实现起来也特别简单。 obj-m := hello.o hello-y := hello1.o hello2.o kernel_path=/home/linux/kernel/linux-3.5
一. 选择Openwrt平台的理由 传统的路由器固件是由官方提供的。这些固件是不开源的,而且这些路由器的功能也比较单一,很难满足日益变化的智能家居需求。因此,在构建物联网核心时,需要考虑第三方固件平台。 Openwrt、DD-Wrt以及Tomato是三个最为著名的第三方路由器固件平台。当然还有其他一些更加小众的版本,和很多从这三大固件衍生出来的修改版固件,在此我就不赘述了。 选择Openwrt而不选择DD-Wrt或者Tomato的理由如下: 1. DD-WRT:是三个固件平台中第三方软件支持最为丰富一个平台。 对于新的路由器的支持也是非常迅速的。然而,正是由于各种丰富的软件或工具的加入,导致DD-WRT的系统不稳定,经常会出现Bug,使得用户体验不友好。往往在一个版本中修改过的Bug,在下一次版本check的时候,又会出现,对于开发者来说比较头疼。 此外,DD-WRT对于无线信号处理方面能力较弱,Qos功能更是被许多玩家吐槽。(由于智能家居需要多设备同时接入,并且不同设备之间会存在流量的冲突,因此Qos显得比较重要,基于该原因) 2. Tomato:官方的Tomato固件是三个平台中最为稳定的。 Tomato较为封闭,对于新的路由器支持力度不够,特别是对于Atheros主控支持力度很小。于是, 我果断放弃了Tomato。 3. Openwrt:三大固件中扩展性最好的。 对于版本的控制较为严谨,通常以往出现的bug在新的版本中极少出现,在稳定性上较之DD-WRT有了很大的提升。 Openwrt对于Qos的功能做了优化,其性能可以媲美Tomato。 Openwrt是基于Linux的,适合开发者进行二次开发。(此外,Openwrt的固件有许多功能是远超过路由器本身的功能的,例如samba局域网文件共享,transmission脱机下载,ushare,uPnP等),这正是我想要的!! 当然,Openwrt本身的web,luci界面是比较丑的,系统设置也没有DD-WRT方便,易用性不是很好。(这个问题嘛,可以通过第三方软件进行改进,对于我来说就不是问题了) 所谓的平台搭建,不仅包括固件的烧写,还包括开发,编译和调试的环境搭建。 Openwrt平台是基于Linux的,其支持虚拟机安装,对于手边没有设备的亲们,可以通过安装虚拟机感受一下(推荐用最新的稳定的ubuntu)。 2. Openwrt开发环境搭建流程(以Atheros 9344为例): (1)操作系统: Ubuntu12.04 Server。(公司服务器)。 (2)在Ubuntu下搭建Openwrt开发环境: 安装依赖库: 我使用的是attitude_adjustment版本,这个版本较为稳定,且 进行源码版本更新: update all feeds, re-create index files, install symlinks cd attitude_adjustment/ svn up ./scripts/feeds update -a ./scripts/feeds install -a 到目前为止,Openwrt的开发环境已经搭好了. (3)Openwrt系统的首次编译(p.s. 在编译过程中,程序会自动通过feed机制,在网上下载相应的依赖文件,这要求编译者所在网络环境良好!): 进行环境检查,查看编译所需依赖库是否都安装: make defconfig 若提示有某个依赖库没有安装,请按照提示按照对应的依赖库. 直到上述检查无返回。 进行编译配置: make menuconfig 对目标固件进行配置. 由于首次编译时间会较长,因此我创建一个无外加软件的固件:(下面三幅图,分别对应于Target System,Subtarget和Target Profile) openwrt-ar71xx-generic-db120-kernel.bin:对应于只烧写内核固件 openwrt-ar71xx-generic-db120-rootfs-squashfs.bin:对应于文件系统固件 openwrt-ar71xx-generic-db120-squashfs-sysupgrade.bin:对应于完整的固件 至此,一个可以烧写的固件就编译好了。当然,可以看出这个系统只能将路由器启动,能够正常加电运转,但是其没有任何功能。(在后面,我们需要对其添加各种软件支持,甚至是通过编译内核的方式添加软件应用) (4) 固件烧写 对于固件烧写呢,有多种方式,我仅将我所使用过的方式列出来: ++Web在线固件升级,这个一般适用于原厂固件升级,或者Openwrt镜像烧写。此方法难度低,如果固件没有问题并且少些过程中没有断电的话,都能成功烧写。 ++tftp方式升级,本人使用的Atheros9344路由,机身自带有console口,可以通过网线直连的方式,直接通过PC进行烧写。 此过程难度较高,需要有一定的开发基础。 此外,还有好多方法,在网上都能找到,由于暂时不需要,我就不一一列出了。 本人用tftp烧写固件的: 方法如下: $ tftp 0x80060000 openwrt-ar71xx-generic-ap135-kernel.bin $ erase 0x9fe80000 +$filesize $ cp.b $fileaddr 0x9fe80000 0x160000 $ tftp 0x80060000 openwrt-ar71xx-generic-ap135-rootfs-squashfs.bin $ erase 0x9f050000 +$filesize $ cp.b $fileaddr 0x9f050000 $filesize $ setenv bootcmd 'bootm 0x9fe80000' $ saveenv (5) 路由重启,测试 (6) 启动后如下,测试
一款Windows环境下使用SSH的开源图形化SFTP客户端。支持SCP协议,它的主要功能是在本地与远程计算机间安全地复制文件。具有快速上手的WindowsExplorer界面,亦可切换为参考NC(Norton Commander)的双视窗排列方式界面,操作便捷,可扩展语言包。主要用于Windows 和虚拟机上的Linux之间复制文件。 SCP(Secure copy)是OpenWRT中的SSH附带的文件传输功能,SCP在操作上类似FTP可以实现一个安全的数据文件传输。 SCP适用于调试阶段的程序文件上下载,从而进行快速测试,这是个非常不错的工具。 第一步:正式开放SSH 在默认的情况下由于没有密码,所以SSH无法登入。首先修改系统超级账户密码,在提示中填写两次新密码: root@Ceac:/# passwd 完成后重启SSH的服务: root@Ceac:/# /etc/init.d/dropbear restart 第二步: 安装WinSCP 大家可以使用一个winscp的工具实现这个操作。winscp工具下载连接参考本文附件,安装这个工具软件。注意AR9331和电脑能够通信,否则登录会失败。 第三步: 点击新建
该文章讲解一下在AR9331上使用Openwrt的编译修改方法,前面先介绍一下硬件平台特点,为后面的代码修改做铺垫,然后描述一下Openwrt的编译烧写流程,最后再重点讲述编译配置、代码修改细节。 1. 硬件平台 手头上ar9331模块64MB DDR2,8MB spi flash,配合底板的硬件特点如下: 使用Ubuntu进行编译开发,需要安装的依赖库如下: apt-get install build-essential asciidoc binutils bzip2 gawk gettext \git libncurses5-dev libz-dev patch unzip zlib1g-dev等等。 还需要安装svn,apt-get install subversion。 2.2. 下载源码 svn co svn://svn.openwrt.org/openwrt/trunk/ 2.3. 编译 以下为编译步骤,在make menuconfig中做的配置将在后面具体介绍,在make V=s前需要做一些移植修改也将在后面具体介绍。 cd trunk/ svn up cp feeds.conf.default feeds.conf ./scripts/feeds update -a ./scripts/feeds install -a make menuconfig Make V=s 最终在trunk/bin/ar71xx/目录下得到最终的烧写目标代码(在此以mr3220-v2路由为基础),openwrt-ar71xx-generic-tl-mr3220-v2-squashfs-factory.bin。 2.4. 烧写 官方的Linux SDK开发包的烧录代码与Openwrt的烧录代码的分区规划不同,如果原来开发板上烧录的是官方的linux SDK代码,则需要先将刷新适用于Openwrt的Uboot,然后再烧写上一步中生成的openwrt-ar71xx-generic-tl-mr3220-v2-squashfs-factory.bin。 对开发板烧写程序可以通过多种方式实现:在Uboot中烧写Uboot与固件、编程器烧写、Web配置界面中程序升级。 2.4.1. 烧写Uboot 使用网线连接开发板与PC,然后连接上串口监控(115200-8-n-1,这里使用SecureCRT),开发板上电,在SecureCRT中看到Uboot打印信息,按任意键打断启动进入Uboot命令行,使用printenv命令查看启动参数,记录下serverip,例如为192.168.1.2,并将PC更改为该IP。PC端开启Tftp Sever软件,在uboot命令行中输入命令如下: #tftpboot 0x80060000 u-boot-ar9331.bin #erase 0x9f000000 +0x200000 #cp.b 0x80060000 0x9f000000 0x20000 重启开发板,看到uboot正常打印。 2.4.2. 烧写固件 同上面烧写uboot一样,进入uboot命令行模式,输入如下命令: #tftpboot 0x80060000 openwrt-ar71xx-generic-tl-mr3220-v2-squashfs-factory.bin #erase 0x9f020000 +0x7c0000 #cp.b 0x80060000 0x9f02000 0x7c0000 2.4.3. 编程器拯救板砖 上面两种方式前提是目标板不是板砖,万一变砖或者开发板上未烧写过程序,则需要使用编程器来烧写目标代码,这就是对普通的SPI flash烧录程序方法,只要知道芯片型号,有固件就行了。 2.4.4. Web升级程序 对于能正常启动的AR9331开发板可以通过web升级程序,登录到web配置界面后选择System->Backup/Flash Firmware,然后选择要升级的固件。 注意这种方式只能升级统一型号的路由固件,如现在正在运行的是wr720的固件,你编译了一个wr740的固件,是无法通过web升级的,只能通过上面在Uboot中烧写固件的方式进行升级。 3. menuconfig配置 这里将介绍上面编译步骤中的make menuconfig中的具体配置。在这里配置时候使用MR3220-V2路由器为蓝本。 开始配置,首先选择CPU与路由型号: Target System—–Atheros AR71xx/AR7240/AR913x/AR934x Target Profile—-TP-LINK MR3220 Kernel Modules配置: Kernel modules —> Native Language Support —> <*> kmod-nls-utf8 Luci配置如下: LuCI—>Collections—– <*> luci LuCI—>Applications —> <*>luci-app-qos LuCI—>Themes —> <*>luci-theme-openwrt LuCI—>Translations—- <*> luci-i18n-Chinese LuCI—>Translations—- <*> luci-i18n-English Network配置: Network—>Routing and Redirection—- <*> ip 在这里我们只做了必须的配置,更加丰富的功能可以再额外配置添加,如常见的USB storage、3G、Network Share(Samba)等等。 4. 代码修改 在这里我们做的移植中有些可以通过直接修改开发板中的配置文件来实现,而不需要重新编译固件,但是我们要的是一个编出来就是默认配置的固件,所以以下移植都将是在编译前实现的。 4.1. 编译权限 个人不喜欢在非root权限下编译,为此做一下简单修改使得能够在root权限下进行编译,修改/trunk/include/prereq-build.mk将原来的require non-root修改为root define Require/non-root #[ "$$(shell whoami)" != "root" ] [ "$$(shell whoami)" != "noroot" ] endef 4.2. flash配置 手头上的AR9331核心板上使用的是8MB的dataflash,而标配的MR3220-V2路由是4MB,为此我们需要修改: trunk/tools/firmware-utils/src/mktplinkfw中MR3220-V2配置为8MB .id = "TL-MR3220v2", .hw_id = HWID_TL_MR3220_V2, .hw_rev = 1, .layout_id = "4Mlzma", //改为 8Mlzma trunk/target/linux/ar71xx/image/Makefile中对应MR3220-V2处flash大小为8MB: MR3220-V2,MR3220-V2,MR3220-V2,ttyATH0,115200,0x32200002,1,8Mlzma 4.3. Wifi配置 Openwrt编译出来的固件,wifi默认是不开启的,需要做一些修改让其上电自动开启AP模式。修改trunk/package/kernel/mac80211/files/lib/wifi/mac80211.sh,其中的detect_mac80211函数中就对wifi-device和wifi-iface即无线物理设配与无线接口做了具体的配置。 在wifi-device配置中屏蔽掉“option disable 1”这句就能使wifi上电自启动了,也可以对channel、hwmode、txpower做进一步的配置,如channel设置为auto,txpower设置为30dBm。 在wifi-iface配置中可以改变wifi的模式(mode)、名字(ssid)、加密方式(encryption)、密码(key)。 如下是一个最终配置示例,这其实也就是生成的/etc/config/wireless配置文件。 4.4. 设置默认登陆密码 Openwrt默认不设置密码,而我们需要一个默认的登陆密码,这可以通过修改package/base-files/files/etc/shadow来实现,修改root项如下: root:$1$PjtjPtvn$F7wPtGnsC8lhpfJsB39.E/:16357:0:99999:7::: (对应密码123456) root:$1$0CVWiweD$4Xsq83ZIZtPJe8PVLfqJH0:16357:0:99999:7::: (对应密码admin) 注意Openwrt的登陆用户名为root,且当设置了密码后将不能在使用Telnet来登陆目标板,需使用SSH。 4.5. 设置LAN默认IP Openwrt固件LAN口默认ip为192.168.1.1,我们可以通过如下方法在编译中修改默认ip: 在trunk目录下新建files/etc/config/目录,然后将已经个性化定制好的network配置文件放在这个目录下,这样再编译出来的固件将执行这个network配置。在此我们只要修改lan口的ipaddr即可,如下所示: config interface 'lan' option ifname 'eth1' option force_link '1' option type 'bridge' option proto 'static' option netmask '255.255.255.0' option ip6assign '60' option ipaddr '192.168.12.10' 最根本的方式是通过修改文件package/base-files/files/lib/functions/uci-defaults.sh实现,修改函数ucidef_set_interface_lan中lan口ip即可。 4.6. 串口释放 AR9331只有一个调试串口,如果想要释放调试串口用作他用比如通信等,为此需要释放系统console打印,具体方法如下: 在target/linux/ar71xx/base-files/etc/inittab中注释掉::askconsole:/bin/ash --login,也就是注释掉这个console登陆。 在package/base-files/files/etc/config/system文件中config system下添加以下两句 option 'conloglevel' '1' option 'kconloglevel' '1' 简单测试:开发板串口连接pc,打开SecureCRT,配115200-8-n-1,使用ssh登陆到开发板,命令行下输入”echo test serial > /dev/ttyATH0”,看到PC端接收正常。 4.7. GPIO控制 如果想使用AR9331的一些GPIO引脚来做一些简单逻辑控制,则需要通过注册gpio设备来实现,修改target/linux/ar71xx/files/arch/mips/ath79/mach-tl-wr741nd-v4.c文件 在tl_mr3220_v2_setup函数中添加我们需要的GPIO管脚注册即可,如: gpio_request_one(18,GPIOF_OUT_INIT_LOW | GPIOF_EXPORT_DIR_FIXED, "Control GPIO18"); 这里的18即是GPIO18,GPIOF_OUT_INIT_LOW限定初始化输出低电平,后面的名字可以自定。 系统启动后可以看到GPIO注册成功: #ls /sys/class/gpio/ #export gpio18/ gpio22/ gpiochip0/ uexport/ 可以使用cat命令获取管脚当前输出值,使用echo命令设置管脚输出。如下: #cat gpio18/value #echo 0 > gpio18/value #cat gpio18/value 这里说明一下,在写C代码程序控制时候可以直接调用system()函数运行linux命令,比如在你的代码中你想控制GPIO18引脚的输出,那么你可以如下使用。 system(“echo 0 > /sys/class/gpio/gpio22/value”); 4.8. 按键与指示灯 target/linux/ar71xx/files/arch/mips/ath79/mach-tl-wr741nd-v4.c文件中对各按键及指示灯定义,具体如下: l GPIO12做复位按键,短按则实现路由复位,长按大于5s则复位路由并恢复默认设置 l GPIO11做WPS输入按键 l GPIO17 做WAN口指示灯,对应Port4,低有效 l GPIO13 LAN1指示灯,对应Port1,高有效 l GPIO14 LAN2指示灯,对应Port1,高有效 l GPIO15 LAN3指示灯,对应Port1,高有效 l GPIO16 LAN4指示灯,对应Port1,高有效 l GPIO0 WIFI指示灯,高有效 l GPIO27 SYSTEM指示灯,低有效 此外将Port4与Port0的对换撤销掉,即恢复为使用Port4做wan口,将tl_ap121_setup函数中的 ath79_setup_ar933x_phy4_switch(true,true); ath79_setup_ar933x_phy4_switch(false, false); 修改target/linux/ar71xx/base-files/etc/uci-defaults/01_leds中mr3220-v2灯定义如下: tl-mr3220-v2) ucidef_set_led_netdev "wan" "WAN" "tp-link:green:wan" "eth1" ucidef_set_led_switch "lan1" "LAN1" "tp-link:green:lan1" "switch0" "0x02" ucidef_set_led_switch "lan2" "LAN2" "tp-link:green:lan2" "switch0" "0x04" ucidef_set_led_switch "lan3" "LAN3" "tp-link:green:lan3" "switch0" "0x08" ucidef_set_led_switch "lan4" "LAN4" "tp-link:green:lan4" "switch0" "0x10" ucidef_set_led_wlan "wlan" "WLAN" "tp-link:green:wlan" "phy0tpt" #ucidef_set_led_usbdev "usb" "USB" "tp-link:green:3g" "1-1" 4.9. 时间同步 修改package/base-files/files/etc/config/system文件如下,这样默认同步时间正确。 config system option hostname 'You can Define' option zonename 'Asia/Shanghai' option timezone 'CST-8' 注:这里hostname可以改成你想要的主机名字,将来在web页面中显示的也将是这个主机名。 4.10. 语言及主题设置 编译中的默认设置为auto,即自动识别,这将根据浏览器设置而定,例如我们使用ie、360浏览器,登陆后得到的是英文界面,而使用火狐则是中文界面。如果向默认设置问中文,则修改build_dir/feeds/luci/modules/base/root/etc/config/luci配置文件,将默认auto修改问zh_cn,并将主题锁定为bootstrap config core 'main' option lang 'zh_cn' option mediaurlbase '/luci-static/bootstrap' option resourcebase '/luci-static/resources' 4.11. 自定义脚本的使用 Openwrt为用户预留的自定义启动脚本/package/base-files/files/etc/rc.local来实现一些扩展功能,我们可以在这里添加一些命令,或者调用一个外部脚本来实现一些扩展功能。 参考网址:http://m.blog.csdn.net/blog/jiaozi07/41774357
Linux下编程一直被诟病的一点是: 没有一个好用的IDE, 但是听说Linux牛人, 黑客之类的也都不用IDE. 但是对我等从Windows平台转移过来的Coder来说, 一个好用的IDE是何等的重要啊, 估计很多人就是卡在这个门槛上了, "工欲善其事, 必先利其器"嘛, 我想如果有一个很好用的IDE, 那些Linux牛人也会欢迎的. 这都是劳动人民的美好愿望罢了, 我今天教大家把gvim改装成一个简易IDE, 说它"简易"是界面上看起来"简易", 但功能绝对不比一个好的IDE差, 该有的功能都有, 不该有的功能也有, 你就自己没事偷着乐吧, 下面我开始介绍今天的工作了. 本文会教你: 1. 中文帮助手册的安装 2. vim编程常用命令 3. 语法高亮 4. 在程序中跳来跳去: Ctags 的使用 5. 教你高效地浏览源码 -- 插件: TagList 6. 文件浏览器和窗口管理器 -- 插件: WinManager 7. Cscope 的使用 8. QuickFix 窗口 9. 快速浏览和操作Buffer -- 插件: MiniBufExplorer 10. c/h文件间相互切换 -- 插件: A 11. 在工程中查找 -- 插件: Grep 12. 高亮的书签 -- 插件: VisualMark 13. 自动补全 14. 加速你的补全 -- 插件: SuperTab 本文不会教你: 1. 如何使用vim. 本文不会从零开始教你如何使用vim, 如果你是第一次接触vim, 建议你先看看其他的vim入门的教程, 或者在shell下输入命令: vimtutor, 这是一个简单的入门教程. 2. 编程技巧. 3. vim脚本的编写. 我的工作环境是: Fedora Core 5 gvim是自己编译的7.0, 如果你还没有安装gvim, 请看我的这篇文章<在Redhat Linux 9中编译和配置gvim 7.0> 由于本人一直从事C语言工作, 所以下面这些例子都是在C语言程序中演示的, 其他语言的没有试过, 如果有朋友在别的语言下有问题, 可以跟我讨论一些, 我会尽量帮助你们的. 本文用的示范源码是vim7.1的源码, 可以在www.vim.org下载到源码包:vim-7.1.tar.bz2, 你也可以不用下载, 就用你自己程序的源码, 关系不大的. 我把源码解压在我的home目录下: ~/vim71 下面对文中的一些名字定义一下: 1. 文中用到的一些用<>括起来的符号比如<C-T>, <C-S-A>, 之类的, 你可以用下面的命令看看解释: :help keycodes 2. 文中说的一些路径, 比如: ~/.vim/plugin ~/.vim/doc ~/.vim/syntax 如果你没有, 就自己创建. 3. 文中说到的.vimrc文件都是指 ~/.vimrc 先给大家看张图片, 我是vim的界面, 解解馋先^_^ (--- 图1 ---) 对照上图的图标, 我在本文中将教会你以下这些功能: 简洁明了的Buffer浏览和操作 文件浏览器 tag浏览器 高亮的书签 更丰富的语法高亮 成员变量的下拉, 自动补全 vim自带的帮助手册是英文的, 对平时编程的人来说没有多大阅读困难, 何况还有"星级译王"呢, 可偏偏有一帮人将其翻译成了中文, 可偏偏我又挡不住诱惑将它安装了, 唉.......又痛失一个学习英文的好机会, 下不为例. 大家看看我的中文帮助界面吧: (--- 图2 ---) 安装方法: 在下面的网站下载中文帮助的文件包: $wget http://nchc.dl.sourceforge.net/sourceforge/vimcdoc/vimcdoc-1.5.0.tar.gz 解包后进入文件夹,使用以下命令安装: $sudo ./vimcdoc.sh -i 启动vim,输入:help,看看帮助文档是否已经便成中文了? 一些注意事项: 1.vim中文文档不会覆盖原英文文档,安装后vim默认使用中文文档。若想使用英文文档,可在vim中执行以下命令: set helplang=en 同理,使用以下命令可重新使用中文文档: set helplang=cn 2. 帮助文件的文本是utf-8编码的, 如果想用vim直接查看, 需要在~/.vimrc中设置: set encoding=utf-8 vim编程常用命令 建议先看看帮助手册中的下面章节, 其中有关tags文件的部分你可以先跳过, 在后面的章节中会讲到, 到时候你在回来看看, 就觉得很简单了: :help usr_29 :help usr_30 下面是我常用的一些命令, 放在这里供我备忘: 跳转到配对的括号去 跳转到代码块的开头去(但要求代码块中'{'必须单独占一行) 跳转到局部变量的定义处 跳转到光标上次停靠的地方, 是两个', 而不是一个" 设置书签,x只能是a-z的26个字母 跳转到书签处("`"是1左边的键) 增加缩进,"x>"表示增加以下x行的缩进 减少缩进,"x<"表示减少以下x行的缩进 写程序没有语法高亮将是一件多么痛苦的事情啊, 幸亏vim的作者是个程序员(如果不是, 那可NB大了), 提供了语法高亮功能, 在上面的图片中大家也可以看到那些注释, 关键字, 字符串等, 都用不同颜色显示出来了, 要做到这样, 首先要在你的 ~/.vimrc 文件中增加下面几句话: syntax enable syntax on 再重新启动vim, 并打开一个c程序文件, 是不是觉得眼前突然色彩缤纷了起来... 如果你不喜欢这个配色方案你可以在"编辑->配色方案"(gvim)中选择一个你满意的配色方案, 然后在~/.vimrc文件中增加下面这句: colorscheme desert desert是我喜欢的配色方案, 你可以改成你的. 如果菜单中的配色方案你还不满意(你也太花了吧), 没关系, 在 vim.org 上跟你一样的人很多, 他们做了各种各样的颜色主题, 你可以下载下来一个一个的试, 多地可以看到你眼花. 如果这样你还不满意(你还真是XXXX), 没关系, vim的作者早想到会有你这种人了, 你可以创建你自己的颜色主题, 把下面的这篇文档好好学习一些一下吧: :help syntax.txt 更炫的语法高亮: 你可能会发现很多东西没有高亮起来, 比如运算符号, 各种括号, 函数名, 自定义类型等, 但是看上面的图片, 我的运算符号和函数名都加亮了^_^, 想知道为什么吗? 哇哈哈哈哈.... 让我来教你吧 ... 主要的思路是新建一个语法文件, 在文件中定义你要高亮的东东, 想高亮什么就高亮什么, 用vim就是这么自信. 所谓的语法文件就是vim用来高亮各种源文件的一个脚本, vim靠这个脚本的描述来使文件中的不同文本显示不同的颜色, 比如C语言的语法文件放在类似于这样的一个路径中: /usr/share/vim/vim64/syntax/c.vim 其他语言的语法文件也可以在这个路径中找到, 你的也许不在这个路径中, 不管它, 在你自己的HOME下新建一个语法文件, 新建一个空文件: ~/.vim/syntax/c.vim 在其中加入 "======================================================== " Highlight All Function "======================================================== syn match cFunction "/<[a-zA-Z_][a-zA-Z_0-9]*/>[^()]*)("me=e-2 syn match cFunction "/<[a-zA-Z_][a-zA-Z_0-9]*/>/s*("me=e-1 hi cFunction gui=NONE guifg=#B5A1FF "======================================================== " Highlight All Math Operator "======================================================== " C math operators syn match cMathOperator display "[-+/*/%=]" " C pointer operators syn match cPointerOperator display "->/|/." " C logical operators - boolean results syn match cLogicalOperator display "[!<>]=/=" syn match cLogicalOperator display "==" " C bit operators syn match cBinaryOperator display "/(&/||/|/^/|<</|>>/)=/=" syn match cBinaryOperator display "/~" syn match cBinaryOperatorError display "/~=" " More C logical operators - highlight in preference to binary syn match cLogicalOperator display "&&/|||" syn match cLogicalOperatorError display "/(&&/|||/)=" " Math Operator hi cMathOperator guifg=#3EFFE2 hi cPointerOperator guifg=#3EFFE2 hi cLogicalOperator guifg=#3EFFE2 hi cBinaryOperator guifg=#3EFFE2 hi cBinaryOperatorError guifg=#3EFFE2 hi cLogicalOperator guifg=#3EFFE2 hi cLogicalOperatorError guifg=#3EFFE2 再打开你的C文件看看, 是不是又明亮了许多. 还有一个压箱底的要告诉你, 如果你自己增加了一个类型或者结构之类的, 怎么让它也象"int", "void"这样高亮起来呢? 再在上面的文件~/.vim/syntax/c.vim中添加下面的东东: "======================================================== " My Own DataType "======================================================== syn keyword cType My_Type_1 My_Type_2 My_Type_3 这样你自己的类型My_Type_1, My_Type_2, My_Type_3就也可以向"int"一样高亮起来了, 这样的缺点是每增加一个类型, 就要手动在这里添加一下, 如果有人知道更简单的方法请一定一定要告诉我, 用下面的地址: Email : lazy.fox.wu#gmail.com Homepage : http://blog.csdn.net/wooin 哇, 这下可厉害了, Tag文件(标签文件)可是程序员的看家宝呀, 你可以不用它, 但你不能不知道它, 因为Linux内核源码都提供了"make tags"这个选项. 下面我们就来介绍Tag文件. tags文件是由ctags程序产生的一个索引文件, ctags程序其是叫"Exuberant Ctags", 是Unix上面ctags程序的替代品, 并且比它功能强大, 是大多数Linux发行版上默认的ctags程序. 那么tags文件是做什么用的呢? 如果你在读程序时看了一个函数调用, 或者一个变量, 或者一个宏等等, 你想知道它们的定义在哪儿, 怎么办呢? 用grep? 那会搜出很多不相干的地方. 现在流行用是的<C-]>, 谁用谁知道呀, 当光标在某个函数或变量上时, 按下"Ctrl+]", 光标会自动跳转到其定义处, 够厉害吧, 你不用再羡慕Visual Studio的程序员了, 开始羡慕我吧~_~. 你现在先别急着去按<C-]>, 你按没用的, 要不要我干什么呀, 你现在要做的是查查你电脑里有没有ctags这个程序, 如果有, 是什么版本的, 如果是Ctags 5.5.4, 就象我一样, 你最好去装一个Ctags 5.6, 这个在后面的自动补全章节中会用到. 在这个网站: http://ctags.sourceforge.net, 下载一个类似 ctags-5.6.tar.gz 的文件下来(现在好像5.7版的也出来了, 不过我还没用过): 用下面的命令解压安装: $ tar -xzvf ctags-5.6.tar.gz $ cd ctags-5.6 $ make # make install // 需要root权限 然后去你的源码目录, 如果你的源码是多层的目录, 就去最上层的目录, 在该目录下运行命令: ctags -R 我现在以 vim71 的源码目录做演示 $ cd /home/wooin/vim71 $ ctags -R 此时在/home/wooin/vim71目录下会生成一个 tags 文件, 现在用vim打开 /home/wooin/vim71/src/main.c $ vim /home/wooin/vim71/src/main.c 再在vim中运行命令: :set tags=/home/wooin/vim71/tags 该命令将tags文件加入到vim中来, 你也可以将这句话放到~/.vimrc中去, 如果你经常在这个工程编程的话. 下面要开始真刀实枪的开干了, 如下图, 将光标放在setmouse()函数上 (--- 图3 ---) 此时按下<C-]>, 光标会自动跳到setmouse()函数的定义处, 见下图: (--- 图4 ---) 如果此时你还想再跳回刚才的位置, 你还可以按<C-T>, 这样又跳回到setmouse()函数被调用的地方了, 变量, 结构, 宏, 等等, 都可以的, 赶快试试吧..... 此时在回头学习一下第3节中说的vim手册吧 :help usr_29 不过还有一个小瑕疵, 你修改程序后, 比如增加了函数定义, 删除了变量定义, tags文件不能自动rebuild, 你必须手动再运行一下命令: $ ctags -R 使tags文件更新一下, 不过让人感到欣慰的是vim不用重新启动, 正在编写的程序也不用退出, 马上就可以又正确使用<C-]>和<C-T>了. 如果有人知道更简单的方法请一定一定要告诉我, 用下面的地址: Email : lazy.fox.wu#gmail.com Homepage : http://blog.csdn.net/wooin 在Windows平台上用过Source Insight看程序的人肯定很熟悉代码窗口左边那个Symbol窗口, 那里面列出了当前文件中的所有宏, 全局变量, 函数名等, 在查看代码时用这个窗口总揽全局, 切换位置相当方便, 今天告诉你一个vim的插件: Taglist, 可以同样实现这个功能. 上一节已经告诉你ctags的用法了, ctags的基本原理是将程序程序中的一些关键字(比如:函数名, 变量名等)的名字, 位置等信息通过一个窗口告诉你, 如果你已经安装好taglist, 则可以用下面的命令看看taglist自带的帮助文件: :help taglist.txt 下面是我翻译的其中的第一段"Overview", 供大家现了解一下taglist, 翻译的不好, 请指教: "Tab List"是一个用来浏览源代码的Vim插件, 这个插件可以让你高效地浏览各种不同语言编写的的源代码, "Tag List"有以下一些特点: * 在Vim的一个垂直或水平的分割窗口中显示一个文件中定义的tags(函数, 类, 结构, 变量, 等) * 在GUI Vim中, 可以选择把tags显示在下拉菜单和弹出菜单中 * 当你在多个源文件/缓冲区间切换时, taglist窗口会自动进行相应地更新. 当你打开新文件时, 新文件中定义的tags会被添加到已经存在的文件列表中, 并且所有文件中定义的tags会以文件名来分组显示 * 当你在taglist窗口中选中一个tag名时, 源文件中的光标会自动跳转到该tag的定 * 自动高亮当前的tag名 * 按类型分组各tag, 并且将各组显示在一个可折叠的树形结构中 * 可以显示tag的原始类型和作用域 * 在taglist窗口可选择显示tag的原始类型替代tag名 * tag列表可以按照tag名, 或者时间进行排序 * 支持以下语言的源文件: Assembly, ASP, Awk, Beta, C, C++, C#, Cobol, Eiffel, Erlang, Fortran, HTML, Java, Javascript, Lisp, Lua, Make, Pascal, Perl, PHP, Python, Rexx, Ruby, Scheme, Shell, Slang, SML, Sql, TCL, Verilog, Vim and Yacc. * 可以很简单的扩展支持新的语言. 对新语言支持的修改也很简单. * 提供了一些函数, 可以用来在Vim的状态栏或者在窗口的标题栏显示当前的tag名 * taglist中的文件和tags的列表可以在被保存和在vim会话间加载 * 提供了一些用来取得tag名和原始类型的命令 * 在控制台vim和GUI vim中都可以使用 * 可以和winmanager插件一起使用. winmanager插件可以让你同时使用文件浏览器, 缓冲区浏览器和taglist插件, 就像一个IDE一样. * 可以在Unix和MS-Windows系统中使用 首先请先在你的~/.vimrc文件中添加下面两句: let Tlist_Show_One_File=1 let Tlist_Exit_OnlyWindow=1 此时用vim打开一个c源文件试试: $ vim ~/vim/src/main.c 进入vim后用下面的命令打开taglist窗口, 如图5: :Tlist (--- 图5 ---) 左边的窗口就是前面介绍的TagList窗口, 其中列出了main.c文件中的tag, 并且按照"typedef", "variable", "function"等进行了分类. 将光标移到VimMain上, 如图中左边红色的方框, 按下回车后, 源程序会自动跳转到VimMain的定义处, 如图中右边的红色方框. 这就是TagList最基本也是最常用的操作. 再教你一个常用的操作, 你在浏览TagList窗口时, 如果还不想让源码跳转, 但是想看看tag在源码中完整的表达, 可以将光标移到你想要看的tag上, 如图中上边黄色的方框, 然后按下空格键, 在下面的命令栏中, 如图下边黄色的方框, 会显示该tag在源码中完整的写法, 而不会跳转到源码处. TagList插件我就介绍到这里, 其实它还有很多用法和设置, 我没法一一地教你了, 好在TagList有一份详细的帮助手册, 用下面的命令打开手册, 好好学习一下吧: :help taglist.txt 在图1中大家可以看到在图标2标识的地方是一个文件浏览器, 里面列出了当前目录中的文件, 可以通过这个浏览器来浏览工程中的源文件, 是不是越来越像常见的IDE了, 当光标停在某个文件或文件夹的时候, 按下回车, 可以打开该文件或文件夹. 这个东东是怎么调出来的呢? 其实这个也是由插件实现的, 这个插件是netrw.vim, 只不过你不用下载和安装, 这个插件已经是标准的vim插件, 已经随vim一起安装进你的系统里了, 现在先简单演示一下, 进入"~/vim71"文件夹后运行vim, 然后在vim中运行命令: :e ~/vim71 你将在vim看到如下图所示的界面: (--- 图6 ---) 在该界面上你可以用下面的一些命令来进行常用的目录和文件操作: <F1> <cr> 如果光标下是目录, 则进入该目录; 如果光标下文件, 则打开该文件 返回上级目录 切换vim 当前工作目录正在浏览的目录 删除目录或文件 切换显示方式 文件或目录重命名 选择排序方式 定制浏览方式, 使用你指定的程序打开该文件 我这里不是教你怎么用netrw.vim插件, 而是要教你通过WinManager插件来将TagList窗口和netrw窗口整合起来, 就像图1中的图标2和3组成的那个效果 现在在你的~/.vimrc中增加下面两句 let g:winManagerWindowLayout='FileExplorer|TagList' nmap wm :WMToggle<cr> 然后重启vim, 打开~/vim71/src/main.c, 在normal状态下输入"wm", 你将看到图7的样子: (--- 图7 ---) 其中左上边是netrw窗口, 左下边是TagList窗口, 当再次输入"wm"命令时这两个窗口又关闭了. WinManager的功能主要就是我介绍的这些, 但是它还有其他一些高级功能, 还可以支持其他几个插件, 如果你觉得我介绍的还不够你用, 建议你把它的帮助手册好好研究一下, 用下面的命令可以调出帮助手册: :help winmanager 这下更厉害了, 用Cscope自己的话说 - "你可以把它当做是超过频的ctags", 其功能和强大程度可见一斑吧, 关于它的介绍我就不详细说了, 如果你安装好了前文介绍的中文帮助手册, 用下面的命令看看介绍吧: :help if_cscop.txt 我在这里简单摘抄一点, 供还在犹豫的朋友看看: Cscope 是一个交互式的屏幕下使用的工具,用来帮助你: * 无须在厚厚的程序清单中翻来翻去就可以认识一个 C 程序的工作原理。 * 无须熟悉整个程序就可以知道清楚程序 bug 所要修改的代码位置。 * 检查提议的改动 (如添加一个枚举值) 可能会产生的效果。 * 验证所有的源文件都已经作了需要的修改;例如给某一个现存的函数添加一个参数。 * 在所有相关的源文件中对一个全局变量改名。 * 在所有相关的位置将一个常数改为一个预处理符号。 它被设计用来回答以下的问题: * 什么地方用到了这个符号? * 这是在什么地方定义的? * 这个变量在哪里被赋值? * 这个全局符号的定义在哪里? * 这个函数在源文件中的哪个地方? * 哪些函数调用了这个函数? * 这个函数调用了哪些函数? * 信息 "out of space" 从哪来? * 这个源文件在整个目录结构中处于什么位置? * 哪些文件包含这个头文件? 安装Cscope: 如果你的系统中有cscope命令, 则可以跳过这一小段, 如果没有, 就先跟着我一起安装一个吧. 在Cscope的主页: http://cscope.sourceforge.net 下载一个源码包, 解压后编译安装: # ./configure # make # make install // 需要root权限 先在~/vimrc中增加一句: :set cscopequickfix=s-,c-,d-,i-,t-,e- 这个是设定是否使用 quickfix 窗口来显示 cscope 结果, 用法在后面会说到。 跟Ctags一样, 要使用其功能必须先为你的代码生成一个cscope的数据库, 在项目的根目录运行下面的命令: $ cd /home/wooin/vim71/ $ cscope -Rbq # 此后会生成三个文件 $ ll cscope.* -rw-rw-r-- 1 wooin wooin 1.1M 2007-09-30 10:56 cscope.in.out -rw-rw-r-- 1 wooin wooin 6.7M 2007-09-30 10:56 cscope.out -rw-rw-r-- 1 wooin wooin 5.1M 2007-09-30 10:56 cscope.po.out # 打开文件, 开始Coding $ cd src $ vi main.c 进入vim后第一件事是要把刚才生成的cscope文件导入到vim中来, 用下面的命令: :cs add /home/wooin/vim71/cscope.out /home/wooin/vim71 上面这条命令很重要, 必须写全, 不能只写前半句: :cs add /home/wooin/vim71/cscope.out 因为源码是多级目录的, 如果这样写, cscope是无法在子目录中的源码中工作的, 当然, 如果你的源码都在同一级目录中就无所谓了. 如果你要经常用cscope的话, 可以把上面那句加到~/.vimrc中去. 下面我们来操练一下, 查找函数vim_strsave()的定义, 用命令: :cs find g vim_strsave (--- 图8 ---) 按下回车后会自动跳转到vim_strsave()的定义处. 此时你肯定会说Ctags也可以做到这个呀, 那么下面说的这个Ctags就无法做到了, 我想查找vim_strsave()到底在那些地方被调用过了, 用命令: :cs find c vim_strsave 按下回车后vim会自动跳转到第一个符合要求的地方, 并且在命令栏显示有多少符合要求的结果, 如图: (--- 图9 ---) 如果自动跳转的位置你不满意, 想看其他的结果, 可以用下面的命令打开QuickFix窗口: (--- 图10 ---) 这时你就可以慢慢挑选了^_^ cscope的主要功能是通过同的子命令"find"来实现的 "cscope find"的用法: cs find c|d|e|f|g|i|s|t name 0 或 s 查找本 C 符号(可以跳过注释) 1 或 g 查找本定义 2 或 d 查找本函数调用的函数 3 或 c 查找调用本函数的函数 4 或 t 查找本字符串 6 或 e 查找本 egrep 模式 7 或 f 查找本文件 8 或 i 查找包含本文件的文件 如果每次查找都要输入一长串命令的话还真是件讨人厌的事情, Cscope的帮助手册中推荐了一些快捷键的用法, 下面是其中一组, 也是我用的, 将下面的内容添加到~/.vimrc中, 并重启vim: nmap <C-_>s :cs find s <C-R>=expand("<cword>")<CR><CR> nmap <C-_>g :cs find g <C-R>=expand("<cword>")<CR><CR> nmap <C-_>c :cs find c <C-R>=expand("<cword>")<CR><CR> nmap <C-_>t :cs find t <C-R>=expand("<cword>")<CR><CR> nmap <C-_>e :cs find e <C-R>=expand("<cword>")<CR><CR> nmap <C-_>f :cs find f <C-R>=expand("<cfile>")<CR><CR> nmap <C-_>i :cs find i ^<C-R>=expand("<cfile>")<CR><CR> nmap <C-_>d :cs find d <C-R>=expand("<cword>")<CR><CR> 当光标停在某个你要查找的词上时, 按下<C-_>g, 就是查找该对象的定义, 其他的同理. 按这种组合键有一点技巧,按了<C-_>后要马上按下一个键,否则屏幕一闪就回到nomal状态了 <C-_>g的按法是先按"Ctrl+Shift+-", 然后很快再按"g" 很奇怪, 其中的这句: nmap <C-_>i :cs find i ^<C-R>=expand("<cfile>")<CR>$<CR> 在我的vim中无法工作, 但是我改成: nmap <C-_>i :cs find i <C-R>=expand("<cfile>")<CR><CR> 就可以正常工作了, 不知道是什么原因? 有哪位朋友知道请告诉我. cscope的其他功能你可以通过帮助手册自己慢慢学习 reset : 重新初始化所有连接。 用法 : cs reset QuickFix 窗口 在上一节的图10中大家可以看到在窗口下面有一个显示查询结果的窗口, 这个窗口中列出了查询命令的查询结果, 用户可以从这个窗口中选择每个结果进行查看, 这个窗口叫"QuickFix"窗口, 以前也是一个vim的插件来的, 只不过现在成了vim的标准插件, 不用你在去安装了, QuickFix窗口的主要作用就是上面看到的那个功能: 输出一些供选择的结果, 可以被很多命令调用, 更详细的介绍和使用方法请用下面的命令打开QuickFix的手册来学习吧: :help quickfix 这里我一个常用的例子来再介绍一种QuickFix窗口的使用方法. 这个例子是要模仿平时我们编程时, 当编译出错时, QuickFix会把出错的信息列出来, 供我们一条条地查看和修改. 首先还是用vim打开~/vim71/src/main.c, 事先最好先编译过vim71, 否则一会儿编译的时候有点慢, 或者你也可以自己写一个小的有错误的程序来跟着我做下面的步骤, 见下图: (--- 图11 ---) 我们修改一下main.c, 人为地造成几处错误, 在第1019行增加了一个baobao_wu的没有任何定义的字符串, 删除了第1020行最后的一个括号")", 然后用下面的命令进行编译: :make 显然编译会报很多错误, 当编译结束并退出到源码界面时, 刚才编译器报的错误都已经看不到了, 但是我们可以用QuickFix窗口再将错误信息找出来, 用下面的命令调出QuickFix窗口: 此时你就可以看如下图所示的QuickFix窗口了: (--- 图12 ---) 在下面的QuickFix窗口中我们可以找到每一个编译错误, 同样你可以用鼠标点击每一条记录, 代码会马上自动跳转到错误处, 你还可以用下面的命令来跳转: :cn // 切换到下一个结果 :cp // 切换到上一个结果 如果你经常使用这两个命令, 你还可以给他们设定快捷键, 比如在~/.vimrc中增加: nmap <F6> :cn<cr> nmap <F7> :cp<cr> 其还有其他的命令/插件也会用到QuickFix窗口, 但是用法基本上的都是类似的, 本文后面还会用到QuickFix窗口, 接着往下看吧. 快速浏览和操作Buffer -- 插件: MiniBufExplorer 在编程的时候不可能永远只编辑一个文件, 你肯定会打开很多源文件进行编辑, 如果每个文件都打开一个vim进行编辑的话那操作起来将是多麻烦啊, 所以vim有buffer(缓冲区)的概念, 可以看vim的帮助: :help buffer vim自带的buffer管理工具只有:ls, :bnext, :bdelete 等的命令, 既不好用, 又不直观. 现在隆重向你推荐一款vim插件(plugin): MiniBufExplorer 使用方法: 重新启动vim, 当你只编辑一个buffer的时候MiniBufExplorer派不上用场, 当你打开第二个buffer的时候, MiniBufExplorer窗口就自动弹出来了, 见下图: (--- 图13 ---) 上面那个狭长的窗口就是MiniBufExplorer窗口, 其中列出了当前所有已经打开的buffer, 当你把光标置于这个窗口时, 有下面几个快捷键可以用: <Tab> 向前循环切换到每个buffer名上 <S-Tab> 向后循环切换到每个buffer名上 <Enter> 在打开光标所在的buffer 删除光标所在的buffer 以下的两个功能需要在~/.vimrc中增加: let g:miniBufExplMapCTabSwitchBufs = 1 <C-Tab> 向前循环切换到每个buffer上,并在但前窗口打开 <C-S-Tab> 向后循环切换到每个buffer上,并在但前窗口打开 如果在~/.vimrc中设置了下面这句: let g:miniBufExplMapWindowNavVim = 1 则可以用<C-h,j,k,l>切换到上下左右的窗口中去,就像: C-w,h j k l 向"左,下,上,右"切换窗口. 在~/.vimrc中设置: let g:miniBufExplMapWindowNavArrows = 1 是用<C-箭头键>切换到上下左右窗口中去 c/h文件间相互切换 -- 插件: A 下面介绍它的用法: 作为一个C程序员, 日常Coding时在源文件与头文件间进行切换是再平常不过的事了, 直接用vim打开其源/头文件其实也不是什么麻烦事, 但是只用一个按键就切换过来了, 这是多么贴心的功能啊.... 安装好a.vim后有下面的几个命令可以用了: 在新Buffer中切换到c/h文件 横向分割窗口并打开c/h文件 纵向分割窗口并打开c/h文件 新建一个标签页并打开c/h文件 其他还有一些命令, 你可以在它的网页上看看, 我都没用过, 其实也都是大同小异, 找到自己最顺手的就行了. 我在~/.vimrc中增加了一句: nnoremap <silent> <F12> :A<CR> 意思是按F12时在一个新的buffer中打开c/h文件, 这样在写程序的时候就可以不假思索地在c/h文件间进行切换, 减少了按键的次数, 思路也就更流畅了, 阿弥陀佛.... 在工程中查找 -- 插件: Grep 下面介绍它的用法: vim有自己的查找功能, 但是跟shell中的grep比起来还是有些差距的, 有时Coding正火急火燎的时候, 真想按下F3, 对光标所在的词来个全工程范围的grep, 不用敲那些繁琐的命令, 现在福音来了, 跟我同样懒的人不在少数, 在grep.vim脚本的前部可以找到一些说明文档: :Grep 按照指定的规则在指定的文件中查找 :Rgrep 同上, 但是是递归的grep :GrepBuffer 在所有打开的缓冲区中查找 :Bgrep :GrepArgs 在vim的argument filenames (:args)中查找 :Fgrep 运行fgrep :Rfgrep 运行递归的fgrep :Egrep 运行egrep :Regrep 运行递归的egrep :Agrep 运行agrep :Ragrep 运行递归的agrep 上面的命令是类似这样调用的: :Grep [<grep_options>] [<search_pattern> [<file_name(s)>]] :Rgrep [<grep_options>] [<search_pattern> [<file_name(s)>]] :Fgrep [<grep_options>] [<search_pattern> [<file_name(s)>]] :Rfgrep [<grep_options>] [<search_pattern> [<file_name(s)>]] :Egrep [<grep_options>] [<search_pattern> [<file_name(s)>]] :Regrep [<grep_options>] [<search_pattern> [<file_name(s)>]] :Agrep [<grep_options>] [<search_pattern> [<file_name(s)>]] :Ragrep [<grep_options>] [<search_pattern> [<file_name(s)>]] :GrepBuffer [<grep_options>] [<search_pattern>] :Bgrep [<grep_options>] [<search_pattern>] :GrepArgs [<grep_options>] [<search_pattern>] 但是我从来都不用敲上面那些命令的^_^, 因为我在~/.vimrc中增加了下面这句: nnoremap <silent> <F3> :Grep<CR> 比如你想在/home/wooin/vim71/src/main.c中查找"FEAT_QUICKFIX", 则将光标移到"FEAT_QUICKFIX"上, 然后按下F3键, 如下图: (--- 图14 ---) 在最下面的命令行会显示: Search for pattern: FEAT_QUICKFIX 此时你还可以编辑该行, grep支持正则表达式, 你想全词匹配的话可以改成: Search for pattern: /<FEAT_QUICKFIX/> 然后按下回车: (--- 图15 ---) 在最下面的命令行会显示: Search in files: * 是问你搜索范围, 默认是该目录下的所有文件, 此时你还可以编辑该行, 比如你只想搜索源码文件: Search in files: *.c *.h 然后在按下回车, 会在弹出的QuickFix窗口中列出所有符合条件的搜索结果, 你可以在其中查找你想要的结果, 如下图: (--- 图16 ---) 其实还有一些其他功能和设置, 但是我都没有用过, 这些功能再加上正则表达式, 已经够我用了, 其他的你可以在网页上看看它的文档, 如果有什么惊人发现记得跟我互通有无, 共同进步哦.... 高亮的书签 -- 插件: VisualMark 该"书签"有个很很大的缺点: 不可见. 我下面要介绍的Visual Mark插件跟vim中的"Mark"没有什么关系, 并不是使其可见, 而是自己本身就是"可见的书签", 接着往下看就明白了, 用作者的话说就是"类似UltraEdit中的书签". 另外, 网上也有使vim中的Mark可见的插件, 但是我试了一下, 好像没Visual Mark好用, 我就不介绍了. 按照上面的方法安装好Visual Mark后, 你什么也不用设置, 如果是gvim, 直接在代码上按下Ctrl+F2, 如果是vim, 用"mm", 怎么样, 发现光标所在的行变高亮了吧, 见下图: (--- 图17 ---) 如果你设置了多个书签, 你可以用F2键正向在期间切换, 用Shift+F2反向在期间切换. 好了, 我Visual Mark介绍完了, 够简单吧^_^. 如果你嫌书签的颜色不好看, 你还可以自己定义, 不过是修改这个插件脚本的的源码, 在目录~/.vim/plugin/中找到并打开visualmark.vim, 找到下面这段代码: if &bg == "dark" // 根据你的背景色风格来设置不同的书签颜色 highlight SignColor ctermfg=white ctermbg=blue guifg=wheat guibg=peru else // 主要就是修改guibg的值来设置书签的颜色 highlight SignColor ctermbg=white ctermfg=blue guibg=grey guifg=RoyalBlue3 endif 安装 visualmark.vim 后,如果是在 Ubuntu 下做标记,会报一个“E197 不能设定语言为'en_US'"的错误,但是在 Windows 下却不会。在网上找了一下,发现修复方法。 只要将exec ":lan mes en_US" 修改为 exec ":lan POSIX" 即可,为了能够在两个系统中都能使用,于是修改了一下 visualmark.vim 源码,就是在 exec 外加了一个判断系统的语句。本来还想直接上传一份供大家下载使用,才发现 Iteye 居然只能上传图像.... 这里就提供具体修改方法: 使用文本编辑器打开 visualmark.vim exec ":lan mes en_US" if has("win32") || has("win95") || has("win64") || has("win16") exec ":lan mes en_US" exec ":lan POSIX" endif 保存即可。 1 这个书签不能自动保存, 关闭vim就没了. 2 切换书签时不能在不同文件间切换, 只能在同一个文件中切换 如果哪位朋友能解决这两个问题, 请一定要告诉寡人啊....还是用下面的地址: Email : lazy.fox.wu#gmail.com Homepage : http://blog.csdn.net/wooin 用过Microsoft Visual Studio的朋友一定知道代码补全功能, 输入一个对象名后再输入"."或者"->", 则其成员名都可以列出来, 使Coding流畅了许多, 实现很多懒人的梦想, 现在我要告诉你, 这不再是Microsoft Visual Studio的专利了, vim也可以做到! 下面由我来教你, 该功能要tags文件的支持, 并且是ctags 5.6版本, 可以看看前文介绍tags文件的章节. 我这里要介绍的功能叫"new-omni-completion(全能补全)", 你可以用下面的命令看看介绍: :help new-omni-completion 你还需要在~/.vimrc文件中增加下面两句: filetype plugin indent on 打开文件类型检测, 加了这句才可以用智能补全 set completeopt=longest,menu 关掉智能补全时的预览窗口 请确定你的Ctags 5.6已经安装好, 并且生成的tags文件已经可以用了, 那么我们就要抄家伙开搞了. 用vim打开源文件 $ vi /home/wooin/vim71/src/main.c 设置tags文件 :set tags=/home/wooin/vim71/tags 随便找一个有成员变量的对象, 比如"parmp", 进入Insert模式, 将光标放在"->"后面, 然后按下"Ctrl+X Ctrl+O", 此时会弹出一个下列菜单, 显示所有匹配的标签, 如下图: (--- 图18 ---) 此时有一些快捷键可以用: Ctrl+P 向前切换成员 Ctrl+N 向后切换成员 Ctrl+E 表示退出下拉窗口, 并退回到原来录入的文字 Ctrl+Y 表示退出下拉窗口, 并接受当前选项 如果你增加了一些成员变量, 全能补全还不能马上将新成员补全, 需要你重新生成一下tags文件, 但是你不用重启vim, 只是重新生成一下tags文件就行了, 这时全能补全已经可以自动补全了, 还真够"全能"吧. vim中的其他补全方式还有: Ctrl+X Ctrl+L Ctrl+X Ctrl+N 根据当前文件里关键字补全 Ctrl+X Ctrl+K 根据字典补全 Ctrl+X Ctrl+T 根据同义词字典补全 Ctrl+X Ctrl+I 根据头文件内关键字补全 Ctrl+X Ctrl+] 根据标签补全 Ctrl+X Ctrl+F 补全文件名 Ctrl+X Ctrl+D 补全宏定义 Ctrl+X Ctrl+V 补全vim命令 Ctrl+X Ctrl+U 用户自定义补全方式 Ctrl+X Ctrl+S 在上面一节中你应该学会了自动补全代码的功能, 按下"Ctrl+X Ctrl+O"就搞定了, 如果你够懒的话肯定会说"这么麻烦啊, 居然要按四个键", 不必为此自责, 因为Gergely Kontra 和 Eric Van Dewoestine也跟你差不多, 只不过人家开发了supertab.vim这个插件, 可以永远懒下去了, 下面我来教你偷懒吧. 在你的~/.vimrc文件中加上这两句: let g:SuperTabRetainCompletionType=2 let g:SuperTabDefaultCompletionType="<C-X><C-O>" 以后当你准备按"Ctrl+X Ctrl+O"的时候直接按<Tab>就好了, 够爽吧 .... 我稍微再介绍一下上面那两句配置信息: let g:SuperTabDefaultCompletionType="<C-X><C-O>" " 设置按下<Tab>后默认的补全方式, 默认是<C-P>, " 现在改为<C-X><C-O>. 关于<C-P>的补全方式, " 还有其他的补全方式, 你可以看看下面的一些帮助: " :help ins-completion " :help compl-omni let g:SuperTabRetainCompletionType=2 " 0 - 不记录上次的补全方式 " 1 - 记住上次的补全方式,直到用其他的补全命令改变它 " 2 - 记住上次的补全方式,直到按ESC退出插入模式为止 但是现在我的<Tab>键不好用了, 我以前爱用<Tab>进行缩进, 如果前面有字符按下<Tab>键后就会进行补全, 而不是我想要的缩进功能, 不知道有没有快捷键可以暂时关闭和激活SuperTab键的功能. 如果哪位朋友知道, 请一定记得告诉我啊....还是用下面的地址: Email : lazy.fox.wu#gmail.com Homepage : http://blog.csdn.net/wooin "帮助可怜的乌干达儿童" vim的作者开发了这么个强大, 好用的编辑器, 并且是完全开源, 完全免费的, 不知道比尔盖子会不会觉得这个人真是不可理喻. 作者对用户用户的唯一期望就是帮助乌干达的儿童, 可以用命令:hlep kcc查看详细的内容, 1、捐助一个读小学的孩子:每月 17 欧元 (或更多)。 2、捐助一个读中学的孩子:每月 25 欧元 (或更多)。 3、捐助诊所:每月或每季度,数额不限。 4、一次性捐赠。有条件的就资助一下, 没条件的就像我一样帮助宣传一下吧, 这也算是开源精神..... 【vim 中Taglist的安装和使用详解】 1. 下载与安装 1)从http://www.vim.org/scripts/script.php?script_id=273下载安装包,也可以从http://vim-taglist.sourceforge.net/index.html下载。 2)进入~/.vim目录,将Taglist安装包解压,解压后会在~/.vim目录中生成几个新子目录,如plugin和doc(安装其它插件时,可能还会新建autoload等其它目录)。 3)进入~/.vim/doc目录,在Vim下运行"helptags ."命令。此步骤是将doc下的帮助文档加入到Vim的帮助主题中,这样我们就可以通过在Vim中运行“help taglist.txt”查看taglist帮助。 4)打开配置文件~/.vimrc,加入以下几行: [html] view plaincopyprint? 在taglist窗口中,可以使用下面的快捷键: <CR> 跳到光标下tag所定义的位置,用鼠标双击此tag功能也一样o 在一个新打开的窗口中显示光标下tag<Space> 显示光标下tag的原型定义u 更新taglist窗口中的tags 更改排序方式,在按名字排序和按出现顺序排序间切换x taglist窗口放大和缩小,方便查看较长的tag+ 打开一个折叠,同zo- 将tag折叠起来,同zc* 打开所有的折叠,同zR= 将所有tag折叠起来,同zM[[ 跳到前一个文件]] 跳到后一个文件q 关闭taglist窗口<F1> 显示帮助 但是!这些大部分可以被鼠标取代!!快捷键是浮云~~ 如果鼠标在Vim 里面点击无效,请在~/.vimrc下加入这句话: - Tlist_Ctags_Cmd选项用于指定你的Exuberant ctags程序的位置,如果它没在你PATH变量所定义的路径中,需要使用此选项设置一下; - 如果你不想同时显示多个文件中的tag,设置Tlist_Show_One_File为1。缺省为显示多个文件中的tag; - 设置Tlist_Sort_Type为”name”可以使taglist以tag名字进行排序,缺省是按tag在文件中出现的顺序进行排序。按tag出现的范围(即所属的namespace或class)排序,已经加入taglist的TODO List,但尚未支持; - 如果你在想taglist窗口是最后一个窗口时退出VIM,设置Tlist_Exit_OnlyWindow为1; - 如果你想taglist窗口出现在右侧,设置Tlist_Use_Right_Window为1。缺省显示在左侧。 - 在gvim中,如果你想显示taglist菜单,设置Tlist_Show_Menu为1。你可以使用Tlist_Max_Submenu_Items和Tlist_Max_Tag_Length来控制菜单条目数和所显示tag名字的长度; - 缺省情况下,在双击一个tag时,才会跳到该tag定义的位置,如果你想单击tag就跳转,设置Tlist_Use_SingleClick为1; - 如果你想在启动VIM后,自动打开taglist窗口,设置Tlist_Auto_Open为1; - 如果你希望在选择了tag后自动关闭taglist窗口,设置Tlist_Close_On_Select为1; - 当同时显示多个文件中的tag时,设置Tlist_File_Fold_Auto_Close为1,可使taglist只显示当前文件tag,其它文件的tag都被折叠起来。 - 在使用:TlistToggle打开taglist窗口时,如果希望输入焦点在taglist窗口中,设置Tlist_GainFocus_On_ToggleOpen为1; - 如果希望taglist始终解析文件中的tag,不管taglist窗口有没有打开,设置Tlist_Process_File_Always为1; - Tlist_WinHeight和Tlist_WinWidth可以设置taglist窗口的高度和宽度。Tlist_Use_Horiz_Window为1设置taglist窗口横向显示; 使用方法:在~/.vimrc文件中,用类似上文提到的格式加入要设置的选项。 比如,设置单击tag就跳到tag定义的位置,就在文件中加入这句话: [html] view plaincopyprint? 1)Vim存在多个配置文件vimrc,比如/etc/vimrc,此文件影响整个系统的Vim。还有~/.vimrc,此文件只影响本用户的Vim。而且~/.vimrc文件中的配置会覆盖/etc/vimrc中的配置。这里我们只修改~/.vimrc文件。 2)Vim的插件(plugin)安装在Vim的runtimepath目录下,你可以在Vim命令行下运行"set rtp“命令查看。这里我们选择安装在~/.vim目录,没有就创建一个。 3)当本文说”在Vim命令行下运行cmdxx命令“时,意思是指在Vim的命令行模式下运行cmdxx命令,即在Vim的正常模式下通过输入冒号":"进入命令行模式,然后紧接着输入命令cmdxx。在后文描述中都会省略冒号":"输入。 4)如果没有说明“在Vim命令行下运行某命令”,则是在shell中执行该命令。 5)如果命令中间被空白符间隔或有与正文容易混淆的字符,我会用双引号将命令与正文区分。所以读者在实际操作时,不要输入命令最前面和最后面引号。 6)本文关于组合快捷键的描述,形如a-b形式的快捷键表示同时按下a键和b键,而形如"a-b c"形式的快捷键,则表示先同时按下a键和b键,然后放开ab键,再按下c键。 7) 本人使用的系统是Ubunt 11.10 ,Vim版本是Vi IMproved 7.3 【vim 中Ctags的安装和使用】 『插件介绍』 Ctags工具是用来遍历源代码文件生成tags文件,这些tags文件能被编辑器或其它工具用来快速查找定位源代码中的符号(tag/symbol),如变量名,函数名等。比如,tags文件就是Taglist和OmniCppComplete工作的基础。 『下载和安装』 一、我是使用apt-get安装的: sudo apt-get install ctags 二、下载源码安装(网上提供的方法) 1)从http://ctags.sourceforge.net/下载源代码包后,解压缩生成源代码目录, 2)然后进入源代码根目录执行./configure, 3)然后执行make, 4)编译成功后执行make install。 『基本功能使用方法』 常用命令列表: 1. $ ctags –R * ($ 为Linux系统Shell提示符) 2. $ vi –t tag (请把tag替换为您欲查找的变量或函数名) 3. :ts (ts 助记字:tags list, “:”开头的命令为VI中命令行模式命令) 4. :tp (tp 助记字:tags preview) 5. :tn (tn 助记字:tags next) 6. Ctrl + ] 7. Ctrl + T 命令解释: “$ ctags –R *”:“-R”表示递归创建,也就包括源代码根目录(当前目录)下的所有子目录。“*”表示所有文件。这条命令会在当前目录下产生一个“tags”文件,当用户在当前目录中运行vi时,会自动载入此tags文件。 Tags文件中包括这些对象的列表: 用#define定义的宏 枚举型变量的值 函数的定义、原型和声明 名字空间(namespace) 类型定义(typedefs) 变量(包括定义和声明) 类(class)、结构(struct)、枚举类型(enum)和联合(union) 类、结构和联合中成员变量或函数 VIM用这个“tags”文件来定位上面这些做了标记的对象。 剩下的命令就是定位这些对象的方法: “$ vi –t tag” :在运行vim的时候加上“-t”参数,例如: [/usr/src]$ vim -t main 这个命令将打开定义“main”(变量或函数或其它)的文件,并把光标定位到这一行。 如果这个变量或函数有多处定义,在VI命令行模式 “:ts”命令就能列出一个列表供用户选择。 “:tp”为上一个tag标记文件, “:tn”为下一个tag标记文件。当然,若当前tags文件中用户所查找的变量或函数名只有一个,“:tp,:tn”命令不可用。 最方便的方法是把光标移到变量名或函数名上,然后按下“Ctrl+]”,这样就能直接跳到这个变量或函数定义的源文件中,并把光标定位到这一行。用“Ctrl+t”可以退回原来的地方。即使用户使用了N次“Ctrl+]”查找了N个变量,按N次“Ctrl+t”也能回到最初打开的文件,它会按原路返回 。 更多功能通过命令man ctags或在Vim命令行下运行help ctags查询。 注意:运行vim的时候,必须在“tags”文件所在的目录下运行。否则,运行vim的时候还要用“:settags=”命令设定“tags”文件的路径,这样vim才能找到“tags”文件。 在完成编码时,可以手工删掉tags文件。 参考网址:namecyf的专栏 TagList下载:点击打开链接 参考网址二:http://blog.csdn.net/duguteng/article/details/7417276OpenWrt的源代码管理默认用的是SVN下载: svn co svn://svn.openwrt.org/openwrt/trunk/ .还可以用Git下载:git clone git://git.openwrt.org/openwrt.git git clone git://git.openwrt.org/packages.git OpenWRT的feeds包括: packages – 提供众多库, 工具等基本功能. 也是其他feed所依赖的软件源, 因此在安装其他feed前一定要先安装packages! luci – OpenWrt默认的GUI(WEB管理界面). xwrt – 另一种可替换LuCI的GUI qpe – DreamBox维护的基于Qt的图形界面, 包含Qt2, Qt4, Qtopia, OPIE, SMPlayer等众多图形界面. device – DreamBox维护与硬件密切相关的软件, 如uboot, qemu等. dreambox_packages – DreamBox维护的国内常用网络工具, 如oh3c, njit8021xclient等. desktop - OpenWrt用于桌面的一些软件包. xfce - 基于Xorg的著名轻量级桌面环境. Xfce建基在GTK+2.x之上, 它使用Xfwm作为窗口管理器. efl - 针对enlightenment. phone -针对fso, paroli. trunk中默认的feeds下载有packages、xwrt、luci、routing、telephony。如要下载其他的软件包,需打开源码根目录下面的feeds.conf.default文件,去掉相应软件包前面的#号,然后更新源: ./scripts/feeds update -a 安装下载好的包: ./scripts/feeds install -a OpenWrt源码目录结构: tools和toolchain目录:包含了一些通用命令, 用来生成固件, 编译器, 和C库. build dir/host目录:是一个临时目录, 用来储存不依赖于目标平台的工具. build dir/toolchain-目录:用来储存依赖于指定平台的编译链. 只是编译文件存放目录无需修改. build dir/target-目录:用来储存依赖于指定平台的软件包的编译文件, 其中包括linux内核, u-boot, packages, 只是编译文件存放目录无需修改. staging_dir目录:是编译目标的最终安装位置, 其中包括rootfs, package, toolchain. package目录:软件包的下载编译规则, 在OpenWrt固件中, 几乎所有东西都是.ipk, 这样就可以很方便的安装和卸载. target目录:目标系统指嵌入式设备, 针对不同的平台有不同的特性, 针对这些特性, "target/linux"目录下按照平台进行目录划分, 里面包括了针对标准内核的补丁, 特殊配置等. bin目录:编译完OpenWrt的二进制文件生成目录, 其中包括sdk, uImage, u-boot, dts, rootfs构建一个嵌入式系统完整的二进制文件. config目录:存放着整个系统的的配置文件. docs目录:里面不断包含了整个宿主机的文件源码的介绍, 里面还有Makefile为目标系统生成docs. include目录:里面包括了整个系统的编译需要的头文件, 但是是以Make进行连接的. feeds目录:扩展软件包索引目录. scripts目录:组织编译整个OpenWrt的规则. tmp目录:编译文件夹, 一般情况为空. dl目录:所有软件的下载目录, 包括u-boot, kernel. logs目录:如果编译出错, 可以在这里找到编译出错的log. 参考网址:http://wiki.openwrt.org/doc/devel/feeds
1、有线和无线网络 目前有线网络中最著名的是以太网(Ethenet),但是无线网络WLAN是一个很有前景的发展领域,虽然可能不会完全取代以太网,但是它正拥有越来越多的用户,无线网络中最有前景的是Wifi。本文介绍无线网络相关内容。 无线网络相比有线网络,还是有许多的缺点的: (*)通信双方因为是通过无线进行通信,所以通信之前需要建立连接;而有线网络就直接用线缆连接,不用这个过程了。 (*)通信双方通信方式是半双工的通信方式;而有线网络可以是全双工。 (*)通信时在网络层以下出错的概率非常高,所以帧的重传概率很大,需要在网络层之下的协议添加重传的机制(不能只依赖上面TCP/IP的延时等待重传等开销来保证);而有线网络出错概率非常小,无需在网络层有如此复杂的机制。 (*)数据是在无线环境下进行的,所以抓包非常容易,存在安全隐患。 (*)因为收发无线信号,所以功耗较大,对电池来说是一个考验。 (*)相对有线网络吞吐量低,这一点正在逐步改善,802.11n协议可以达到600Mbps的吞吐量。 Ethenet和Wifi采用的协议都属于IEEE 802协议集。其中,Ethenet以802.3协议做为其网络层以下的协议;而Wifi以802.11做为其网络层以下的协议。无论是有线网络,还是无线网络,其网络层以上的部分,基本一样。 这里主要关注的是Wifi网络中相关的内容。Wifi的802.11协议包含许多子部分。其中按照时间顺序发展,主要有: (1)802.11a,1999年9月制定,工作在5gHZ的频率范围(频段宽度325MHZ),最大传输速率54mbps,但当时不是很流行,所以使用的不多。 (2)802.11b,1999年9月制定,时间比802.11a稍晚,工作在2.4g的频率范围(频段宽度83.5MHZ),最大传输速率11mbps。 (3)802.11g,2003年6月制定,工作在2.4gHZ频率范围(频段宽度83.5MHZ),最大传输速率54mbps。 (4)802.11n,2009年才被IEEE批准,在2.4gHZ和5gHZ均可工作,最大的传输速率为600mbps。 这些协议均为无线网络的通信所需的基本协议,最新发展的,一般要比最初的有所改善。 另外值得注意的是,802.11n在MAC层上进行了一些重要的改进,所以导致网络性能有了很大的提升例如: (*)因为传输速率在很大的程度上取决于Channel(信道)的ChannelWidth有多宽,而802.11n中采用了一种技术,可以在传输数据的时候将两个信道合并为一个,再进行传输,极大地提高了传输速率(这又称HT-40,high through)。 (*)802.11n的MIMO(多输入输出)特性,使得两对天线可以在同时同Channel上传输数据,而两者却能够不相互干扰(采用了OFDM特殊的调制技术) 讲述之前,我们需要对无线网络中一些常用的术语有所了解。这里先列出一些,后面描述中出现的新的术语,将会在描述中解释。 (*)LAN:即局域网,是路由和主机组成的内部局域网,一般为有线网络。 (*)WAN:即广域网,是外部一个更大的局域网。 (*)WLAN(Wireless LAN,即无线局域网):前面我们说过LAN是局域网,其实大多数指的是有线网络中的局域网,无线网络中的局域网,一般用WLAN。 (*)AP(Access point的简称,即访问点,接入点):是一个无线网络中的特殊节点,通过这个节点,无线网络中的其它类型节点可以和无线网络外部以及内部进行通信。这里,AP和无线路由都在一台设备上(即Cisco E3000)。 (*)Station(工作站):表示连接到无线网络中的设备,这些设备通过AP,可以和内部其它设备或者无线网络外部通信。 (*)Assosiate:连接。如果一个Station想要加入到无线网络中,需要和这个无线网络中的AP关联(即Assosiate)。 (*)SSID:用来标识一个无线网络,后面会详细介绍,我们这里只需了解,每个无线网络都有它自己的SSID。 (*)BSSID:用来标识一个BSS,其格式和MAC地址一样,是48位的地址格式。一般来说,它就是所处的无线接入点的MAC地址。某种程度来说,它的作用和SSID类似,但是SSID是网络的名字,是给人看的,BSSID是给机器看的,BSSID类似MAC地址。 (*)BSS(Basic Service Set):由一组相互通信的工作站组成,是802.11无线网络的基本组件。主要有两种类型的IBSS和基础结构型网络。IBSS又叫ADHOC,组网是临时的,通信方式为Station<->Station,这里不关注这种组网方式;我们关注的基础结构形网络,其通信方式是Station<->AP<->Station,也就是所有无线网络中的设备要想通信,都得经过AP。在无线网络的基础形网络中,最重要的两类设备:AP和Station。 (*)DS(Distributed System):即分布式系统。分布式系统属于802.11逻辑组件,负责将帧转发至目的地址,802.11并未规定其技术细节,大多数商业产品以桥接引擎合分步式系统媒介共同构成分布式系统。分步式系统是接入点之间转发帧的骨干网络,一般是以太网。其实,骨干网络并不是分步系统的全部,而是其媒介。主要有三点:骨干网(例如以太网)、桥接器(具有有线无线两个网络接口的接入点包含它)、属于骨干网上的接入点所管辖的基础性网络的station通信(和外界或者BSS内部的station)必须经过DS、而外部路由只知道station的mac地址,所以也需要通过分布式系统才能知道station的具体位置并且正确送到。分步式系统中的接入点之间必须相互传递与之关联的工作站的信息,这样整个分步式系统才能知道哪个station和哪个ap关联,保证分步式系统正常工作(即转达给正确的station)。分步式系统也可以是使用无线媒介(WDS),不一定一定是以太网。总之,分步式系统骨干网络(例如以太网)做为媒介,连接各个接入点,每个接入点与其内的station可构成BSS,各个接入点中的桥接控制器有到达骨干网络和其内部BSS无线网的接口(类似两个MAC地址),station通信需要通过分布式系统。 二、实践基础 ============================ 1、一些参数 (*)MAC MAC(即Medium/MediaAccess Control, 介质访问控制),是数据链路层的一部分。MAC地址是烧录在NetworkInterfaceCard(即网卡,简称NIC)里的,它也叫硬件地址,是由48位(即bit,一字节为8位,即1byte=8bits)16进制的数字组成。其中0-23位叫做组织唯一标志符(organizationally unique,简称OUI),是识别LAN(局域网)节点的标识(在有些抓包工具抓包的时候会将前三个字节映射成某种组织名称的字符,也可以选择不显示这种映射)。24-47位是由厂家自己分配。 (*)SSID 表示一个子网的名字,无线路由通过这个名字可以为其它设备标识这个无线路由的子网。设备进行扫描的时候,就会将相应SSID扫描到,然后就能够选择相应的SSID连接到相应的无线网络(当然不扫描,理论上也可以直接指定自己事先已经知道的ssid进行连接)。SSID可以和其它的重复,这样扫描的时候会看到两个同样SSID的无线网络,其实这一般用于将一个无线网络扩大的情况(毕竟无线路由器无线信号的覆盖范围是有线的):当想要扩大一个无线网络(即SSID固定)的范围的时候,可以给多个路由设置相同的SSID来达到这个目的。(这也是漫游的原理,漫游的时候,我们可以在远方或者本地都能够打电话,也就是访问移动通信网络)。 SSID和BSSID不一定一一对应,一个BSSID在不同的Channel上面可能会对应到多个SSID,但是它们在一个Channel是一一对应的;另外,漫游的时候,虽然SSID不变,但是BSSID一定是会变化的。我们经常可以看到实际数据包中的AP的MAC地址和BSSID只差几位,其实实际设备的MAC地址可能只有一个,和BSSID没什么对应关系。在一个包含了路由功能和AP功能的无线路由器(Fat AP)上面,很可能是:路由器有两个MAC地址,一个用于外网(WAN),一个用于内网(WLAN和LAN),一般路由器上面或者配置路由器的网页上面只标注外网的MAC地址;内网的MAC地址和外网MAC地址一般只有几位不同(甚至连续,也有些相差很多的例外)。 (*)Band(频率范围) 一般ap可以支持5g或2.4g两个频率范围段的无线信号。如果两者同时可以设置,而不是互斥那么,这个路由器还能够同时支持两种频段(频段即Band),这相当于这个ap可建立两个无线网络,它们采用不同的频段(这类似收音机在长波范围内收音和短波范围内收音)。 (*)Channel(信道) Channel是对频段的进一步划分(将5G或者2.4G的频段范围再划分为几个小的频段,每个频段称作一个Channel),有”5.18GHZ“,“Auto(DFS)”等等,处于不同传输信道上面的数据,如果信道覆盖范围没有重叠,那么不会相互干扰。对于信道的使用,在国际上有所规定。其中有些信道是无需授权即可直接使用的(究竟是那个频段的那个信道,依照各个国家而不同),无需授权使用的意思是,传输数据的时候(无论以哪种无线方式),可以让设备收发的功率导致传输时的数据进入该信道的频率并在该信道所在频段宽度内进行传输;授权的使用的意思是,不允许传输时使用授权信道进行,否则会违反规定,并且干扰该信道上其他数据的传输。另外,除了wifi,微波、红外线、蓝牙(使用802.15协议)的工作频段也都有在2.4gHZ范围内的,所以,它们传输的时候会对wifi传输造成干扰,因为两者在不同的协议下进行通信,所以互相将对方传输的信号识别为噪声。有时候配置AP的时候,Channel中有一个类似“Auto”的选项值,这表示打开AP的时候,AP自己Scan周围的环境,选择一个干扰最小的Channel来进行通信,当选择好了一个Channel的时候,一般就不会改变了。 (*)Channel Width(信道宽度) 这里的Channel Width是信道的带宽,有”20M HZ“、”40M HZ“等,它表示一个Channel片段的宽度(假设5g的频段宽度总共为100M,平均划分为互不干扰的10个Channel,那么每个Channel的Channel Width就为100M/10=10M,实际Channel并不一定是完全不重叠的)。这个参数可能依赖于一些其它的选项,例如不是802.11N的协议,就可能不会有40M HZ的Channel Width(N模式有一个特点就是可以把两个Channel合并,通过提高ChannelWidth来提高吞吐量)。例如选择了"20M HZ"这个Channel Width之后,后面再选择一个“5.18GHZ”的Channel,则表示以5.18GHZ为中心的前"10M HZ"以及其后面的"10M HZ"频带范围被占用。 至此可知,配置无线AP的时候,如果屋子里面有很多的AP(也就是无线路由接入点)的话,仔细设置它们的Channel Width和Channel可以保证它们相互之间的干扰(类似收音机里面的串台)尽可能小。当然,如果相互干扰了,那么Net Mode所指定的协议也会有相应的处理方式让他们之间进行协调(例如让谁先通信谁等一会再通信之类的),但是这样网络的性能就不如没有干扰的时候好了。 (*)Wireless Security(无线网络的安全性) 这里主要涉及WEP、WPA、WPA2和RC4、TKIP、AES。 IEEE 802.11 所制定的是技术性标准 ,Wi-Fi 联盟所制定的是商业化标准 , 而 Wi-Fi 所制定的商业化标准基本上也都符合 IEEE 所制定的技术性标准。WEP 是1999年9月通过的 IEEE 802.11 标准的一部分;WPA(Wi-Fi Protected Access) 事实上就是由 Wi-Fi 联盟所制定的安全性标准 , 这个商业化标准存在的目的就是为了要支持 IEEE 802.11i 这个以技术为导向的安全性标准;而 WPA2 其实就是 WPA 的第二个版本。直观点说,WEP是较老的认证方法它有好几个弱点,因此在2003年被WPA淘汰,WPA又在2004年由完整的 IEEE 802.11i 标准(又称为 WPA2)所取代。 WEP(Wired Equivalent Privacy),采用名为RC4的RSA加密技术;WPA(Wi-Fi Protected Access) ,采用新的TKIP算法,TKIP算法保留了RC4所以也有其弱点,但是这个时候更好的CCMP还没完成,所以先在WPA上用TKIP技术;WPA2是WPA的第2个版本,采用CCMP加密协定(在有些路由器等设备上设定加密协定或者加密算法的时候,可能会用类似AES之类的字眼替代CCMP)。所以WPA2+AES是安全性最强的。 另外,在有些无线网路设备的参数中会看到像 WPA-Enterprise / WPA2-Enterprise 以及 WPA-Personal / WPA2-Personal 的字眼 , 其实 WPA-Enterprise / WPA2-Enterprise 就是 WPA / WPA2 ; WPA-Personal / WPA2-Personal 其实就是 WPA-PSK / WPA2-PSK, 也就是以 ”pre-share key” 或 ” passphrase” 的验证 (authentication) 模式来代替 IEEE 802.1X/EAP 的验证模式 ,PSK 模式下不须使用验证服务器 ( 例如 RADIUS Server), 所以特别适合家用或 SOHO 的使用者。 还有,wep是旧的加密方式,工作于802.11B/G模式下而802.11N草案并不支持此加密方式,所以如果802.11N的设备采用wep加密方式后,它也只会工作在802.11b/g模式下,N的性能发挥不出来。 实际中,在有些路由器上面,设置的时候,可能不是严格按照这个规定来设置的(例如设定了采用WPA方式,还可以选择AES),但是大体一样。 (*)Region(区域) 一般在无线网络中的AP上都有一个参数,表明它是处于哪个Region(地区)。Station根据AP中设置的Region调整其相应的发射功率以遵守该地区的规定。AP的调整过程一般都是手动设定,设置好AP所处的Region之后,这些信息就会在AP发送的Beacon帧(后面会说到)中包含了;通过这个AP连接到无线网络上的Station,从Beacon帧中了解到这些Region信息,并且根据这些信息中的规定和AP进行通信。如果AP开始设置错了,那么Station和AP通信的时候,采用的将会是不符合Region规定的频段,可能会对该Region中的其它传输网络造成干扰,这应当是“非法”的。 (*)Transmission Rate 设置传输速率。这里采用不同的无线网络传输协议(802.11a,802.11b,802.11g等),那么可以设置的速率范围有所不同,这里的速度是指理论的速度,实际中,由于各种干扰因素,传输的速率可能会比设置的小。 一般而言,在无线网络中,对于某种协议的性能进行描述时,我们需要注意的是,描述时提到的传输速率(Datarate)和吞吐量( Throughput)是不同的。Datarate是理论上面最大数据传输速率,而Throughput是数据的实际最大吞吐量。因为厂家以及传输时所使用的协议等各种因素造成的开销,会导致实际吞吐量比理论吞吐量要小,一般实际最大吞吐为理论最大的50%左右(一个不太准确但是相对直观的估计:在网络中,高清视频所需的Throughput也就30mbps左右,网络上一般的视频也就4mbps左右)。 (*)Qos(质量保证) 无线网络中的QOS是质量保证,大致的意思是,传输数据的时候,考虑各种因素(例如收费策略,所处地区等),以一定的优先级来保证传输的特定要求(一般就是速度),如果带宽足够的话,QOS反而不需要了。 (*)RTS Threshold / CTS Protection Mode: 这里的RTS是Request-To-Send的简写,CTS是Clear-To-Send的简写。设置好RTS的阈值之后,如果超过这个阈值就会在发送信息之前先发送RTS,以减少干扰,相应的CTS会回应之前的RTS。一般都是AP发送CTS数据,而Station发送RTS数据。 这里对RTS和CTS做一个简单解释:假设在同一个AP所覆盖的无线网络范围内的两个Station A和B,它们之间可能会因为距离的原因互相不可见(例如它们在AP网络范围的两端,而这两端的距离大于两者的信号覆盖范围),但是AP却知道它们是在自己的范围内。当一个A想要在AP的网络中进行通信的时候,必定要经过AP转发它的信息,由于A不知道B的存在,所以如果同时B也通过AP进行网络通信,那么会出现AP同时收到A、B两个Station的通信请求,而这在无线网络中是不允许的(无线网络中,同一时刻不能有多个人传输数据)。在这种情况下,B和A互相干扰了对方的通信,但是却互相不可见(不可见的节点互相被称作隐藏节点)。如果在一个网络中,这样的隐藏节点很多,那么势必会影响网络的性能(因为数据一旦发送失败,就要重传,隐藏节点会导致重传的机率增大)。这个时候,可采用RTS和CTS机制。即:在A想要通信的时候,先广播发送RTS给AP,告诉AP“它想要通信”,同时接受到RTS的别的Station(它们对发送RTS的Station而言可见)会知道A将要发送数据,于是它们不会发送数据以免干扰A;AP收到RTS之后,会广播发送CTS,告诉所有在AP范围内的Station(包括对A而言的隐藏节点B)”A将要通信(同时也相当于告诉A,A可以无干扰的发送信息了)”,这样对A而言的隐藏节点B也知道有一个A的存在并且要发送信息了,于是B就不会干扰A了。 这里,A和B两者可以在不同的网络上,也就是说,不同网络的工作站之间也可以通过RTS/CTS来清除相互的干扰。 (*)Beacon Interval: 表示无线路由定期广播其SSID的时间间隔。这个一般不会特别设置,就采用默认值即可。如果不广播了,那么Station端扫描的时候可能会发现不定期广播的AP对应的SSID的网络不见了,所以可能会断开连接。这里定期广播,表示AP会定时向其范围内广播SSID的信息,以表示AP的存在,这样Station进入一个区域之后,就能够通过扫描知道这个区域是否有AP的存在。当然,除了AP广播SSID以告知其无线网络存在之外,Station也可主动广播探寻包,在其能够覆盖的范围内询问是否有AP存在(即我们通常所说的扫描寻找接入点)。 (*)DTIM Interval: DTIM/TIM表示告诉Station,AP在为Station做package buffer(例如Station睡眠的时候)的缓存时间。为了节省电池使用时间,处于无线网络中的Station可能会在一定时间之后自动进入休眠状态。这个时候,AP会为这个Station缓存发送给它的数据,而处于休眠状态的Station只会在一定时间间隔内给AP发送一个数据帧,以确认是否有发送给自己的数据存在。例如,当我们在主机上ping另外一台睡眠的机器的时候,收到另外一台机器响应的时间,要比它不睡眠的时候响应的时间长很多。 (*)Fragmentation Threshold: 表示一个package的分片阈值。我们可以设置分片大小,当发送的数据包超过这个阈值之后,802.11协议会自动对这个数据包进行分割。如果设置的这个分片值越小,那么整个数据包越容易传输成功(因为如果出错,那么只需要传送一个片段而不是整个包,无线wifi网络中数据传输时出错的概率比有线的以太网要大的多的多),当然开销也越大(因为需要额外的信息标记每个分片,以及各个分片传输成功之后涉及到的重组问题)。 一般来说,我们的机器上面的软件抓取无线网卡上面的包的时候,其实这些包的目标地址都是这个机器的无线网卡,因为不是发给这个机器无线网卡的包都被网卡过滤了。所以如果我们想要抓取所处无线网络环境下所有的包的时候,需要给机器配备一种特殊的设备(sniffer就是嗅探器),然后再通过抓包工具抓取并分析。有一个硬件设备叫做AirPcap,就是做这个用的,大有几百到上千美金,它可以同时做为嗅探器或者无线网卡使用,不过做为嗅探器的时候,会抓取所有经过它的包。这个工具目前只有Windows上面的驱动,所以使用这个工具,只能在Windows上面,配合Wireshark抓包软件进行抓包。 这里假设采用AirPcap嗅探,Wireshark软件抓包(其它抓包软件,例如linux下面的tcpdump等分析类似)。不用图形方式详细展示具体的抓包过程以及分析方法了,主要说一下抓包(这里的包实际主要指的是网络层以下的包,更常见的称呼应该是数据帧)时候需要注意的问题。 (*)Wireshark展示包的时候,大致都是按照协议规定的字段展示,也些地方按照它自己特定的方式展示。因为这里着重讲述一些抓包时注意的基本原理上面的东西,所以不会对此进行过多阐述。大致就是:Wireshark软件中,对包展示的时候,按照协议规定的字段分别用Header和Body两个部分展示;另外,在Header之前还有两个部分是Wireshark为方便用户而展示的包的大小、时间等全局信息(例如见过表示这个包在B和G mode中的Channel 1时,用"BG1"表示)。所以,其实我们分析的时候,实际应该按照后面的Header和Body两个部分进行。 后面将基于以上所述,进行进一步的讲解。 (*)抓包的时候,需要首先确认这个包是否是完整、正确的包。只要是校验位(checksum)不对的,就是错误的包,也无法确定接收的时候那里出了差错,所以这个包是应该忽略的,几乎没有分析的价值。另外,抓包的时候,由于干扰等原因,抓取的内容可能不是在实际传输所处的Channel上的包(例如在Channel 1上面嗅探,却嗅探到了Channel 2上的包)。 (*)抓取授权阶段的包,需要注意实际的授权是在后面进行的。Authentication的时候,开始阶段实际是Open的(即无授权),也就是说,开始实际已经建立好了连接,所以我们在抓包的时候,开始看到的一般都是通过验证,但是在后面紧接着采用了类似802.11x等安全加强的协议,来进行再次鉴权认证,如果这里无法通过则立即将已经建立的Association断开。这样的机制,是因为原来的802.11没有充分考虑安全才会这样的,这样也兼容了以前的802.11。 (*)抓取的包的数据,要注意这个包是否是被加过密的。根据协议标准的描述,包中如果有dataprotected字段,则表示这个数据本身是被加了密的,不知道这个数据具体是什么,当然,如果有密码,wireshark也有一个可以按照这个密码解密的工具,有时候不好用。这里所说的数据加密和网络的加密不一样,可能访问网络本身是需要密码(网络是security的),而数据本身没有crpted(加密)。对于一个加了密的数据包,我们一般看不出来这个包到底是做什么用的或者什么类型的等等。 (*)抓包的时候,要注意包中指示的源和目的地址以及包的序号。在无线网络中通信的时候,我们抓包的时候可能会看到被抓取的包对应AP的MAC地址是不存在的,其实抓包时AP的MAC是BSSID,它和实际标注的MAC地址不一定一样(但是一般都差不多,也就是之后最后面的几位不一样)。有时候,我们看到抓取的包中的MAC地址有许多只相差几位,那么可能它们都属于一个设备(因为虽然设备可能只标注了一个网卡的MAC地址,但是它却“虚拟”出或者实际有多个MAC地址),所以当我们看到包中对应两个AP的MAC地址几乎一样的时候,一般来说,这两个MAC地址很可能就是一个设备的。还有在抓包的时候,一个地址上面的包的sequence(序号)是连续的,除非丢包了导致重复或者缺失。如果一个设备虚拟出来两个地址,那么也可能由于没有经过什么处理,导致这两个地址上面的包共同起来是连续的(如前所述,这两个地址和MAC很接近,应该是BSSID)。 (*)抓取的数据帧如果是广播帧则不需要确认(ACK),如果是单播帧,则一般需要确认(ACK)。例如,Probe帧是广播帧,所以它无对应的ACK确认帧,对Probe的回复则叫做Probe Response;注意ACK帧本身用于确认,是单播的,但是它本身却不需要再被确认了。从包中的目的MAC地址中,可以看出这个包是广播/多播帧还是单播帧。MAC第一个字节的第一个位是1,表示组播,前两位是1表示广播,第一个字节第一个位是0表示单播。这里注意,MAC不是值,而是一个Pattern,所以没有Endian之说,也没有那个位高,那个MAC大之说。例如:“a8:27:26:....:b7”,这里第一个字节就是a8(10101000),其第一个字节的第一位就是8的最“右”位,即“0”,所以它的第一个字节的第一个位是0,是一个单播地址。其实,这里涉及到大端小端问题,后面也会讲到,总之,以太网线路上按“Big Endian”字节序传送报文(也就是最高字节先传送),而比特序是”Little Endian”(也就是字节内最低位先传送)所以,一个十六进制表示法表示的MAC地址01-80-C2-00-00-00,传送时的bit顺序就是:1000 0000 0000 0001 0100 0011 0000 0000 0000 0000 0000 0000。 (*)使用Wire Shark在抓包或者显示包的时候,都可以设置过滤器(filter)。抓包时候设置的过滤器叫做capture filter,它是用BPF(berkerley package filter)这个比较通用的语言来描述(注意这不是Wireshark专用的filter语言,而是一个通用的语言)。但是抓包期间的过滤,有时候不准,所以我们一般先将所有的包抓取下来,然后用WireShark中显示的过滤器(即view filter)来显示我们关注的包,这里我们可以用macro来定义比较复杂的显示过滤条件。保存的时候,可以用按照显示过滤还是抓取过滤的方式保存内容。 (*)尽量不要抓取Channel Width为40MHZ的Channel上的帧。我们还需要注意的是,使用Sniffer抓取无线网络包的时候,AirPcap无法正常抓取40MHZ Channel Width的包,或者说对抓取这个Channel Width上面的包支持不好。如果非要抓取40MHZ Channel Width的包,那么就在40或者36号Channel上面进行抓取,并在Wireshark上面设置“channel=36,offset+1”(平时offset都是0),这样能够抓取 Channel Width为40MHZ的包(但是,其他Channel上面的40mHZ的包还是无法抓取),这是由AirPcap内部的芯片固件的问题决定的(估计broad com芯片公司也不愿花过多的精力来支持这个很少有人用的抓包工具的这个功能)。 另外,假设一个无线工作站是基于Android系统的(例如智能手机或者平板电子书)那么我们可以利用“wpa_cli status”命令来可以查看当前设备的连接的SSID,BSSID,MAC,IP等信息,(这里“cli”=“command line interface”)。 还有更“复杂”的命令“wc”和“wl”,其中wc是比较上层的命令,wl是下层的命令(是基于芯片是否支持的,例如wl在broadcom芯片上支持,但是在ti上面就没有了)。 三、一些原理 ============================ 1、常见的帧 在802.11中的帧有三种类型:管理帧(Management Frame,例如Beacon帧、Association帧)、控制帧(Control Frame,例如RTS帧、CTS帧、ACK帧)、数据帧(Data Frame,承载数据的载体,其中的DS字段用来标识方向很重要)。帧头部中的类型字段中会标识出该帧属于哪个字段。 (*)ACK帧 单播(unicast)帧都需要用ACK来确认,ACK本身不是广播帧,ACK在MAC上是unicast的,帧中有receive地址字段(用来标识是对谁的确认),但是它却不需要再确认了。ACK只有接收地址(receive)而无源地址(src)和序号(sequence),因为发送和接受是一个整体,发送之后,其他人(除了这个发送的接受者)都不会再发送数据了(无线协议中的冲突避免机制),所以接受者会发送一个没有src的ack帧给receiver,而接收ACK的一端会根据这个知道它收到了一个ACK帧(其实根据协议,应当把发送单播帧和收到它相应的ACK看作一个原子的不可分割的整体,表示一次成功的通信)。 (*)Beacon帧 Beacon帧定时广播发送,主要用来通知网络AP的存在性。Station和AP建立Association的时候,也需要用到Beacon。Station可以通过Scan来扫描到Beacon,从而得知AP的存在,也可以在扫描的时候通过主动发送Probe来探寻AP是否存在。也就是说,建立Association的时候有主动的扫描或者被动的扫描两种方式。另外,Beacon还包含了关于Power Save、以及地区等信息。 (*)Association帧 通常Association帧都有Probe Request和相应的Probe Response。Association的Request中有其所需要的Channel以及Data Rate等状态,以便让AP决定是否让它与自己建立Association。而关联是否成功,主要是看Response中的Status code是否为Success。 (*)Data帧 Data Frame具有方向,这个方向用DS(分布式系统)字段来标识,以区分不同类型帧中关于地址的解析方式;其它的类型Frame例如Control Frame或者管理帧中,这个字段是全零。这个字段用两位表示,这两个位的含义分别表示“To Ds”和“From Ds”,大致含义如下: (a)To DS:表示Station->AP,一般也叫Upload。 (b)From DS表示AP->Station,一般也叫Download。 这里,我们可以大致将DS看做AP,To/From是从AP的角度来考虑的。To DS就是让AP干活。另外Data Frame中还有一个比较重要的字段就是Sequence,表示帧的序号。重传帧序号一样,但是多了一个Retry的字段表示该帧是重传的。 为了便于理解,这里再次详细解释一下DS字段的含义: To DS=0,From DS=0:表示Station之间的AD Hoc类似的通信,或者控制侦、管理侦。 To DS=0,From DS=1:Station接收的侦。 To DS=1,From DS = 0:Station发送的侦。 To DS=1,From DS = 1:无线桥接器上的数据侦。 这里,我们主要关注To DS和From DS分别是01和10的情况,DS虽然大致等于AP但是它不是AP,它其实是一个系统,从Station的角度来看,比较容易理解。并且To DS和From DS一定是无线网络上面数据侦才有的字段。 2、帧和大端小端 Ethernet和802.11都是按照Little Endian的方式来传输数据,也就是说,而MAC层传输的时候,是采用Little Endian的方式,一个字节一个字节的传输的,前面的低位字节先传输,后面的高位字节后传输(传输单位不是按位而是字节);在协议标准上描述一个帧的时候,一般是先按照Little Endian的方式对其进行总体描述,然后具体细节说每个字段的值,这时候这个字段值是Big Endian方式表示的,这一点应当注意。 例如,协议标准中可能能对某个帧格式做如下的描述: |b0|b1|b2|b3|b4|b5|b6|b7|b8|b9|...|...| 这里,最低位b0在最前面,所以这里采用的就是小端的方式来描述帧的总体格式信息。传输的时候,就按照这里的方式,以字节为单位向物理层进行传输(先传b0~b7然后b8~b16等等)。 但是,在解释这个帧的各个域的时候却采用大端的方式进行描述。假设b3=0,b2=1,b1=0,b0=0四者共同组成一个名字为“FLAG”的域,那么会有类似如下的描述: FLAG=4(即FLAG为0100):表示XXX。 所以,协议标准中具体描述某个域的时候,一般直接用大端方式表示的数值(b3b2b1b0=0100)来描述;而传输数据帧或者在协议标准中描述整体帧的时候,中给出的却是小端的方式(b0b1b2b3=0010)。 这里的每个字段都是帧的一个部分,在管理帧(后面会说)中长度不固定的部分又叫IE(information Element) 。 另外注意,内存地址是用来标记每个字节的而不是位,所以内存里面大端小端也是以字节而不是位为单位的(前面描述“大端“、”小端”的时候却以位序而非字节序,这一点需要明辨,不要混淆)。假设奔腾的机器,CPU为32位,采用Little Endian方式,那么表示1这个int类型整数的时候,假设它在数值上是十六进制的"00000001",那么存放在内存中却是由低位到高位依次存放的,由低到高地址依次为:"01"、"00"、"00"、"00"(也就是说小端方式存放在内存中的时候,是按照含有最低位的字节存放在低地址,注意是字节,在内存中“位”没有地址,所以没有大端小端一说)。在传递帧的时候,也是按照一个字节一个字节的传输,而一个字节内部在实际上其实没有什么端的分别,但是wireshark一律使用“b7b6b5b4b3b2b1b0”这样的方式来用大端的方式显示。 总之,需要注意网络层下面的帧的大端小端问题(不是网络中的字节序,TCP/IP中规定的网络字节序是Big Endian),大致就是:协议规定,传输的时候使用Little Endian;标准描述的时候用Big Endian和Little Endian都用;另外,Wire shark软件抓的包中,好象全都用Big Endian来进行标示(无论是信息窗口还是内存窗口都这样展示)。 3、CSMA/CA的机制 与以太网的CSMA/CD机制(冲突检测)相对,802.11采用的CSMA/CA机制(冲突避免)。采用这个机制,可以保证每次通信的原子性(即每次通信所需要传输的多种不同类型的帧之间没有夹杂其它通信的帧的干扰),大体过程是: (a)链路空闲下来之后,所有Station在发送帧之前都首先等待一段时间(即DIFS,又称帧间隔时间); (b)到达DIFS之后,所有的Station进入竞争时间窗口(就是竞争期间),将这个竞争时间窗口分割成多个Slot(退避时间间隔),然后每个Station随机选择一个Slot; (c)当某个Station到达它的Slot对应的时间之后,就开始发送数据。这里,选择的Slot越靠前,则表示Station在DIFS之后再等待的时间(退避时间)越短,也就会越早发送实际数据; (d)退避窗口的Slot有多个,选择的时候,可能某个Slot被多个站点同时选取,这个时候发送会产生真正的数据冲突(如果多个人同时发送,那么它们都要经过AP来转发,AP无法同时听见多个人的“说话声音”)那么Station就会再重新选择并发送; (e)当一个Station发送数据之后,所有Station会检测到链路忙,于是放弃尝试发送,等那个Station发送完数据之后,链路开始空闲,于是又进入到(a)重新开始这个过程。 对于以上的机制,如果我们让某个Station经过DIFS之后,选择的Slot越小,就意味着它发送帧的机会越大,也就是说这个Station的优先权越高。这就是Qos(质量保证)的基本,前面也说过,Qos就是“以一定的优先级来保证传输的特定要求”,要获得这种优先级,就要有相应的条件(例如“花钱”)(有一种不常用的无竞争发送,其实就是DIFS之后,不退避而直接发送)。 另外,其实对物理层上来说,所有的发送都是广播,单播与否只是在链路层以上分辨的。上面提到的检测链路是否忙,可以从链路上用软件方式进行(例如增加帧的特殊字段),也可以直接在物理层上进行,实际因为在物理层上成本较高,经常用的是前者,具体参见协议。软件检测大致的思路就是,进行一个通信的时候,这个通信包含多个帧,每个帧有不同的作用,发送的第一帧的时候,会通过其中的某个特殊字段(Duration字段,也叫NAV,即网络分配向量,是一个延迟时间值)告诉所有其它Station,在未来的一段时间内,链路被占用,以完成整个通信过程。这样,其它Station在此期间就不会发送数据干扰这次通信了,以后这个通信的每一帧以及其ACK确认帧之间都会有一个很小的时间间隔(小于DIFS,即SIFS),并且每帧会视情况延长那个Duration字段,保证整个通信期间确实不会有其它人干扰,这样整个通信就是原子性的了。 4、帧的来源和目的地址 因为无线网络中没有采用有线电缆而是采用无线电波做为传输介质,所以需要将其网络层以下的帧格式封装的更复杂,才能像在有线网络那样传输数据。其中,仅从标识帧的来源和去向方面,无线网络中的帧就需要有四个地址,而不像以太网那样简单只有有两个地址(源和目的)。这四个地址分别是: SRC:源地址(SA),和以太网中的一样,就是发帧的最初地址,在以太网和wifi中帧格式转换的时候,互相可以直接复制。 DST:目的地址(DA),和以太网中的一样,就是最终接受数据帧的地址,在以太网和wifi中帧格式转换的时候,互相可以直接复制。 TX:也就是Transmiter(TA),表示无线网络中目前实际发送帧者的地址(可能是最初发帧的人,也可能是转发时候的路由)。 RX:也就是Receiver(RA),表示无线网络中,目前实际接收帧者的地址(可能是最终的接收者,也可能是接收帧以便转发给接收者的ap)。 注意,其实,还有一个BSSID,用来区分不同网络的标识。在802.11帧中,有四个地址字段,一般只用到其中的三个,并且,这四个字段对应哪种地址或者使用哪些地址,根据帧中的另外一个DS字段以及帧的类型而有不同的解释。 (1)无线网络中的Station和以太网中的Host进行通信: Station<- - - - ->AP<---------->Host a)当Station->Host的时候: 首先Station->AP,这时候Src=Station,Dst=Host,Tx=Station,Rx=AP,然后AP->Host,这时候Src=Station,Dst=Host,因为AP转发的时候,是在以太网中,所以没有Tx和Rx。 b)当Host->Station的时候: 首先Host->AP,这时候Src=Host,Dst=Station,然后AP->Station,这时候,Src=Host,Dst=Station,Tx=AP,Rx=Station。 (2)无线网络中的Station之间进行通信: Station1<- - - - ->AP<- - - - ->Station2 a)当Station1->Station2时 首先Station1->AP,Src=Station1,Dst=Station2,Tx=Station1,Rx=AP,然后AP->Station2,Src=Station1, Dst=Station2, Tx=AP, Rx=Station2。 可见,在无线网络中,始终存在Tx和Rx,但是,这四个地址中还是只有三个地址足矣。 (3)当两个无线网络中的Station进行通信的时候: Station1<- - - - ->AP1<- - - - ->AP2<- - - - - ->Station2 当Station1->Station2时: 首先Station1->AP1,Src=Station,Dst=Station2,Tx=Station1,Rx=AP1,然后AP1->AP2,Src=Station, Dst=Station2, Tx=AP1, Rx=AP2,然后AP2->Station2,Src=Station1,Dst=Station2,Tx=AP2,Rx=Station2。 注意,这个时候,AP起到桥接的作用,所以四个地址各不相同,同时,AP之间或者Station和AP之间的那部分连接,也可以是以太网。 综上可知,无线网络中的Station想要通信,必须经过AP来进行“转发”,其实,Tx和Rx是无线网络中的发和收,也就是Radio;而Src和Dst是真正的发送源和接收者。 5、Sleep和Power save(节电) 其实,无线网络中的Power save是指Station的Sleep(睡眠),并且这个Sleep并不是整个系统的Sleep,确切来说,应该是其wifi中Receiver(接收天线)的Sleep。Station在睡眠的期间还是可以Transmit(发送)的,只是当AP知道Station的Receiver处于Sleep状态时,就不会给Station发送帧了。Station在Sleep之前,会给AP发送一个特殊的帧,告诉AP说它(Station)要睡眠了,AP通过这个帧来记住是这个Station睡眠了,然后AP就不会给这个Station单独发送数据了。 当有和这个Station通信的包想通过AP转达的给这个Station时候,AP会帮这个Station将它们缓存起来,然后在Beacon广播帧中添加一个特殊的位(实际这个位是一个bitmap中的位,这个bitmap表示所有和该AP建立了关联的Station,而这个睡眠的Station的相应位为被置1则表示有消息要传达给这个Station),来表示这个Station有数据到达了(Beacon是定时广播的帧,前面说过它是用来通知无线网络,这个AP的状态),而不是直接发送给Station。而这个睡眠的Station,会在睡眠期间不时地醒来,以检查Beacon帧中的状态,当发现有给它的数据的时候,就会通过发送一个Power Poll的帧来收取数据,收取之后继续睡眠(所以ping一个睡眠状态的Station,响应的时间要慢好多)。 对于发送给这个Station的广播帧,其处理方式和普通帧有一点不同:当有广播帧要传达给这个Station的时候,AP会为这个Station缓存发送给它的广播帧,但是缓存的时间是DTIM(一般为300ms)。注意:单播帧缓存的时间不一定是多少,广播帧却缓存DTIM的时间。AP每发送一个Beacon的时候,都会将Dtim减少1,而Station睡眠的时候,会不时地醒来,查看一下Beacon帧中的dtim值。当Station发现其DTIM值变成0的时候,就醒来长一些的时间,看看有没有广播给它的数据,如果有的话就用类似Power Save Poll的帧接受,没有则继续睡眠。 这里,接收数据是根据是否有more data类似的字段来确认是否有更多的数据的;重发的帧是用类似retry的字段来标记。另外注意,当Station进行Sleep的时候,还是可以主动Tranmit消息的,当Station主动Transmit消息的时候,它会等待Reply,所以这个时候,Receiver是on的状态。用一个图示来标识Sleep,Receive,Transmit时的电源消耗状况,大致如下: power(横线部分) trans | |-------------------| | | | receive | |-----------| | sleep |-------| |-------------------- |----------------------------------------------------------------------> time 可见不同状态,电源消耗状态不同(传送比接收更耗电),另外,如果电源供电不足,在某个状态中就会出现通信失败的情况。(好像ap上面broadcom芯片中的睡眠之后,醒来立即重新发送的时候经常开始会失败,可能就是这个原因)。 6、建立Association 下面是Station和Ap建立开放Association的过程: (0)Ap周期性地广播Beacon帧 (1)Station广播Probe Request到达Ap (2)Ap向Station发送Probe Reponse (3)Station向Ap发送ACK (4)Station向Ap发送Authentication Request (5)Ap向Station发送ACK (6)Ap向Station发送Authentication Reponse (7)Station向Ap发送ACK (8)Station向Ap发送Association Request (9)Ap向Station发送ACK (10)Ap向Station发送Association Reponse (11)Station向Ap发送ACK (12)Station和Ap开始相互通信。 可见,广播帧不用回复,单播帧需要用ACK确认,ACK本身不用被确认。
•Pull on the Jumper connecting JP4.2&1(HOST0), JP3.2&1(HOST1) to make SP241/SP242 board in USB bootstrap mode. Pull on the Jumper connecting JP5.2&1(1.2VMOD), JP7.2&3(EJAG), JP31.2&3(IOT). Pull out the Jumper which connects JP26.1&2. 具体跳线如下: 2、逻辑事务处理 C/S架构合理地让客户端和服务器承担一部分逻辑事务处理,使得服务器的负担减轻了,而且客户端也能进行一些数据处理和存储的功能。B/S架构的浏览器就是它的客户端,可是这个客户端只能进行一些简单的输入输出和信息发布共享的功能,主要的逻辑事务处理还是要靠服务器,所以服务器的负担很重。 3、工作原理 C/S架构是客户端和服务器直接相连,实现点对点的通信,B/S是浏览器通过WEB服务器向数据库服务器发送数据请求,实现多对多的通信。 4、响应速度 C/S架构的客户端和服务器直接相连,中间没有任何阻隔,所以相应速度快,尤其是在用户增多时更加明显。B/S架构相应速度慢,主要的重任在数据库服务器身上,由于B/S架构的无限扩展性,当用户激增,访问量庞大时,服务器相应速度慢,服务器存在瘫痪的危险。 C/S架构开发时,硬件需要一次性购买,费用较高,且需要训练有素的技术人员,培训费用高,而且软件后期也需要不断投入大量资金。B/S架构只需要一次性投入几乎可以一劳永逸,有利于软件项目控制和IT黑洞。 6、维护、升级以及扩展 C/S架构一旦有业务的变更或要升级,客户端界面就要重新设计,需要投入大量的人力物力。软件维护也比较麻烦,需要专业人士进行维护。用户扩展也比较麻烦,需要安装客户端,对软硬件要求高。B/S架构的维护和升级都非常容易,只要更改页面内容或者增减页面即可,客户端几乎是零维护,只需要维护好服务器。所以相对来说更简易,方便。由于B/S可以随时随地的访问,所以极易扩展。 7、信息共享 C/S架构是建立在局域网之上的,面向的是可知的有限用户,信息共享只在小范围内。B/S架构建立在广域网之上,用户随时随地都可以访问,外部用户也可以访问,尤其是WEB技术的不断发展,B/S面对的是几乎无限的用户群体,所以信息共享性很强。 8、客户端界面 C/S架构可以针对不同的功能设计出不同的很有特色的用户界面,实现个性化。但是一旦业务改变就需要重新设计,很麻烦。B/S架构的用户界面很通用,不能针对用户突出个性,但是业务改变时只需要改变界面内容或者增减页面,很轻松就能实现。 2)小的差别 1、适用的网络 C/S架构是建立在局域网的基础之上的,局域网之间通过专用服务器提供连接提供服务。B/S架构是建立在广域网的基础之上,有更大的使用范围。 C/S架构下,用户需要安装客户端才能够访问服务器,而B/S架构下,用户可以随时随地访问,只要有网有浏览器,方便快捷。 3、数据库连接类型 C/S采用的是ODBC连接,所以只要用户连接了数据库就一直保持连接不会断开,所以限制了用户数,而B/S采用的是JDBC连接,用户并不保持对数据库的连接,所以用户数几乎是无限的。 C/S架构能够实现单一的复杂功能,如财政管理等,所以现在大多数比较大型的ERP系统仍是C/S架构,B/S架构的界面比较通用,所能处理的逻辑事务较少,所以功能较弱。 5、安全性 C/S架构建立在局域网之上,面向比较固定的用户,对安全的要求较高。B/S架构建立在广域网上,面对不可知人群,安全性差。 6、信息流向不同 C/S 程序一般是典型的中央集权的机械式处理, 交互性相对低B/S 信息 流向可变化, B-B B-C B-G等信息、流向的变化, 更像个交易中心。 7、对技术人员的要求 C/S的功能比较专业化,对设计和开发人员的要求较高,需要专业的培训。B/S的界面比较人性化,通用化,所以不需要多高的技能。 转自:百度文库
提出这种思路的人认为,计算机网络和电信网的一个重大区别就是终端设备的性能差别很大。电信网的终端是非常简单的、没有什么智能的电话机。因此电信网的不可靠必然会严重地影响人们利用电话的通信。但计算机网络的终端是有很多智能的主机。这样就使得计算机网络和电信网有两个重要区别。第一,即使传送数据的因特网有一些缺陷(如造成比特差错或分组丢失),但具有很多智能的终端主机仍然有办法实现可靠的数据传输(例如,能够及时发现差错并通知发送方重传刚才出错的数据)。第二,即使网络可以实现 100% 地无差错传输,端到端的数据传输仍然有可能出现差错。 为了说明这点,我们可以用一个简单例子来说明这个问题。这就是主机 A 向主机 B 传送一个文件的情况。 文件是通过一个文件系统存储在主机 A 的硬盘中。主机 B 也有一个文件系统,用来接收和存储从 A 发送过来的文件。应用层使用的应用程序现在就是文件传送程序,这个程序一部分在主机 A 运行,另一部分在主机 B 运行。 现在讨论文件传送的大致步骤: (1)主机 A 的文件传送程序调用文件系统将文件从硬盘中读出。然后文件系统将文件传递给文件传送程序。 (2)主机 A 请求数据通信系统将文件传送到主机 B。这里包括使用一些通信协议和将数据文件划分为适当大小的分组。 (3)通信网络将这些数据分组逐个传送给主机 B。 (4)在主机 B,数据通信协议将收到的数据传递给文件传送应用程序在主机 B 运行的那一部分。 (5)在主机 B,文件传送程序请求主机 B 的文件系统将收到的数据写到主机 B 的硬盘中。 (1)虽然文件原来是正确写在主机 A 的硬盘上,但在读出后就可能出现差错(如在磁盘存储系统中的硬件出现了故障)。 (2)文件系统、文件传送程序或数据通信系统的软件在对文件中的数据进行缓存或复制的过程中都有可能出现故障。 (3)主机 A 或 B 的硬件处理机或存储器在主机 A 或 B 进行数据缓存或复制的过程中也有可能出现故障。 (4)通信系统在传输数据分组时有可能产生检测不出来的比特差错或甚至丢失某些分组。 (5)主机 A 或 B 都有可能在进行数据处理的过程中突然崩溃。 由此可看出,即使对于这样一个简单的文件传送任务,仅仅使通信网络非常可靠并不能保证文件从主机 A 硬盘到主机 B 硬盘的传送是可靠的。也就是说,花费很多的钱将通信网络做成为非常可靠的,对传送计算机数据来说是得不偿失的。既然现在的终端设备有智能,就应当把网络设计得简单些,而让具有智能的终端来完成“使传输变得可靠”的任务。 于是,计算机网络的设计者采用了一种策略,这就是“端到端的可靠传输”。更具体些,就是在运输层使用面向连接的 TCP 协议,它可保证端到端的可靠传输。只要主机 B 的 TCP 发现了数据的传输有差错,就告诉主机 A 将出现差错的那部分数据重传,直到这部分数据正确传送到主机 B 为止。而 TCP 发现不了数据有差错的概率是很小的。采用这样的建网策略,既可以使网络部分价格便宜和灵活可靠,又能够保证端到端的可靠传输。 这样,我们可以这样想像,将因特网的范围稍微扩大一些,即扩大到主机中的运输层。由于运输层使用了 TCP 协议,使得端到端的数据传输成为可靠的,因此这样扩大了范围的因特网就成为可靠的网络。 因此,说“因特网提供的数据传输是不可靠的”或“因特网提供的数据传输是可靠的”这两种说法都可以在文献中找到,问题是是怎样界定因特网的范围。如果说因特网提供的数据传输是不可靠的,那么这里的因特网指的是不包括主机在内的网络(仅有下三层)。说因特网提供的数据传输是可靠的,就表明因特网的范围已经扩大到主机的运输层。再回到通过邮局寄平信的例子。当我们寄出一封平信后,可以等待收信人的确认(通过他的回信)。如果隔了一些日子还没有收到回信,我们可以将该信件再寄一次。这就是将“端到端的可靠传输”的原理用于寄信的例子。 参考网址:http://blog.csdn.net/shiniji_hh1126/article/details/5997671
答案是:面向连接和无连接指的都是协议。也就是说,这些术语指的并不是物理介质本身,而是用来说明如何在物理介质上传输数据的。面向连接和无连接协议可以,而且通常也确实会共享同一条物理介质。 如果两者的区别与承载数据的物理介质无关,又和什么有关呢?它们的本质区别在于,对无连接协议来说,每个分组的处理都独立于所有其他分组,而对面向连接的协议来说,协议实现则维护了与后继分组有关的状态信息。 无连接协议中的分组被称为数据报(datagram),每个分组都是独立寻址,并由应用程序发送的。从协议的角度来看,每个数据报都是一个独立的实体,与在两个相同的对等实体之间传送的任何其他数据报都没有关系,这就意味着协议很可能是不可靠的。也就是说,网络会尽最大努力传送每一个数据报,但并不保证数据报不丢失、不延迟或者不错序传输。 另一方面,面向连接的协议则维护了分组之间的状态,使用这种协议的应用程序通常都会进行长期的对话。记住这些状态,协议就可以提供可靠的传输。比如,发送端可以记住哪些数据已经发送出去了但还未被确认,以及数据是什么时候发送的。如果在某段时间间隔内没有收到确认,发送端可以重传数据。接收端可以记住已经收到了哪些数据,并将重复的数据丢弃。如果分组不是按序到达的,接收端可以将其保存下来,直到逻辑上先于它的分组到达为止。 典型的面向连接协议有三个阶段。第一阶段,在对等实体间建立连接。接下来是数据传输阶段,在这个阶段中,数据在对等实体间传输。最后,当对等实体完成数据传输时,连接被拆除。 一种标准的类比是:使用无连接协议就像寄信,而使用面向连接的协议就像打电话。 给朋友寄信时,每封信都是一个独立寻址且自包含的实体。邮局在处理这些信件时不会考虑到两个通信者之间的任何其他信件。邮局不会维护以往通信者的历史记录--也就是说,它不会维护信件之间的状态。邮局也不保证信件不丢失、不延迟、不错序。这种方式就对应于无连接协议发送数据报的方式。(用明信片进行类比会更合适一些,因为写错地址的信件会被退回发信人,而(和典型的无连接协议数据报一样)明信片则不会。) 现在来看看不是给朋友寄信,而是打电话时会发生些什么事情。 首先,拨朋友的号码来发起呼叫。朋友应答,会说“嗨”之类的话,然后我们回应:“嗨,Lucy。我是 Bob。”我们和朋友聊一会儿,然后互说再见并挂机。这是面向连接协议中发生的典型状况。在连接建立阶段,一端与其对等实体联系,交换初始问候信息,对会话中要用到的一些参数和选项进行沟通,然后连接进入数据传输阶段。 在电话交谈的过程中,两端用户都知道他们在和谁说话,因此没必要不停地说“这是 Bob 在跟 Lucy 说话”。也没必要在每次说话之前都拨一次朋友的电话号码——我们的电话已经连接起来了。同理,在面向连接协议的数据传输阶段,也没必要说明我们自己或对等实体的地址。连接为我们维护的状态中包含了这些地址。我们只要发送数据就行了,不需要考虑寻址或其他与协议相关的问题。 就像用电话交谈一样,连接的任一端完成数据的传输时,都要通知其对等实体。两端都完成传输时,要依次将连接拆除。 既然无连接协议有这么多的缺点,大家可能会奇怪,为什么还要使用这种协议呢?我们会看到,在很多情况下,使用无连接协议构建应用程序都是有意义的。比如,使用无连接协议可以很方便地支持一对多和多对一通信,而面向连接协议通常都需要多个独立的连接才能做到。但更重要的是,无连接协议是构建面向连接协议的基础。TCP/IP 是基于一个4层的协议栈,如下图所示: 如图所示,TCP 和 UDP 都是构建在 IP 之上的。因此,IP 是构建整个 TCP/IP 协议族的基础。但 IP 提供的是一种尽力而为的、不可靠的无连接服务。它接收来自其上层的分组,将它们封装在一个 IP 分组中,根据路由为分组选择正确的硬件接口,从这个接口将分组发送出去。一旦将分组发送出去了,IP 就不再关心这个分组了。和所有无连接协议一样,它将分组发送出去之后就不再记得这个分组了。 这种简单性也是 IP 的主要优点。因为它对底层的物理介质没有作任何假设,所以在任何能够承载分组的物理链路上都可以运行 IP。例如,IP 可以运行在简单的串行链路、以太网和令牌环 LAN、X.25 和使用 ATM(Asychronous Transfer Mode,异步转移模式)的 WAN、CDPD(Cellular Digital Packet Data,无线蜂窝数字分组数据)网,以及很多其他网络上。尽管这些网络技术之间有很大的差异,但 IP 对它们一视同仁,除了认为它们可以转发分组之外没有对其作任何假设。这种机制隐含了很深的意义。IP 可以运行在任何能够承载分组的网络上,所以整个 TCP/IP 协议族也可以。 现在我们来看看 TCP 是怎样利用这种简单的无连接服务来提供可靠的面向连接服务的。TCP 的分组被称为段(segment),是放在 IP 数据报中发送的,因此,根本无法假定这些分组会抵达目的地,更不用说保证分组无损坏且以原来的顺序到达了。 为了提供这种可靠性,TCP 向基本的 IP 服务中添加了三项功能:首先,它为 TCP 段中的数据提供了校验和。这样有助于确保抵达目的地的数据在传输过程中不会被网络损坏。第二,它为每字节分配了一个序列号,这样,如果数据抵达目的地时真的错序了,接收端也能够按照恰当的顺序将其重装起来。当然,TCP 并没有为每字节都附加一个序列号。实际上,每个 TCP 段的首部都包含了段中第一字节的序列号。这样,就隐含地知道了段中其他字节的序列号。第三,TCP 提供了一种确认-重传机制,以确保最终每个段都会被传送出去。 另一方面,UDP 为编写应用程序的程序员提供了一种不可靠的无连接服务。事实上,UDP 只向底层的 IP 协议中添加了两项功能。 首先,它提供了一个可选的校验和来检测数据的损坏情况。尽管 IP 也有校验和,但它只对 IP 分组首部进行计算,所以,TCP 和 UDP 也都提供了校验和来保护它们自己的首部和数据。 其次,UDP 向 IP添加的第二项特性就是端口的概念。 回到与电话/寄信的类比中来,我们可以把 TCP 连接中的网络地址当作一个办公室总机的电话号码,把端口号当作办公室中某台正被呼叫的特定电话的分机号。同理,可以将UDP网络地址当作一座公寓楼的地址,并把端口号当作公寓楼大厅中的个人邮箱。 参考网址:http://network.chinabyte.com/169/13301669.shtml
广播地址(Broadcast Address)是专门用于同时向网络中(通常指同一子网)所有工作站进行发送的一个地址。在使用TCP/IP 协议的网络中,主机标识段host ID(简称主机 ID) 为全 1 的 IP 地址为广播地址,广播的分组传送给同一个子网的所有计算机。例如,对于10.1.1.0 (255.255.255.0 )网段,其广播地址为10.1.1.255 (255 即为 2 进制的 11111111 ),当发出一个目的地址为10.1.1.255 的数据包时,它将被分发给该网段上的所有计算机。广播地址应用于网络内的所有主机。 1.1 模型为了减少协议设计的复杂性,大多数网络模型均采用分层的方式来组织。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。每一层利用下一层提供的服务来为上一层提供服务,本层服务的实现细节对上层屏蔽。 用户接触到的,只是最上面的一层,根本没有感觉到下面的层。要理解互联网,必须从最下层开始,自下而上理解每一层的功能。 如何分层有不同的模型,有的模型分七层( 不常用 ),有的分四层( 现在就是用这种 ),如下图: 3.1 定义 单纯的 0 和 1 没有任何意义,必须规定解读方式:多少个电信号算一组?每个信号位有何意义?这就是“链接层”的功能,它在“物理层”的上方,确定了 0 和 1 的分组方式。3.2 以太网协议 早期的时候,每家公司都有自己的电信号分组方式。逐渐地,一种叫做“以太网”(Ethernet)的协议,占据了主导地位。 以太网规定,一组电信号构成一个数据包,叫做“帧”(Frame)。每一帧分成两个部分:标头(Head)和数据(Data)。 “标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等等;"数据"则是数据包的具体内容。 “标头”的长度,固定为 18 字节。"数据"的长度,最短为 46 字节,最长为 1500 字节。因此,整个"帧"最短为 64 字节,最长为 1518 字节。如果数据很长,就必须分割成多个帧进行发送。 3.3 MAC 地址 上面提到,以太网数据包的“标头”,包含了发送者和接受者的信息。那么,发送者和接受者是如何标识呢? 以太网规定,连入网络的所有设备,都必须具有“网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。通过网卡能够使不同的计算机之间连接,从而完成数据通信等功能。网卡的地址,就是数据包的发送地址和接收地址,这叫做 MAC 地址。 定义地址只是第一步,后面还有更多的步骤。 首先,一块网卡怎么会知道另一块网卡的 MAC 地址? 回答是有一种 ARP 协议,可以解决这个问题。这个留到后面介绍,这里只需要知道,以太网数据包必须知道接收方的 MAC 地址,然后才能发送。 其次,就算有了 MAC 地址,系统怎样才能把数据包准确送到接收方? 回答是以太网采用了一种很“原始”的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机发送,让每台计算机自己判断,是否为接收方。 上图中,1号计算机向 2 号计算机发送一个数据包,同一个子网络的 3 号、4号、5号计算机都会收到这个包。它们读取这个包的“标头”,找到接收方的 MAC 地址,然后与自身的 MAC 地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做“广播”(broadcasting)。 有了数据包的定义、网卡的 MAC 地址、广播的发送方式,“链接层”就可以在多台计算机之间传送数据了。 4.1 网络层的由来 以太网协议,依靠 MAC 地址发送数据。理论上,单单依靠 MAC 地址,北京的网卡就可以找到深圳的网卡了,技术上是可以实现的。 但是,这样做有一个重大的缺点。以太网采用广播方式发送数据包,所有成员人手一“包”,不仅效率低,而且局限在发送者所在的子网络。也就是说,如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理的,否则互联网上每一台计算机都会收到所有包,那会引起灾难(广播风暴)。 互联网是无数子网络共同组成的一个巨型网络,很像想象北京和深圳的电脑会在同一个子网络,这几乎是不可能的。 这就导致了“网络层”的诞生。它的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做“网络地址”,简称“网址”。 于是,“网络层”出现以后,每台计算机有了两种地址,一种是 MAC 地址,另一种是网络地址。两种地址之间没有任何联系,MAC 地址是绑定在网卡上的,网络地址则是管理员分配的,它们只是随机组合在一起。 网络地址帮助我们确定计算机所在的子网络,MAC 地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理 MAC 地址。4.2 IP 协议规定网络地址的协议,叫做 IP 协议。它所定义的地址,就被称为 IP 地址。 目前,广泛采用的是 IP 协议第四版,简称 IPv4。这个版本规定,网络地址由 32 个二进制位组成。 习惯上,我们用分成四段的十进制数表示 IP 地址,从0.0.0.0一直到 255.255.255.255。 互联网上的每一台计算机,都会分配到一个 IP 地址。这个地址分成两个部分,前一部分代表网络,后一部分代表主机。 比如,IP 地址 172.16.254.1,这是一个 32 位的地址,假定它的网络部分是前 24 位(172.16.254),那么主机部分就是后 8 位(最后的那个1)。处于同一个子网络的电脑,它们 IP 地址的网络部分必定是相同的,也就是说 172.16.254.2 应该与 172.16.254.1 处在同一个子网络。 但是,问题在于单单从 IP 地址,我们无法判断网络部分。还是以 172.16.254.1 为例,它的网络部分,到底是前 24 位,还是前 16 位,甚至前 28 位,从 IP 地址上是看不出来的。那么,怎样才能从 IP 地址,判断两台计算机是否属于同一个子网络呢?这就要用到另一个参数“子网掩码”(subnet mask)。 所谓“子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于 IP 地址,也是一个 32 位二进制数字,它的网络部分全部为1,主机部分全部为0,并且1和0分别连续。 比如,IP 地址 172.16.254.1,如果已知网络部分是前 24 位,主机部分是后 8 位,那么子网络掩码就是 11111111.11111111.11111111.00000000,写成十进制就是 255.255.255.0。 我们可以通过“子网掩码”来区分哪部分是子网 ID,哪部分为主机 ID。IP 地址和子网掩码中 1 相与“&”即可得到子网 ID,IP 地址和子网掩码中 0 相或 “|”,即可得到主机 ID。 知道“子网掩码”,我们就能判断,任意两个 IP 地址是否处在同一个子网络。方法是将两个 IP 地址与子网掩码分别进行 AND 运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。 比如,已知 IP 地址 172.16.254.1 和 172.16.254.233 的子网掩码都是 255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行 AND 运算,结果都是 172.16.254.0,因此它们在同一个子网络。 总结一下,IP 协议的作用主要有两个,一个是为每一台计算机分配 IP 地址,另一个是确定哪些地址在同一个子网络。 4.3 IP 数据包 根据 IP 协议发送的数据,就叫做 IP 数据包。不难想象,其中必定包括 IP 地址信息。 但是前面说过,以太网数据包只包含 MAC 地址,并没有 IP 地址的栏位。那么是否需要修改数据定义,再添加一个栏位呢? 回答是不需要,我们可以把 IP 数据包直接放进以太网数据包的“数据”部分,因此完全不用修改以太网的规格。这就是互联网分层结构的好处:上层的变动完全不涉及下层的结构。具体来说,IP 数据包也分为“标头”和“数据”两个部分。 “标头”部分主要包括版本、长度、IP 地址等信息,“数据”部分则是 IP 数据包的具体内容。它放进以太网数据包后,以太网数据包就变成了下面这样。 IP 数据包的“标头”部分的长度为 20 到 60 字节,整个数据包的总长度最大为 65,535字节。因此,理论上,一个 IP 数据包的“数据”部分,最长为 65,515字节。前面说过,以太网数据包的“数据”部分,最长只有 1500 字节。因此,如果 IP 数据包超过了 1500 字节,它就需要分割成几个以太网数据包,分开发送了。 4. 4 ARP 协议 关于"网络层",还有最后一点需要说明。 因为 IP 数据包是放在以太网数据包里发送的,所以我们必须同时知道两个地址,一个是对方的 MAC 地址,另一个是对方的 IP 地址。通常情况下,对方的 IP 地址是已知的(后文会解释),但是我们不知道它的 MAC 地址。 所以,我们需要一种机制,能够从 IP 地址得到 MAC 地址。 这里又可以分成两种情况。第一种情况,如果两台主机不在同一个子网络,那么事实上没有办法得到对方的 MAC 地址,只能把数据包传送到两个子网络连接处的“网关”(gateway),让网关去处理。 第二种情况,如果两台主机在同一个子网络,那么我们可以用 ARP 协议,得到对方的 MAC 地址。ARP 协议也是发出一个数据包(包含在以太网数据包中),其中包含它所要查询主机的 IP 地址,在对方的 MAC 地址这一栏,填的是 FF:FF:FF:FF:FF:FF,表示这是一个“广播”地址。它所在子网络的每一台主机,都会收到这个数据包,从中取出 IP 地址,与自身的 IP 地址进行比较。如果两者相同,都做出回复,向对方报告自己的 MAC 地址,否则就丢弃这个包。 总之,有了 ARP 协议之后,我们就可以得到同一个子网络内的主机 MAC 地址,可以把数据包发送到任意一台主机之上了。 接下来的问题是,同一台主机上有许多程序都需要用到网络,比如,你一边浏览网页,一边与朋友在线聊天。当一个数据包从互联网上发来的时候,你怎么知道,它是表示网页的内容,还是表示在线聊天的内容? 也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做“端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。 “端口”是 0 到 65535 之间的一个整数,正好 16 个二进制位。0到 1023 的端口被系统占用,用户只能选用大于 1023 的端口。不管是浏览网页还是在线聊天,应用程序会随机选用一个端口,然后与服务器的相应端口联系。 “传输层”的功能,就是建立“端口到端口”的通信。相比之下,“网络层”的功能是建立“主机到主机”的通信。只要确定主机和端口,我们就能实现程序之间的交流。因此,Unix 系统就把主机+端口,叫做“套接字”(socket)。有了它,就可以进行网络应用程序开发了。 5. 2 UDP 协议 现在,我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做 UDP 协议,它的格式几乎就是在数据前面,加上端口号。 UDP 数据包,也是由“标头”和“数据”两部分组成。 “标头”部分主要定义了发出端口和接收端口,“数据”部分就是具体的内容。然后,把整个 UDP 数据包放入 IP 数据包的“数据”部分,而前面说过,IP 数据包又是放在以太网数据包之中的,所以整个以太网数据包现在变成了下面这样: UDP 数据包非常简单,“标头”部分一共只有 8 个字节,总长度不超过 65,535字节,正好放进一个 IP 数据包。 5.3 TCP 协议 UDP 协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。 为了解决这个问题,提高网络可靠性,TCP 协议就诞生了。这个协议非常复杂,但可以近似认为,它就是有确认机制的 UDP 协议,每发出一个数据包都要求确认。如果有一个数据包遗失,就收不到确认,发出方就知道有必要重发这个数据包了。 因此,TCP 协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。 TCP 数据包和 UDP 数据包一样,都是内嵌在 IP 数据包的“数据”部分。TCP 数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常 TCP 数据包的长度不会超过 IP 数据包的长度,以确保单个 TCP 数据包不必再分割。 举例来说,TCP 协议可以为各种各样的程序传递数据,比如 Email、WWW、FTP 等等。那么,必须有不同协议规定电子邮件、网页、FTP 数据的格式,这些应用程序协议就构成了“应用层”。 这是最高的一层,直接面对用户。它的数据就放在 TCP 数据包的“数据”部分。因此,现在的以太网的数据包就变成下面这样。
需要引起注意的:ISO/IEC 9899-1999里面,这么写是非法的,这个仅仅是GNU C的扩展,gcc可以允许这一语法现象的存在。但最新的C/C++不知道是否可以,我没有测试过。(C99允许。微软的VS系列报一个WARNING,即非常的标准扩展。) 结构体最后使用0或1的长度数组的原因,主要是为了方便的管理内存缓冲区,如果你直接使用指针而不使用数组,那么,你在分配内存缓冲区时,就必须分配结构体一次,然后再分配结构体内的指针一次,(而此时分配的内存已经与结构体的内存不连续了,所以要分别管理即申请和释放)而如果使用数组,那么只需要一次就可以全部分配出来,(见下面的例子),反过来,释放时也是一样,使用数组,一次释放,使用指针,得先释放结构体内的指针,再释放结构体。还不能颠倒次序。 其实就是分配一段连续的的内存,减少内存的碎片化。 Linux目录中[root@deng /]# vim usr/include/linux/if_pppox.h 在Linux系统里,/usr/include/linux/if_pppox.h里面有这样一个结构: struct pppoe_tag { __u16 tag_type; __u16 tag_len; char tag_data[0]; } __attribute ((packed)); 最后一个成员为可变长的数组,对于TLV(Type-Length-Value)形式的结构,或者其他需要变长度的结构体,用这种方式定义最好。使用起来非常方便,创建时,malloc一段结构体大小加上可变长数据长度的空间给它,可变长部分可按数组的方式访问,释放时,直接把整个结构体free掉就可以了。例子如下: struct pppoe_tag *sample_tag; __u16 sample_tag_len = 10; sample_tag = (struct pppoe_tag *)malloc(sizeof(struct pppoe_tag)+sizeof(char)*sample_tag_len); sample_tag->tag_type = 0xffff; sample_tag->tag_len = sample_tag_len; sample_tag->tag_data[0]=.... free(sample_tag) 是否可以用 char *tag_data 代替呢?其实它和 char *tag_data 是有很大的区别,为了说明这个问题,我写了以下的程序: 例1:test_size.c 10 struct tag1 20 { 30 int a; 40 int b; 50 }__attribute ((packed)); 70 struct tag2 80 { 90 int a; 100 int b; 110 char *c; 120 }__attribute ((packed)); 140 struct tag3 150 { 160 int a; 170 int b; 180 char c[0];190 }__attribute ((packed)); 210 struct tag4 220 { 230 int a; 240 int b; 250 char c[1];260 }__attribute ((packed)); 280 int main() 290 { 300 struct tag2 l_tag2; 310 struct tag3 l_tag3; 320 struct tag4 l_tag4; 340 memset(&l_tag2,0,sizeof(struct tag2)); 350 memset(&l_tag3,0,sizeof(struct tag3)); 360 memset(&l_tag4,0,sizeof(struct tag4)); 380 printf("size of tag1 = %d\n",sizeof(struct tag1)); 390 printf("size of tag2 = %d\n",sizeof(struct tag2)); 400 printf("size of tag3 = %d\n",sizeof(struct tag3)); 420 printf("l_tag2 = %p,&l_tag2.c = %p,l_tag2.c = %p\n",&l_tag2,&l_tag2.c,l_tag2.c); 430 printf("l_tag3 = %p,l_tag3.c = %p\n",&l_tag3,l_tag3.c); 440 printf("l_tag4 = %p,l_tag4.c = %p\n",&l_tag4,l_tag4.c); 450 exit(0); 460 } __attribute ((packed)) 是为了强制不进行4字节对齐,这样比较容易说明问题。 程序的运行结果如下: size of tag1 = 8 size of tag2 = 12 size of tag3 = 8 size of tag4 = 9 l_tag2 = 0xbffffad0,&l_tag2.c = 0xbffffad8,l_tag2.c = (nil) l_tag3 = 0xbffffac8,l_tag3.c = 0xbffffad0 l_tag4 = 0xbffffabc,l_tag4.c = 0xbffffac4 从上面程序和运行结果可以看出:tag1本身包括两个32位整数,所以占了8个字节的空间。tag2包括了两个32位的整数,外加一个char *的指针,所以占了12个字节。tag3才是真正看出char c[0]和char *c的区别,char c[0]中的c并不是指针,是一个偏移量,这个偏移量指向的是a、b后面紧接着的空间,所以它其实并不占用任何空间。tag4更加补充说明了这一点。所以,上面的struct pppoe_tag的最后一个成员如果用char *tag_data定义,除了会占用多4个字节的指针变量外,用起来会比较不方便: 方法一,创建时,可以首先为struct pppoe_tag分配一块内存,再为tag_data分配内存,这样在释放时,要首先释放tag_data占用的内存,再释放pppoe_tag占用的内存; 方法二,创建时,直接为struct pppoe_tag分配一块struct pppoe_tag大小加上tag_data的内存,从例一的420行可以看出,tag_data的内容要进行初始化,要让tag_data指向strct pppoe_tag后面的内存。 struct pppoe_tag { __u16 tag_type; __u16 tag_len; char *tag_data; } __attribute ((packed)); struct pppoe_tag *sample_tag; __u16 sample_tag_len = 10; sample_tag = (struct pppoe_tag *)malloc(sizeof(struct pppoe_tag)); sample_tag->tag_len = sample_tag_len; sample_tag->tag_data = malloc(sizeof(char)*sample_tag_len); sample_tag->tag_data[0]=... free(sample_tag->tag_data); free(sample_tag); sample_tag = (struct pppoe_tag *)malloc(sizeof(struct pppoe_tag)+sizeof(char)*sample_tag_len); sample_tag->tag_len = sample_tag_len; sample_tag->tag_data = ((char *)sample_tag)+sizeof(struct pppoe_tag); sample_tag->tag_data[0]=... free(sample_tag); 所以无论使用那种方法,都没有char tag_data[0]这样的定义来得方便。 讲了这么多,其实本质上涉及到的是一个C语言里面的数组和指针的区别问题(也就是我们提到的内存管理问题,数组分配的是在结构体空间地址后一段连续的空间,而指针是在一个随机的空间分配的一段连续空间)。char a[1]里面的a和char *b的b相同吗?《Programming Abstractions in C》(Roberts, E. S.,机械工业出版社,2004.6)82页里面说:“arr is defined to be identical to &arr[0]”。也就是说,char a[1]里面的a实际是一个常量,等于&a[0]。而char *b是有一个实实在在的指针变量b存在。所以,a=b是不允许的,而b=a是允许的。两种变量都支持下标式的访问,那么对于a[0]和b[0]本质上是否有区别?我们可以通过一个例子来说明。 10 #include <stdio.h> 20 #include <stdlib.h> 40 int main() 50 { 60 char a[10]; 70 char *b; 90 a[2]=0xfe; 100 b[2]=0xfe; 110 exit(0); 120 } 编译后,用objdump可以看到它的汇编: 080483f0 <main>: 80483f0: 55 push %ebp 80483f1: 89 e5 mov %esp,%ebp 80483f3: 83 ec 18 sub $0x18,%esp 80483f6: c6 45 f6 fe movb $0xfe,0xfffffff6(%ebp) 80483fa: 8b 45 f0 mov 0xfffffff0(%ebp),%eax 80483fd: 83 c0 02 add $0x2,%eax 8048400: c6 00 fe movb $0xfe,(%eax) 8048403: 83 c4 f4 add $0xfffffff4,%esp 8048406: 6a 00 push $0x0 8048408: e8 f3 fe ff ff call 8048300 <_init+0x68> 804840d: 83 c4 10 add $0x10,%esp 8048410: c9 leave 8048411: c3 ret 8048412: 8d b4 26 00 00 00 00 lea 0x0(%esi,1),%esi 8048419: 8d bc 27 00 00 00 00 lea 0x0(%edi,1),%edi 可以看出,a[2]=0xfe是直接寻址,直接将0xfe写入&a[0]+2的地址,而b[2]=0xfe是间接寻址,先将b的内容(地址)拿出来,加2,再0xfe写入计算出来的地址。所以a[0]和b[0]本质上是不同的。 但当数组作为参数时,和指针就没有区别了。int do1(char a[],int len); int do2(char *a,int len); 这两个函数中的a并无任何区别。都是实实在在存在的指针变量。 顺便再说一下,对于struct pppoe_tag的最后一个成员的定义是char tag_data[0],某些编译器不支持长度为0的数组的定义,在这种情况下,只能将它定义成char tag_data[1],使用方法相同。 在openoffice的源代码中看到如下数据结构,是一个unicode字符串结构,他的最后就用长度为1数组,可能是为了兼容或者跨编译器。 typedef struct _rtl_uString sal_Int32 refCount; sal_Int32 length; sal_Unicode buffer[1];} rtl_uString; 这是不定长字符串。大概意思是这样: rtl_uString * str = malloc(256); str->length = 256; str->buffer现在就指向一个长度为256 - 8的缓冲区 总结:通过上面的转载的文章,可以清晰的发现,这种方法的优势其实就是为了简化内存的管理,我们假设在理想的内存状态下,那么分配的内存空间,可以是按序下来的(当然,实际因为内存碎片等的原因会不同的)我们可以利用最后一个数组的指针直接无间隔的跳到分配的数组缓冲区,这在LINUX下非常常见,在WINDOWS下的我只是在MFC里见过类似的,别的情况下记不清楚了,只记得MFC里的是这么讲的,可以用分配的结构体的指针(this)直接+1(详细的方法请看我的博客:CE分类里的:内存池技术的应用和详细说明),就跳到实际的内存空间,当初也是想了半天,所以说,很多东西看似很复杂,其实都是基础的东西,要好好打实基础,这才是万丈高楼拔地巍峨的前提和保障,学习亦是如是,切忌好高骛远,应该脚踏实地,一步一步的向前走,而且要不时的总结自己的心得和体会,理论和实践不断的相互印证,才能够走得更远,看到更美丽的风景。 【柔性数组结构成员 C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其 他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。】 C语言大全,“柔性数组成员” 看看 C99 标准中 灵活数组成员: 结构体变长的妙用——0个元素的数组 有时我们需要产生一个结构体,实现了一种可变长度的结构。如何来实现呢? 看这个结构体的定义: typedef struct st_type int nCnt; int item[0]; }type_a; (有些编译器会报错无法编译可以改成:) typedef struct st_type int nCnt; int item[]; }type_a; 这样我们就可以定义一个可变长的结构,用sizeof(type_a)得到的只有4,就是sizeof(nCnt)=sizeof(int)那 个0个元素的数组没有占用空间,而后我们可以进行变长操作了。 C语言版: type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int)); C++语言版: type_a *p = (type_a*)new char[sizeof(type_a)+100*sizeof(int)]; 这样我们就产生了一个长为100的type_a类型的东西用p->item[n]就能简单地访问可变长元素,原理十分简单 ,分配了比sizeof(type_a)多的内存后int item[];就有了其意义了,它指向的是int nCnt;后面的内容,是没 有内存需要的,而在分配时多分配的内存就可以由其来操控,是个十分好用的技巧。 而释放同样简单: C语言版: free(p); C++语言版: delete []p; 其实这个叫灵活数组成员(fleible array member)C89不支持这种东西,C99把它作为一种特例加入了标准。但 是,C99所支持的是incomplete type,而不是zero array,形同int item[0];这种形式是非法的,C99支持的 形式是形同int item[];只不过有些编译器把int item[0];作为非标准扩展来支持,而且在C99发布之前已经有 了这种非标准扩展了,C99发布之后,有些编译器把两者合而为一。 下面是C99中的相关内容: 6.7.2.1 Structure and union specifiers As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member. With two exceptions, the flexible array member is ignored. First, the size of the structure shall be equal to the offset of the last element of an otherwise identical structure that replaces the flexible array member with an array of unspecified length.106) Second, when a . (or ->) operator has a left operand that is (a pointer to) a structure with a flexible array member and the right operand names that member, it behaves as if that member were replaced with the longest array (with the same element type) that would not make the structure larger than the object being accessed; the offset of the array shall remain that of the flexible array member, even if this would differ from that of the replacement array. If this array would have no elements, it behaves as if it had one element but the behavior is undefined if any attempt is made to access that element or to generate a pointer one past it. 例如在VC++6里使用两者之一都能通过编译并且完成操作,而会产生warning C4200: nonstandard extension used : zero-sized array in struct/union的警告消息。 而在DEVCPP里两者同样可以使用,并且不会有警告消息 参考例子: #include <stdio.h> #include <string.h> #include <stdlib.h> struct a_t{ char ch1; char ch2; char str[0]; struct a1_t{ char ch1; char ch2; int main(void) printf("sizeof(a) = %d\n", sizeof(struct a_t)); printf("sizeof(a1) = %d\n", sizeof(struct a1_t)); struct a_t *a = (void *)malloc(sizeof(struct a_t) + 10); a->ch1 = 96; a->ch2 = 97; a->ch3 = 98; memset(a->str, 0, 10); memcpy(a->str, "hello", 5); printf("====> %s\n", a->str); free(a); return 0;
程序运行起来后(每个进程)都有一张文件描述符的表,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符 0、1、2 记录在表中。程序运行起来后这三个文件描述符是默认打开的。 #define STDIN_FILENO 0 //标准输入的文件描述符 #define STDOUT_FILENO 1 //标准输出的文件描述符 #define STDERR_FILENO 2 //标准错误的文件描述符 1. 引言大多数LInux文件IO只需用到5个函数:open read write lseek close. 2. 文件描述符对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时, 用open或create返回的文件描述符标识该文件,将其作为参数传送给read或write。在POSIX.1应用程序中,幻数0、1、2应被代换成符号常数STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO。这些常数都定义在头文件<unistd.h>中。 每个进程运行时,系统已分配3个文件描述符(0,1,2),分别对应标准输入, 标准输出, 标准错误输出 3. 文件IO中常用的函数 3.1 open: 打开一个文件或者设备文件, 返回一个文件描述符。当操作此文件描述符时,就是操作相应的文件或设备int open(char *pathname,int falgs,.../*mode*/)flags 必须指其中O_RDONLY O_WRONLY O_RDWR唯一项可选项:O_APPEND 每次写操写都将文件指针定位文件尾处O_CREAT 如果文件不存在创建新文件O_EXCL 如指定O_CREAT时文件存在 出错返回O_TRUNC 必须以O_WRONL或O_RDWR进行操作,把文件清空O_NONBLOCK 以非阻塞的方式打开 O_NOCTTY 如果打开文件为终端设备,不将该设备分配为此进程的控制终端O_SYNC 每次write都等到I/O操作完成,并等文件的属性更新O_RSYNC 作为read操作等侍,直到任何对同文件部分未决的写入完成O_DSYNC 每次write都等到I/O操作完成,不等文件的属性更mode指定创建文件的权限//创建一个文件,假设这个文件存在时要清空open("文件", O_RDWR|O_CREAT|O_TRUNC, 0777);fd = open("txt", O_RDONLY | O_CREAT | O_EXCL, 0644); 3.2 creat int creat(char *pathname,mode_t mode) 等价于:open(char *,O_WRONLY | O_CREAT | O_TRUNC, mode) 3.3 read读取已经打开的文件中的数据。读了文件以后,文件描述符对应文件的offset会自动偏移。ssize_t read(int fd, void *buf, size_t count);从文件描述符fd指向文件里读取最大为count字节的数据放到buf指定的地址上去.返回值: 为实际上读取的数值, 为0时读到文件尾, 为-1时错误 3.4 write向指定的文件中写数据。成功写以后, 文件描述符的offset自动偏移size_t write(int fd, const void *buf, size_t count);把在buf地址指定的数据写到fd指向的文件里,最大写count字节.成功返回写了多少字节, -1失败。write出错的一个常见原因是:磁盘已写满,或者超过了对一个给定进程的文件长度限制。 3.5 lseek每个打开文件都有一个与其相关联的“当前文件位移量”。它是一个非负整数,用以度量 从文件开始处计算的字节数。就是改变文件描述符的偏移位置。off_t lseek(int fildes, off_t offset, int whence);whence:SEEK_SET : 把偏移量设为offset, 从文件头开始.SEEK_CUR : 把当前偏移量加上offset的值SEEK_END : 先从文件尾开始偏移offset的值返回值:成功返回定位之后的文件指针偏移 失败返回 -1返回当前文件的偏移量off_t currpos = lseek(fd, 0, SEEK_CUR); 3.7 close把没用的文件描述符关掉,把此文件描述符重新分配.int close(int fd); 3.8 dup可用来复制一个现存的文件描述符.int dup(int oldfd);int dup2(int oldfd, int newfd); dup2(oldfd, newfd) //让newfd成为oldfd的一个副本dup2(fd, 1); //让fd替代标准输出int ret=dup2(old,new)如果new 打开的,则关闭new 返回新的文件描述符 失败返回-1. du2是一个原子操作。dup2可以用newfd参数指定新描述符的数值。如果newfd当前已经打开,则先将其关闭再做dup2操作,如果oldfd等于newfd,则dup2直接返回newfd而不用先关闭newfd再复制。 3.9 fcntl 用于改变已打开的文件的性质 只能改变O_APPEND, O_NONBLOCK,O_ASYNC, O_DIRECTint fcntl(int fileds,int cmd,.../*int arg */)第三个参数除了cmd用于记录锁时为一个结构指针之外,其余均为整数fcntl五种功能如下:.复制一个现有的描述符 cmd=F_DUPFD复制的新文件描述符清掉文件描述符标志 并且共享同一个文件表项.获得/设置文件描述符标记 cmd=F_GETFD/F_SETFDflags = fcntl(fd, F_GETFL); //获取fcntl(fd, F_SETFL, flags); //设置.获得/设置文件状态标志 cmd=F_GETFL/F_SETFL 忽略 O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC标志 由于历史原因O_RDONLY O_WRONLY O_RDWR并不各占一位, 它们之间互斥。因此首先必须用屏蔽字O_ACCMODE 取得访问模式然后与这三种flags比较 value=fcntl(fd,F_GETFL,0); switch(value & O_ACCMODE){case O_RDONLY : }.获得/设置导步I/O所有权cmd=F_GETOWN/F_SETOWN.获得/设置记录锁 cmd=F_GETLK F_SETLK/F_SETLKW 3.10 pread pwrite 定位文件进行读写 不影响文件指针 偏移和读写操作为原子操作 3.11 sync void sync(void) :函数只是将所有修改过的块缓冲区排入写队列,然后就返回,并不等实际写磁盘完成 int fsync(int fd) :等待实际磁盘写操作完成,并且同步更新文件的属性,可用于数据库类型的应用程序 int fdatasync(int fd) :类似于fsync,但只影响文件的数据部分不影响文件的属性 3.12 ioctl int ioctl(int fd, int request, ...); 称之为I/O操作的垃圾箱 只要其字操函数不能或难于实现在 它都可能很容易做到 ========================================================== 4. 某些系统下提供名为/dev/fd/N 等文件。打开文件/dev/fd/N 等效于复制N文件描述符(假定N描述符是打开的) 与其N共享文件表项 也有某些系统为/dev/fd/stdin /dev/fd/stdout 等,均为同等操作 homework: 1. 实现mycopy拷贝一个文件到另外一个文件(功能相当于 cp a.txt b.txt) 2. 实现mytouch 创建一个文件(功能相当于touch a.txt)。 3. 编写一个同dup2功能相同的函数,要求不调用fcntl函数并且要有正确的出错处理。 4. 在如启用添加标志打开一文件以便读、写,能否用lseek在任一位置开始读?能否用lseek更新文件中任一部分的数据?请写一段程序以验证之。
系统调用概述 系统调用,顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获得系统时间或设置定时器等。 从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。 系统服务之所以需要通过系统调用来提供给用户空间的根本原因是为了对系统进行“保护”,因为我们知道 Linux 的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中,逻辑上相互隔离。所以用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间函数。比如我们熟悉的“hello world”程序(执行时)就是标准的用户空间进程,它使用的打印函数 printf 就属于用户空间函数,打印的字符“hello word”字符串也属于用户空间数据。 但是很多情况下,用户进程需要获得系统服务(调用系统程序),这时就必须利用系统提供给用户的“特殊接口”——系统调用了,它的特殊性主要在于规定了用户进程进入内核的具体位置;换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全无误。我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实地坐在观光车上,按照规定的路线观光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。 答案是软件中断。软件中断和我们常说的中断(硬件中断)不同之处在于,它是通过软件指令触发而并非外设引发的中断,也就是说,又是编程人员开发出的一种异常(该异常为正常的异常)。操作系统一般是通过软件中断从用户态切换到内核态。 中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。更多详细说明请看《系统调用的实现原理》。 系统调用和库函数的区别 Linux 下对文件操作有两种方式:系统调用(system call)和库函数调用(Library functions)。 库函数由两类函数组成: 1)不需要调用系统调用 不需要切换到内核空间即可完成函数全部功能,并且将结果反馈给应用程序,如strcpy、bzero 等字符串操作函数。 2)需要调用系统调用 需要切换到内核空间,这类函数通过封装系统调用去实现相应功能,如 printf、fread等。 系统调用是需要时间的,程序中频繁的使用系统调用会降低程序的运行效率。当运行内核代码时,CPU工作在内核态,在系统调用发生前需要保存用户态的栈和内存环境,然后转入内核态工作。系统调用结束后,又要切换回用户态。这种环境的切换会消耗掉许多时间。 库函数访问文件的时候根据需要,设置不同类型的缓冲区,从而减少了直接调用 IO 系统调用的次数,提高了访问效率。缓冲区详情请看《浅谈标准I/O缓冲区》。 其中有一些函数的作用完全相同,只是参数不同。(可能很多熟悉C++朋友马上就能联想起函数重载,但是别忘了Linux核心是用C语言写的,所以只能取成不同的函数名)。还有一些函数已经过时,被新的更好的函数所代替了(gcc在链接这些函数时会发出警告),但因为兼容的原因还保留着,这些函数我会在前面标上“*”号以示区别。 一、进程控制: 创建一个新进程 clone 按指定条件创建子进程 execve 运行可执行文件 _exit 立即中止当前进程 getdtablesize 进程所能打开的最大文件数 getpgid 获取指定进程组标识号 setpgid 设置指定进程组标志号 getpgrp 获取当前进程组标识号 setpgrp 设置当前进程组标志号 getpid 获取进程标识号 getppid 获取父进程标识号 getpriority 获取调度优先级 setpriority 设置调度优先级 modify_ldt 读写进程的本地描述表 nanosleep 使进程睡眠指定的时间 改变分时进程的优先级 pause 挂起进程,等待信号 personality 设置进程运行域 prctl 对进程进行特定操作 ptrace sched_get_priority_max 取得静态优先级的上限 sched_get_priority_min 取得静态优先级的下限 sched_getparam 取得进程的调度参数 sched_getscheduler 取得指定进程的调度策略 sched_rr_get_interval 取得按RR算法调度的实时进程的时间片长度 sched_setparam 设置进程的调度参数 sched_setscheduler 设置指定进程的调度策略和参数 sched_yield 进程主动让出处理器,并将自己等候调度队列队尾 vfork 创建一个子进程,以供执行新程序,常与execve等同时使用 等待子进程终止 wait3 参见wait waitpid 等待指定子进程终止 wait4 参见waitpid capget 获取进程权限 capset 设置进程权限 getsid 获取会晤标识号 setsid 设置会晤标识号 【1】cJSON_AddItemToObject(root, "value", cJSON_CreateNumber(value)); 【2】cJSON_AddNumberToObject(root, "value", value); 【1】和【2】效果完全相同。 【1】 cJSON_AddItemToObject(root, "name", cJSON_CreateString(name)); 【2】 cJSON_AddStringToObject(root, "name",name); 【1】和【2】效果完全相同。 宏的定义原型: /* Macros for creating things quickly. */ #define cJSON_AddNullToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateNull()) #define cJSON_AddTrueToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateTrue()) #define cJSON_AddFalseToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateFalse()) #define cJSON_AddBoolToObject(object,name,b) cJSON_AddItemToObject(object, name, cJSON_CreateBool(b)) #define cJSON_AddNumberToObject(object,name,n) cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n)) #define cJSON_AddStringToObject(object,name,s) cJSON_AddItemToObject(object, name, cJSON_CreateString(s)) cJSON *root = cJSON_CreateObject(); //先创建一个对象 cJSON_AddNumberToObject(root, "value", 123.4); cJSON_AddStringToObject(root, "year", "2015"); cJSON_AddNullToObject(root, "secret"); cJSON_AddTrueToObject(root, "Bool"); cJSON_AddFalseToObject(root, "false"); cJSON_AddBoolToObject(root, "Yes", 3); cJSON_AddBoolToObject(root, "Yes", 0); out = cJSON_Print(root); //打印对象 //out = cJSON_PrintUnformatted(root); //非格式化的打印 //printf("===> 2: %s\n", out); printf("%s\n", out); cJSON_Delete(root); //释放对象所占的内存 free(out); 函数的原型: /* These calls create a cJSON item of the appropriate type. */ extern cJSON *cJSON_CreateNull(void); extern cJSON *cJSON_CreateTrue(void); extern cJSON *cJSON_CreateFalse(void); extern cJSON *cJSON_CreateBool(int b); extern cJSON *cJSON_CreateNumber(double num); extern cJSON *cJSON_CreateString(const char *string); extern cJSON *cJSON_CreateArray(void); extern cJSON *cJSON_CreateObject(void); 示例:参考上面博客的代码 函数原型: /* These utilities create an Array of count items. */ extern cJSON *cJSON_CreateIntArray(const int *numbers,int count); extern cJSON *cJSON_CreateFloatArray(const float *numbers,int count); extern cJSON *cJSON_CreateDoubleArray(const double *numbers,int count); extern cJSON *cJSON_CreateStringArray(const char **strings,int count); /* Append item to the specified array/object. */ extern void cJSON_AddItemToArray(cJSON *array, cJSON *item); extern void cJSON_AddItemToObject(cJSON *object,const char *string,cJSON *item); extern void cJSON_AddItemToObjectCS(cJSON *object,const char *string,cJSON *item); char *out = NULL; int array[] = {1, 2, 3, 4}; int array1[] = {1, 2, 3, 4, 5, 6}; cJSON *json = NULL; cJSON *item = NULL; cJSON *tmp = NULL; json = cJSON_CreateObject(); //cJSON_AddItemToObject(json,"array", cJSON_CreateIntArray(array, 4)); //与下面两行等价 item = cJSON_CreateIntArray(array, 4); cJSON_AddItemToObject(json, "array", item); out = cJSON_Print(json); printf("%s\n", out); printf("=========================\n"); printf("size: %d\n", cJSON_GetArraySize(item)); //输出为4 4个元素 tmp = cJSON_GetArrayItem(json, 0); //获取json中的第一个节点 if (NULL != tmp) printf("ok..\n"); printf("size: %d\n", cJSON_GetArraySize(tmp)); printf("error...\n"); //此时item数组中的元素有 1 2 3 4 5 6 7 printf("=========================\n"); cJSON_AddItemToArray(item, cJSON_CreateNumber(5)); cJSON_AddItemToArray(item, cJSON_CreateNumber(6)); cJSON_AddItemToArray(item, cJSON_CreateNumber(7)); printf("size: %d\n", cJSON_GetArraySize(item)); out = cJSON_Print(json); printf("%s\n", out); tmp = cJSON_CreateIntArray(array1, 6); //cJSON_AddItemToObject(json, "array1", tmp); //好像与下面一句话等价呢 cJSON_AddItemReferenceToObject(json, "array1", tmp); out = cJSON_Print(json); printf("%s\n", out); //整形数组的参考示例 int array3[] = {1, 3, 5, 7, 9}; json = cJSON_CreateIntArray(array3, 5); out = cJSON_Print(json); cJSON_Delete(json); printf("%s\n", out); free(out); //字符指针数组 const char *strings[7]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}; json = cJSON_CreateStringArray(strings, 7); out = cJSON_Print(json); cJSON_Delete(json); printf("%s\n", out); free(out); JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写,同时也易于机器解析和生成。它基于JavaScript(Standard ECMA-262 3rd Edition - December 1999)的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。 cJSON是一个超轻巧,携带方便,单文件,简单的可以作为ANSI-C标准的JSON解析器。 接触yeelink平台之后,慢慢接触到JSON格式,虽然一些简单的情况可以通过string库函数解析和组装JSON数据包,但是若有cJSON库的帮助,解析和组装JSON数据包的工作便会变得简单的多,下面就从两个例子出发说明cJSON数据包的使用方法。 2.type变量决定数据项类型(键的类型),数据项可以是字符串可以是整形,也可以是浮点型。如果是整形值的话可从valueint,如果是浮点型的话可从valuedouble取出,以此类推。 3.string可理解为节点的名称,综合此处的第2点可理解为“键”的名称。 密码学(cryptography):目的是通过将信息编码使其不可读,从而达到安全性。 明文(plain text):发送人、接受人和任何访问消息的人都能理解的消息。 密文(cipher text):明文消息经过某种编码后,得到密文消息。 加密(encryption):将明文消息变成密文消息。 解密(decryption):将密文消息变成明文消息。 算法:取一个输入文本,产生一个输出文本。 加密算法:发送方进行加密的算法。 解密算法:接收方进行解密的算法。 密钥(key):只有发送方和接收方理解的消息 对称密钥加密(Symmetric Key Cryptography):加密与解密使用相同密钥。 非对称密钥加密(Asymmetric Key Cryptography):加密与解密使用不同密钥。 2、相关的加密算法介绍 DES算法即数据加密标准,也称为数据加密算法。加密过程如下: 在SSL中会用到分组DES、三重DES算法等加密算法对数据进行加密。当然可以选用其他非DES加密算法,视情况而定,后面会详细介绍。 3、密钥交换算法 使用对称加密算法时,密钥交换是个大难题,所以Diffie和Hellman提出了著名的Diffie-Hellman密钥交换算法。 Diffie-Hellman密钥交换算法原理: (1)Alice与Bob确定两个大素数n和g,这两个数不用保密 (2)Alice选择另一个大随机数x,并计算A如下:A=gx mod n (3)Alice将A发给Bob (4)Bob选择另一个大随机数y,并计算B如下:B=gy mod n (5)Bob将B发给Alice (6)计算秘密密钥K1如下:K1=Bx mod n (7)计算秘密密钥K2如下:K2=Ay mod n K1=K2,因此Alice和Bob可以用其进行加解密 RSA加密算法是基于这样的数学事实:两个大素数相乘容易,而对得到的乘积求因子则很难。加密过程如下: (1)选择两个大素数P、Q (2)计算N=P*Q (3)选择一个公钥(加密密钥)E,使其不是(P-1)与(Q-1)的因子 (4)选择私钥(解密密钥)D,满足如下条件: (D*E) mod (P-1)(Q-1)=1 (5)加密时,明文PT计算密文CT如下: CT=PTE mod N (6)解密时,从密文CT计算明文PT如下: PT=CTDmodN 这也是SSL中会用一种密钥交换算法。 3、散列算法: 主要用于验证数据的完整性,即保证时消息在发送之后和接收之前没有被篡改对于SSL中使用到的散列算法有MD5、SHA-1。 4、数字证书: 数字证书其实就是一个小的计算机文件,其作用类似于我们的身份证、护照,用于证明身份,在SSL中,使用数字证书来证明自己的身份,而不是伪造的。 5、简单的总结: 在SSL中会使用密钥交换算法交换密钥;使用密钥对数据进行加密;使用散列算法对数据的完整性进行验证,使用数字证书证明自己的身份。好了,下面开始介绍SSL协议。 SSL介绍: 安全套接字(Secure Socket Layer,SSL)协议是Web浏览器与Web服务器之间安全交换信息的协议,提供两个基本的安全服务:鉴别与保密。 SSL是Netscape于1994年开发的,后来成为了世界上最著名的web安全机制,所有主要的浏览器都支持SSL协议。 目前有三个版本:2、3、3.1,最常用的是第3版,是1995年发布的。 SSL协议的三个特性 ① 保密:在握手协议中定义了会话密钥后,所有的消息都被加密。 ② 鉴别:可选的客户端认证,和强制的服务器端认证。 ③ 完整性:传送的消息包括消息完整性检查(使用MAC)。 SSL的位置 SSL介于应用层和TCP层之间。应用层数据不再直接传递给传输层,而是传递给SSL层,SSL层对从应用层收到的数据进行加密,并增加自己的SSL头。 SSL的工作原理 握手协议(Handshake protocol) 记录协议(Record protocol) 警报协议(Alert protocol) 1、握手协议 握手协议是客户机和服务器用SSL连接通信时使用的第一个子协议,握手协议包括客户机与服务器之间的一系列消息。SSL中最复杂的协议就是握手协议。该协议允许服务器和客户机相互验证,协商加密和MAC算法以及保密密钥,用来保护在SSL记录中发送的数据。握手协议是在应用程序的数据传输之前使用的。 每个握手协议包含以下3个字段 (1)Type:表示10种消息类型之一 (2)Length:表示消息长度字节数 (3)Content:与消息相关的参数 握手协议的4个阶段 1.1 建立安全能力 SSL握手的第一阶段启动逻辑连接,建立这个连接的安全能力。首先客户机向服务器发出client hello消息并等待服务器响应,随后服务器向客户机返回server hello消息,对client hello消息中的信息进行确认。 Client hello消息包括Version,Random,Session id,Cipher suite,Compression method等信息。 ClientHello 客户发送CilentHello信息,包含如下内容: (1)客户端可以支持的SSL最高版本号 (2)一个用于生成主秘密的32字节的随机数。(等会介绍主秘密是什么) (3)一个确定会话的会话ID。 (4)一个客户端可以支持的密码套件列表。 密码套件格式:每个套件都以“SSL”开头,紧跟着的是密钥交换算法。用“With”这个词把密钥交换算法、加密算法、散列算法分开,例如:SSL_DHE_RSA_WITH_DES_CBC_SHA, 表示把DHE_RSA(带有RSA数字签名的暂时Diffie-HellMan)定义为密钥交换算法;把DES_CBC定义为加密算法;把SHA定义为散列算法。 (5)一个客户端可以支持的压缩算法列表。 ServerHello服务器用ServerHello信息应答客户,包括下列内容 (1)一个SSL版本号。取客户端支持的最高版本号和服务端支持的最高版本号中的较低者。 (2)一个用于生成主秘密的32字节的随机数。(客户端一个、服务端一个) (3)会话ID (4)从客户端的密码套件列表中选择的一个密码套件 (5)从客户端的压缩方法的列表中选择的压缩方法 这个阶段之后,客户端服务端知道了下列内容: (1)SSL版本 (2)密钥交换、信息验证和加密算法 (3)压缩方法 (4)有关密钥生成的两个随机数。 1.2 服务器鉴别与密钥交换 服务器启动SSL握手第2阶段,是本阶段所有消息的唯一发送方,客户机是所有消息的唯一接收方。该阶段分为4步: (a)证书:服务器将数字证书和到根CA整个链发给客户端,使客户端能用服务器证书中的服务器公钥认证服务器。 (b)服务器密钥交换(可选):这里视密钥交换算法而定 (c)证书请求:服务端可能会要求客户自身进行验证。 (d)服务器握手完成:第二阶段的结束,第三阶段开始的信号 这里重点介绍一下服务端的验证和密钥交换。这个阶段的前面的(a)证书 和(b)服务器密钥交换是基于密钥交换方法的。而在SSL中密钥交换算法有6种:无效(没有密钥交换)、RSA、匿名Diffie-Hellman、暂时Diffie-Hellman、固定Diffie-Hellman、Fortezza。 在阶段1过程客户端与服务端协商的过程中已经确定使哪种密钥交换算法。 如果协商过程中确定使用RSA交换密钥,那么过程如下图: 这个方法中,服务器在它的第一个信息中,发送了RSA加密/解密公钥证书。不过,因为预备主秘密是由客户端在下一个阶段生成并发送的,所以第二个信息是空的。注意,公钥证书会进行从服务器到客户端的验证。当服务器收到预备主秘密时,它使用私钥进行解密。服务端拥有私钥是一个证据,可以证明服务器是一个它在第一个信息发送的公钥证书中要求的实体。 其他的几种密钥交换算法这里就不介绍了。可以参考Behrouz A.Forouzan著的《密码学与网络安全》。 1.3 客户机鉴别与密钥交换: 客户机启动SSL握手第3阶段,是本阶段所有消息的唯一发送方,服务器是所有消息的唯一接收方。该阶段分为3步: (a)证书(可选):为了对服务器证明自身,客户要发送一个证书信息,这是可选的,在IIS中可以配置强制客户端证书认证。 (b)客户机密钥交换(Pre-master-secret):这里客户端将预备主密钥发送给服务端,注意这里会使用服务端的公钥进行加密。 (c)证书验证(可选),对预备秘密和随机数进行签名,证明拥有(a)证书的公钥。 下面也重点介绍一下RSA方式的客户端验证和密钥交换。 这种情况,除非服务器在阶段II明确请求,否则没有证书信息。客户端密钥交换方法包括阶段II收到的由RSA公钥加密的预备主密钥。 阶段III之后,客户要有服务器进行验证,客户和服务器都知道预备主密钥。 1.4 完成 客户机启动SSL握手第4阶段,使服务器结束。该阶段分为4步,前2个消息来自客户机,后2个消息来自服务器。 1.5 密钥生成的过程 这样握手协议完成,下面看下什么是预备主密钥,主密钥是怎么生成的。为了保证信息的完整性和机密性,SSL需要有六个加密秘密:四个密钥和两个IV。为了信息的可信性,客户端需要一个密钥(HMAC),为了加密要有一个密钥,为了分组加密要一个IV,服务也是如此。SSL需要的密钥是单向的,不同于那些在其他方向的密钥。如果在一个方向上有攻击,这种攻击在其他方向是没影响的。生成过程如下: 记录协议在客户机和服务器握手成功后使用,即客户机和服务器鉴别对方和确定安全信息交换使用的算法后,进入SSL记录协议,记录协议向SSL连接提供两个服务: (1)保密性:使用握手协议定义的秘密密钥实现 (2)完整性:握手协议定义了MAC,用于保证消息完整性 记录协议的过程: 3、警报协议 客户机和服务器发现错误时,向对方发送一个警报消息。如果是致命错误,则算法立即关闭SSL连接,双方还会先删除相关的会话号,秘密和密钥。每个警报消息共2个字节,第1个字节表示错误类型,如果是警报,则值为1,如果是致命错误,则值为2;第2个字节制定实际错误类型。 SSL中,使用握手协议协商加密和MAC算法以及保密密钥 ,使用握手协议对交换的数据进行加密和签名,使用警报协议定义数据传输过程中,出现问题如何去解决。 整个过程比较复杂,如果大家有不理解和我叙述不周的地方,欢迎大家指正出来!
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。 这些特性使JSON成为理想的数据交换语言。 JSON大致3种结构,JSON对象、JSON数组和JSON对象和数组嵌套。 1 JSON对象 JSON对象简单而言便是键值对或名值对,而“值”可以是数值、字符串和布尔类型等。 JSON对象具体格式如图1所示。 2 JSON数组 JSON数组的表达方法和C语言数组的表达方法完全相同。下面的例子中存在一个JSON对象,该JSON对象只有一个键值对,键为lists而键值为JSON数组——[5,6,7,8]。这里已经存在JSON类型的嵌套,具体请看下面一个例子。 {"lists":[5,6,7,8]} 4.javascript中eval函数 【为什么要加括号 】 加上圆括号的目的是迫使eval函数在评估JavaScript代码的时候强制将括号内的表达式(expression)转化为对象,而不是作为语句(statement)来执行。例如对象字面量{},如若不加外层的括号,那么eval会将大括号识别为JavaScript代码块的开始和结束标记,那么{}将会被认为是执行了一句空语句。参考——【3】javascript eval和JSON之间的联系 3.配置对应的寄存器. 一.再开发板上我们的外部设备led,在核心板.所以我们需要打开核心板电路图. zshh@HP:~/work/arm/arm资料/exynos4412_lzy/schematic$ lsLCD-HD700-1306.pdf Tiny4412_1306_core_board_sch.pdfLCD-S701-121212.pdf Tiny4412SDK_1306_main_board_sch.pdf 可以看到,再这个目录下有四份手册, 第一份是高清lcd屏幕手册, 第二个就是我们需要的核心板电路图. 第三个是标准lcd屏幕手册. 第四份主板电路图. (注解:使用evince命令加上&符号的意思是,再后台打开该文件.他不会占用当前的控制终端.) zshh@HP:~/work/arm/arm资料/exynos4412_lzy/schematic$ evince Tiny4412_1306_core_board_sch.pdf & 它在这个手册p13,右上角的位置,由四个led灯的原理图. 1.从原理图上看管脚的一端应该拉低,led灯亮.以为它的另一个管脚给定是VDD_SYS_3.3V 2.那么我们再查找一下它链接4412的那个管脚是哪个,会在手册的第六页的右下方的位置. 看到led1,led2,led3, led4,gpio为GPM4_0 --GPM4_3控制. 二: 这时,我们需要打开另外一个手册,路径如下. zshh@HP:~/work/arm/arm资料/exynos4412_lzy/datasheet/4412$ ls SEC_Exynos4412_Users+Manual_Ver.1.00.00_bac.pdf 1.使用相同的方式打开SEC_Exynos4412_Users+Manual_Ver.1.00.00_bac.pdf手册. zshh@HP:~/work/arm/arm资料/exynos4412_lzy/datasheet/4412$ evince SEC_Exynos4412_Users+Manual_Ver.1.00.00_bac.pdf 2.我们找到GPM4描述,p107 (p107的意思是107页)GPM4CON 0x02E0 Port group GPM4 configuration registerGPM4DAT 0x02E4 Port group GPM4 data register GPM4PUD 0x02E8 Port group GPM4 pull-up/ pull-down register GPM4DRV 0x02EC Port group GPM4 drive strength control register GPM4CONPDN 0x02F0 Port group GPM4 power down mode configuration register 0x0000GPM4PUDPDN 0x02F4 Port group GPM4 power down mode pull-up/ pull-down register 我们需要配置控制寄存器.和数据寄存器.来完成led的亮灭. 找到p284,中由GPM4CON寄存器的描述,该寄存器的每4位控制一个gpio的工作模式.0-15位对应的是. gpm4con[0]-gpm4con[3]如下是每个4位设置如下的一个值,0x0 = Input 如果开始4位设置为0的话,gpm40讲会被设置为输入模式.0x1 = Output 0x2 = CAM_I2C0_SCL0x3 = CAM_GPIO[10]0x4 to 0xE = Reserved0xF = EXT_INT12[0] 我们需要控制led等的亮灭,那么我们需要设置gpm4con为1,则是设置输出模式.Base Address: 0x1100_0000 Address = Base Address + 0x02E0, Reset Value = 0x0000_0000这个是GPM4CON的基地址. 0x11000000+0x02E0GPM4DAT Base Address: 0x1100_0000 Address = Base Address + 0x02E4, Reset Value = 0x00GPM4DAT[7:0] 其中每一位定义一个gpio的的数据位. 三.定义并配置gpio.1.创建led.h./************************************************************************* > File Name: led.c > Author: zshh0604 > Mail: zshh0604@.com > Created Time: Thu 25 Dec 2014 08:12:41 PM************************************************************************/#ifndef __MY_LED_H#define __MY_LED_Hextern void led_init(void);extern void led_on(int no);extern void led_off(int no);#define GPM4CON (*((volatile unsigned int *) (0x11000000+0x02E0)))#define GPM4DAT (*((volatile unsigned int *) (0x11000000+0x02E4)))#endif2.创建led.c文件./************************************************************************* > File Name: led.c > Author: zshh0604 > Mail: zshh0604@.com > Created Time: Thu 25 Dec 2014 08:12:41 PM************************************************************************/#include<common.h>#include<led.h>void led_init(void){//清空0-15位.GPM4CON &= ~0XFFFF;GPM4CON |= 0x1111; //将它的值设置为输出模式. GPM4DAT |= 0xF; //默认灯全部都是灭的.}void led_on(int no){if(no<0 || no > 3){printf("led_on param error\n"); return;}GPM4DAT &= ~(0x1 << no);}void led_off(int no){if(no<0 || no > 3){printf("led_on param error\n"); return;}GPM4DAT |= (0x1 << no);} 参考网址:http://blog.csdn.net/shaohuazuo/article/details/42154697LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,LSL #< offset_12>] LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,LSR #< offset_12>] LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,ASR #< offset_12>] LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,ROR #< offset_12>] LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,RRX] · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <Rm>为偏移地址寄存器,包含内存访问地址偏移量; · LSL表示逻辑左移操作; · LSR表示逻辑右移操作; · ASR表示算术右移操作; · ROR表示循环右移操作; · RRX表示扩展的循环右移。 · <shift_imm>为移位立即数。 (3)操作伪代码 Case shift of 0b00 /*LSL*/ Index = Rm logic_shift_left shift_imm 0b01 /*LSR*/ If shift_imm = = 0 then /*LSR #32*/ Index = 0 Index = Rm logical_shift_right shift_imm 0b10 /*ASR*/ If shift_imm = = 0 then /*ASR #32*/ If Rm[31] = = 1 then Index = 0xffffffff Index = 0 Index = Rm Arithmetic_shift_Right shift_imm 0b11 /* ROR or RRX*/ If shift_imm = = 0 then /*RRX*/ Index = (C flag Logical_shift_left 31) OR (Rm logical_shift_Right 1) Else /*ROR*/ Index = Rm Rotate_Right shift_imm Endcase If U = = 1 then Address = Rn + index Else /*U = = 0*/ Address = Rn – index (4)说明 如果Rn被指定为程序计数器r15,其值为当前指令地址加8;如果r15被用作偏移地址寄存器Rm的值,指令的执行结果不可预知。 4.[Rn,#±< offset_12>]! (1)编码格式 指令的编码格式如图4.17所示。 图4.17 内存访问指令——前索引立即数偏移寻址编码格式 内存地址为基址寄存器Rn加/减立即数offset_8的值。当指令执行的条件<cc>满足时,生成的地址写回基址寄存器Rn中。 该寻址方式适合访问数组自动进行数组下标的更新。 (2)语法格式 LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<offset_12>] ! · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <offset_12>为12位立即数,内存访问地址偏移量; · !设置指令编码中的W位,更新指令基址寄存器。 (3)操作伪代码 If U == 1 then Address = Rn + offset_12 Address = Rn – offset_12 If ConditionPassed{cond} then Rn = address (4)说明 ① 如果指令中没有指定立即数,使用[<Rn>],编译器按[<Rn>,#0] ! 形式编码。 ② 如果Rn被指定为程序计数器r15,指令的执行结果不可预知。 5.[Rn,±Rm]! (1)编码格式 指令的编码格式如图4.18所示。 图4.18 内存访问指令——前索引寄存器偏移寻址编码格式 内存访问地址为基址寄存器Rn的值加(或减)偏移寄存器Rm的值。当指令的执行条件<cc>满足时,生成地地址将写回基址寄存器。 (2)语法格式 LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>] · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <Rm>为偏移地址寄存器,包含内存访问地址偏移量; · !设置指令编码中的W位,更新指令基址寄存器。 (3)操作伪代码 If U = = 1 then Address = Rn + Rm Address = Rn – Rm If ConditionPassed{cond} then Rn = address (4)说明 如果Rn和Rm指定为同一寄存器,指令的执行结果不可预知。 6.[Rn,±Rm,<shift>#< offset_12>]! (1)编码格式 指令的编码格式如图4.19所示。 图4.19 内存访问指令——带移位的前索引寄存器偏移寻址编码格式 内存地址为Rn的值加/减通过移位操作后的Rm的值。当指令的执行条件<cc>满足时,生成地地址将写回基址寄存器。 (2)语法格式 语法格式有以下5种。 LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,LSL #< offset_12>] ! LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,LSR #< offset_12>] ! LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,ASR #< offset_12>] ! LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,ROR #< offset_12>] ! LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>,±<Rm>,RRX] ! · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <Rm>为偏移地址寄存器,包含内存访问地址偏移量; · LSL表示逻辑左移操作; · LSR表示逻辑右移操作; · ASR表示算术右移操作; · ROR表示循环右移操作; · RRX表示扩展的循环右移。 · <shift_imm>为移位立即数。 · !设置指令编码中的W位,更新指令基址寄存器。 (3)操作伪代码 Case shift of 0b00 /*LSL*/ Index = Rm logic_shift_left shift_imm 0b01 /*LSR*/ If shift_imm = = 0 then /*LSR #32*/ Index = 0 Index = Rm logical_shift_right shift_imm 0b10 /*ASR*/ If shift_imm = = 0 then /*ASR #32*/ If Rm[31] = = 1 then Index = 0xffffffff Index = 0 Index = Rm Arithmetic_shift_Right shift_imm 0b11 /* ROR or RRX*/ If shift_imm = = 0 then /*RRX*/ Index = (C flag Logical_shift_left 31) OR (Rm logical_shift_Right 1) Else /*ROR*/ Index = Rm Rotate_Right shift_imm Endcase If U = = 1 then Address = Rn + index Else /*U = = 0*/ Address = Rn – index If ConditionPassed{cond} then Rn = address (4)说明 ① 当PC用作基址寄存器Rn或Rm时,指令执行结果不可预知。 ② 当Rn和Rm是同一个寄存器时,指令的执行结果不可预知。 7.[Rn],#±< offset_12> (1)编码格式 指令的编码格式如图4.20所示。 图4.20 内存访问指令——后索引立即数偏移寻址编码格式 指令使用基址寄存器Rn的值作为实际内存访问地址。当指令的执行条件满足时,将基址寄存器的值加/减偏移量产生新的地址值回写到Rn寄存器中。 (2)语法格式 LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>],±<offset_12> · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <offset_12>为12位立即数,内存访问地址偏移量。 (3)操作伪代码 Address = Rn If conditionPassed{cond} then If U = = 1 then Rn = Rn + offset_12 Rn = Rn – offset_12 (4)说明 ① LDRBT、LDRT、STRBT和STRT指令只支持后索引寻址。 ② 如果Rn被指定为程序计数器r15,指令的执行结果不可预知。 8.[Rn],±<Rm> (1)编码格式 指令的编码格式如图4.21所示。 图4.21 内存访问指令——后索引寄存器偏移寻址编码格式 指令访问地址为实际的基址寄存器的值。当指令的执行条件满足时,将基址寄存器的值加/减索引寄存器Rm的值回写到Rn基址寄存器。 (2)语法格式 LDR|STR{<cond>}{B}{T} <Rd>,[Rn],±<Rm> · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <Rm>为偏移地址寄存器,包含内存访问地址偏移量。 (3)操作伪代码 Address = Rn If conditionPassed{cond} then If U = = 1 then Rn = Rn + Rm Rn = Rn – Rm (4)说明 ① LDRBT、LDRT、STRBT和STRT指令只支持后索引寻址。 ② 如果Rm和Rn指定为同一寄存器,指令的执行结果不可预知。 9.[Rn],±Rm,<shift>#< offset_12>] (1)编码格式 指令的编码格式如图4.22所示。 图4.22 内存访问指令——带移位的后索引寄存器偏移寻址编码格式 实际的内存访问地址为寄存器Rn的值。当指令的执行条件满足时,将基址寄存器值加/减一个地址偏移量产生新的地址值。 (2)语法格式 语法格式有以下5种。 LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>],±<Rm>,LSL #< offset_12> LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>],±<Rm>,LSR #< offset_12> LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>],±<Rm>,ASR #< offset_12> LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>],±<Rm>,ROR #< offset_12> LDR|STR{<cond>}{B}{T} <Rd>,[<Rn>],±<Rm>,RRX · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <Rm>为偏移地址寄存器,包含内存访问地址偏移量; · LSL表示逻辑左移操作; · LSR表示逻辑右移操作; · ASR表示算术右移操作; · ROR表示循环右移操作; · RRX表示扩展的循环右移。 · <shift_imm>为移位立即数。 (3)操作伪代码 Address = Rn Case shift of 0b00 /*LSL*/ Index = Rm logic_shift_left shift_imm 0b01 /*LSR*/ If shift_imm = = 0 then /*LSR #32*/ Index = 0 Index = Rm logical_shift_right shift_imm 0b10 /*ASR*/ If shift_imm = = 0 then /*ASR #32*/ If Rm[31] = = 1 then Index = 0xffffffff Index = 0 Index = Rm Arithmetic_shift_Right shift_imm 0b11 /* ROR or RRX*/ If shift_imm = = 0 then /*RRX*/ Index = (C flag Logical_shift_left 31) OR (Rm logical_shift_Right 1) Else /*ROR*/ Index = Rm Rotate_Right shift_imm Endcase If ConditionPassed{cond} then If U = = 1 then Rn = Rn + index Else /*U = = 0*/ Rn = Rn – index (4)说明 ① LDRBT、LDRT、STRBT和STRT指令只支持后索引寻址。 ② 当PC用作基址寄存器Rn或Rm时,指令执行结果不可预知。 ③ 当Rn和Rm是同一个寄存器时,指令的执行结果不可预知。 4.2.2 杂类Load/Store指令的寻址方式 使用该类寻址方式的指令的语法格式如下。 LDR|STR{<cond>}H|SH|SB|D <Rd>,<addressing_mode> 使用该类寻址方式的指令包括:(有符号/无符号)半字Load/Store指令、有符号字节Load/Store指令和双字Load/Store指令。 该类寻址方式分为6种类型,如表4.4所示。 表4.4 杂类Load/Store指令的寻址方式 · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <offset_8>为8位立即数,内存访问地址偏移量,在指令编码格式中被拆为immedH和immedL两部分; · !设置指令编码中的W位,更新指令基址寄存器。 (3)操作伪代码 offset_8 = (immedH) << 4 OR immedL If U == 1 then Address = Rn + offset_8 Address = Rn – offset_8 If ConditionPassed{cond} then Rn = address (4)说明 ① 如果指令中没有指定立即数,使用[<Rn>],编译器按[<Rn>,#0] ! 形式编码。 ② 如果Rn被指定为程序计数器r15,指令的执行结果不可预知。 4.[Rn,±Rm] ! (1)编码格式 指令的编码格式如图4.27所示。 图4.27 杂项内存访问指令——前索引寄存器偏移寻址编码格式 内存访问地址为基址寄存器Rn的值加(或减)偏移寄存器Rm的值。当指令的执行条件<cc>满足时,生成地地址将写回基址寄存器。 (2)语法格式 LDR|STR{<cond>}H|SH|SB|D <Rd>,[<Rn>,±<Rm>] · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <Rm>为偏移地址寄存器,包含内存访问地址偏移量; · !设置指令编码中的W位,更新指令基址寄存器。 (3)操作伪代码 If U = = 1 then Address = Rn + Rm Address = Rn – Rm If ConditionPassed{cond} then Rn = address (4)说明 ① 如果Rn和Rm指定为同一寄存器,指令的执行结果不可预知。 ② 如果程序计数器r15被用作Rm或Rn,则指令的执行结果不可预知。 5.[Rn],#±< offset_8> (1)编码格式 指令的编码格式如图4.28所示。 图4.28 杂项内存访问指令——后索引立即数偏移寻址编码格式 指令使用基址寄存器Rn的值作为实际内存访问地址。当指令的执行条件满足时,将基址寄存器的值加/减偏移量生产新的地址值回写到Rn寄存器中。 (2)语法格式 LDR|STR{<cond>}H|SH|SB|D <Rd>,[<Rn>],±<offset_8> · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <offset_8>为8位立即数,内存访问地址偏移量。 (3)操作伪代码 Address = Rn Offset_8 = (immedH << 4) OR immedL If conditionPassed{cond} then If U = = 1 then Rn = Rn + offset_8 Rn = Rn – offset_8 (4)说明 ① 当指令中没有指定立即数时,汇编器按“[<Rn>],#0”编码。 ② 如果Rn被指定为程序计数器r15,指令的执行结果不可预知。 6.[Rn],±<Rm> (1)编码格式 指令的编码格式如图4.29所示。 图4.29 杂项内存访问指令——后索引寄存器偏移寻址编码格式 指令访问地址为实际的基址寄存器的值。当指令的执行条件满足时,将基址寄存器的值加/减索引寄存器Rm的值回写到Rn基址寄存器。 (2)语法格式 LDR|STR{<cond>}H|SH|SB|D <Rd>,[Rn],±<Rm> · Rn为基址寄存器,该寄存器包含内存访问的基地址; · <Rm>为偏移地址寄存器,包含内存访问地址偏移量。 (3)操作伪代码 Address = Rn If conditionPassed{cond} then If U = = 1 then Rn = Rn + Rm Rn = Rn – Rm (4)说明 ① 程序寄存器r15被指定为Rm或Rn,指令的执行结果不可预知。 ② 如果Rm和Rn指定为同一寄存器,指令的执行结果不可预知。 4.2.3 批量Load/Store指令寻址方式 批量Load/Store指令将一片连续内存单元的数据加载到通用寄存器组中或将一组通用寄存器的数据存储到内存单元中。 批量Load/Store指令的寻址模式产生一个内存单元的地址范围,指令寄存器和内存单元的对应关系满足这样的规则,即编号低的寄存器对应于内存中低地址单元,编号高的寄存器对应于内存中的高地址单元。 指令的语法格式如下。 LDM|STM{<cond>}<addressing_mode> <Rn>{!},<registers><^> 指令的寻址方式如表4.6所示。 表4.6 批量Load/Store指令的寻址方式 End_address = Rn + (Number_of_Set_Bits_In(register_list)*4) – 4 If conditionPassed(cond) and W = = 1 then Rn = Rn + (Number_of_Set_Bits_In(register_list)*4) 2.DA寻址 (1)编码格式 指令的编码格式如图4.32所示。 图4.32 批量Load/Store指令——后递减寻址 该寻址方式指定一片连续的内存地址空间,地址空间的大小<address_length>等于寄存器列表中寄存器数目的4倍。内存地址范围起始地址<start_address>等于基址寄存器Rn的值减去地址空间大小<address_length>并加4。结束地址<end_address>等于基址寄存器的值。 地址空间中的每个内存单元对应寄存器列表中的一个寄存器。编号低的寄存器对应于内存中低地址单元,编号高的寄存器对应于内存中的高地址单元。 当指令执行条件满足并且指令编码格式中W位置位时,基址寄存器Rn的值等于内存地址范围起始地址<start_address>减4。 (2)语法格式 LDM|STM{<cond>}IA <Rn>{!},<registers><^> · DA标识指令使用“后递减”寻址方式; · Rn为基址寄存器,包含内存访问的基地址; · <registers>为指令操作的寄存器列表; · <^>表示如果寄存器列表中包含程序计数器PC,是否将spsr拷贝到cpsr。 (3)操作伪代码 Start_address = Rn – (Number_of_Set_Bits_In(register_list)*4) + 4 End_address = Rn If conditionPassed(cond) and W = = 1 then Rn = Rn - (Number_of_Set_Bits_In(register_list)*4) 3.IB寻址 (1)编码格式 指令的编码格式如图4.33所示。 图4.33 批量Load/Store指令——前增加寻址 该寻址方式指定一片连续的内存地址空间,地址空间的大小<address_length>等于寄存器列表中寄存器数目的4倍。内存地址范围起始地址<start_address>等于基址寄存器Rn的值加4。结束地址<end_address>等于起始地址<start_address>加上地址空间大小<address_length>。 地址空间中的每个内存单元对应寄存器列表中的一个寄存器。编号低的寄存器对应于内存中低地址单元,编号高的寄存器对应于内存中的高地址单元。 当指令执行条件满足并且指令编码格式中W位置位,基址寄存器Rn的值等于内存地址范围结束地址<end_address>。 (2)语法格式 LDM|STM{<cond>}IB <Rn>{!},<registers><^> · IB标识指令使用“前增加”寻址方式; · Rn为基址寄存器,包含内存访问的基地址; · <registers>为指令操作的寄存器列表; · <^>表示如果寄存器列表中包含程序计数器PC,是否将spsr拷贝到cpsr。 (3)操作伪代码 Start_address = Rn + 4 End_address = Rn + (Number_of_Set_Bits_In(register_list)*4) If ConditionPassed(cond) and W= = 1 then Rn = Rn + (Number_Of_Set_Bits_In(register_list)*4) 4.DB寻址 (1)编码格式 指令的编码格式如图4.34所示。 图4.34 批量Load/Store指令——前递减寻址 该寻址方式指定一片连续的内存地址空间,地址空间的大小<address_length>等于寄存器列表中寄存器数目的4倍。内存地址范围起始地址<start_address>等于基址寄存器Rn的值减地址空间的大小<address_length>。结束地址<end_address>等于基址寄存器的值减4。 地址空间中的每个内存单元对应寄存器列表中的一个寄存器。编号低的寄存器对应于内存中低地址单元,编号高的寄存器对应于内存中的高地址单元。 当指令执行条件满足并且指令编码格式中W位置位,基址寄存器Rn的值等于内存地址范围起始地址<address_address>。 (2)语法格式 LDM|STM{<cond>}DB <Rn>{!},<registers><^> · DB标识指令使用“前递减”寻址方式; · Rn为基址寄存器,包含内存访问的基地址; · <registers>为指令操作的寄存器列表; · <^>表示如果寄存器列表中包含程序计数器PC,是否将spsr拷贝到cpsr。 (3)操作伪代码 Start_address = Rn - (Number_Of_Set_Bits_In(register_list)*4) End_address = Rn - 4 If ConditionPassed(cond) and W = = 1 then Rn = Rn – (Number_Of_Set_Bits_In(register_list)*4) 4.2.4 堆栈操作寻址方式 堆栈操作寻址方式和批量Load/Store指令寻址方式十分类似。但对于堆栈的操作,数据写入内存和从内存中读出要使用不同的寻址模式,因为进栈操作(pop)和出栈操作(push)要在不同的方向上调整堆栈。 下面详细讨论如何使用合适的寻址方式实现数据的堆栈操作。 根据不同的寻址方式,将堆栈分为以下4种。 ① Full栈:堆栈指针指向栈顶元素(last used location)。 ② Empty栈:堆栈指针指向第一个可用元素(the first unused location)。 ③ 递减栈:堆栈向内存地址减小的方向生长。 ④ 递增栈:堆栈向内存地址增加的方向生长。 根据堆栈的不同种类,将其寻址方式分为以下4种。 ① 满递减FD(Full Descending)。 ② 空递减ED(Empty Descending)。 ③ 满递增FA(Full Ascending)。 ④ 空递增EA(Empty Ascending)。 数据处理指令的基本语法格式如下。 <opcode> {<cond>} {S} <Rd>,<Rn>,<shifter_operand> 其中<shifter_operand>有下面11种形式,如表4.1所示。 表4.1 <shifter_operand>的寻址方式 MOV r2,r0 ;r0的值送r2 ADD r4,r3,r2 ;r2加r3,结果送r4 CMP r7,r8 ;比较r7和r8的值 3.寄存器移位方式 寄存器的值在被送到ALU之前,可以事先经过桶形移位寄存器的处理。预处理和移位发生在同一周期内,所以有效的使用移位寄存器,可以增加代码的执行效率。 具体的移位(或者循环移位)方式有下面几种。 · ASR:算术右移。 · LSL:逻辑左移。 · LSR:逻辑右移。 · ROR:循环右移。 · RRX:扩展的循环右移。 以上5种移位方式,移位值均可以由立即数或寄存器指定。下面是一些在指令中使用了移位操作的例子。 ADD r2,r0,r1,LSR #5 MOV r1,r0,LSL #2 RSB r9,r5,r5,LSL #1 SUB r1,r2,r0,LSR #4 MOV r2,r4,ROR r0 4.1.4 寻址方式分类详解 数据处理指令的寻址方式根据<shifter_operand>的不同,相应的分为11种。详见表4.1。下面对各类寻址方式进行详细说明。 1.#<immediate> (1)编码格式 指令的编码格式如图4.2所示。 图4.2 数据处理指令——立即数寻址编码格式 立即数寻址为数据处理指令提供了一个可直接操作的立即数。立即数的生成方法见前面章节介绍。如果移位值为0,则移位进位值为程序状态寄存器CPSR的C标志位;否则,为32-bit立即数的bit[31]。 (2)操作伪代码 Shifter_operand = immed_8 Rotate_Right (rotate_imm*2) if rotate_imm == 0 then shifter_carry_out = C flag else /* rotate_imm != 0*/ shifter_carry_out = shifter_operand[31] (3)说明 ① 并不是所有的32-bit立即数都是可以使用的合法立即数。只有那些通过将一个8-bit的立即数循环右移偶数位可以得到的立即数才可以在指令中使用。 ② 有些立即数可以通过不止一种方法得到。由于立即数的构造方法中移位包含了循环操作,而循环移位操作会影响CPSR的条件标志位C。因此,同一个合法的立即数由于采用了不同的编码方式,将使这些指令的执行产生不同的结果,这是不能允许的。ARM汇编器按照下面的规则来生成立即数的编码。 · 当立即数数值在0和0xFF范围时,令immed_8=<immediate>,immed_4=0。 · 其他情况下,汇编编译器选择使用immed_4数值最小的编码方式。 ③ 为了更精确地控制立即数的生成,可以使用下面的语法格式控制立即数的生成。 #<immed_8>,<rotate_amout> 其中,<rotate_amout> = 2*rotate_imm (4)举例 SUBS r0,r0,#1 ;寄存器r0中的数值减1,结果保存到r0 MOV r0,#0xff00 ; 0xff00 → r0 ;将立即数0xff00放入r0保存 2.<Rm> (1)编码格式 指令的编码格式如图4.3所示。 图4.3 数据处理指令——寄存器寻址编码格式 指令的操作数即为寄存器中的数值。移位寄存器的进位为程序状态寄存器CPSR的C标志位。 指令的语法格式为:<opcode> {<cond>} {S} <Rd>,<Rn>,<Rm> (2)操作伪代码 Shifter_operand = Rm Shifter_carry_out = C Flag (3)说明 ① 从指令的解码格式来看,寄存器寻址方式和使用立即数逻辑左移寻址解码格式是相同的,只是其移位数为0。 ② 如果指令中的Rm或Rn指定为程序计数器r15,则操作数的值为当前指令地址加8。 (4)举例 MOV r1,r2 ; r2 → r1 SUB r0,r1,r2 ; r1 – r2 → r0 3.<Rm>, LSL #<shift_imm> (1)编码格式 指令的编码格式如图4.4所示。 图4.4 数据处理指令——立即数逻辑左移寻址编码格式 指令的操作数为寄存器Rm的数值逻辑左移shift_imm位。左移的范围在0到31之间。左移移出的位用0补齐。进位标志位是最后移出的位(如果移位数为0,则为C标志位)。 指令的语法格式为:<opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,LSL #<shift_imm>,其中: · <Rm>为进行逻辑左移操作的寄存器; · LSL为逻辑左移操作标识; · <shift_imm>为逻辑左移位数,范围为0~31。 (2)操作伪代码 if shift_imm == 0 then /*执行寄存器操作*/ shifter_operand = Rm shifter_carry_out = C flag else /*移位寄存器大于零*/ shifter_operand = Rm logical_shift_left shift_imm shifter_carry_out = Rm[32 – shift_imm] (3)说明 ① 如果移位立即数<shift_imm> =0,则该寻址方式为立即数直接寻址。 ② 如果指令中的Rm或Rn指定为程序计数器r15,则操作数的值为当前指令地址加8。 (4)举例 SUB r0,r1,r2,LSL #10 ;r1的值减去r2的值左移10bit,结果放到r0寄存器 MOV r0,r2,LSL #3 ;r2的值左移3bit,结果放入r0,即r0 = r2×8 4.<Rm>, LSL <Rs> (1)编码格式 指令的编码格式如图4.5所示。 图4.5 数据处理指令——寄存器逻辑左移寻址编码格式 寄存器逻辑左移十分适合寄存器值乘2的倍数操作。 这个指令是将寄存器Rm的值逻辑左移一定的位数。位移的位数由Rs的最低8位bit[7∶0]决定。Rm移出的位用0补齐。进位值是移位寄存器最后移出的位,如果移位数大于0,则进位值为0。 (2)语法格式 <opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,LSL <Rs> · <Rm>为指令被移位的寄存器; · LSL为逻辑左移操作标识; · <Rs>为包含逻辑左移位数的寄存器。 (3)操作伪代码 if Rs[7:0] = = 0 then shifter_operand = Rm shifter_carry_out = C flag else if Rs[7:0] < 32 then shifter_operand = Rm logical_shift_left Rs[7:0] shifter_carry_out = Rm[32 – Rs[7:0]] else if Rs[7:0] = = 32 then shifter_operand = 0 shifter_carry_out = Rm[0] else /*Rs的后8位大于零*/ shifter_operand = 0 shifter_carry_out = 0 (4)说明 如果程序计数器r15被用作Rd,Rm,Rn或Rs中的任意一个,则指令的执行结果不可预知。 (5)举例 MOV r0,r2,LSL r3 ;r2的值左移r3位,结果放入r0 ANDS r1,r1,r2,LSL r3 ;r2的值左移r3位,然后和r1相与,结果放入r1 5.<Rm>, LSR #<shift_imm> (1)编码格式 指令的编码格式如图4.6所示。 图4.6 数据处理指令——立即数逻辑右移寻址编码格式 指令的操作数为寄存器Rm的值右移<shift_imm>位,相当于Rm的值除以一个2的倍数。<shift_imm>值的范围为0~31,移位后空出的位添0。循环器进位值为Rm最后移出的位。 (2)语法格式 <opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,LSR #<shift_imm> · <Rm>为被移位的寄存器; · LSR为逻辑右移操作标识; · <shift_imm>为逻辑右移位数,范围为0~31。 (3)操作伪代码 if shift_imm == 0 then /*执行寄存器操作*/ shifter_operand = 0 shifter_carry_out = Rm[31] else /*移位立即数大于零*/ shifter_operand = Rm logical_shift_Right shift_imm shifter_carry_out = Rm[shift_imm - 1] (4)说明 ① shift_imm的取值范围为0~31,当shift_imm=0时,移位位数为32,所以移位位数范围为1~32位。 ② 如果指令中的Rm或Rn指定为程序计数器r15,则操作数的值为当前指令地址加8。 6.<Rm>, LSR <Rs> (1)编码格式 指令的编码格式如图4.7所示。 图4.7 数据处理指令——寄存器逻辑右移寻址编码格式 此操作将寄存器Rm的数值逻辑右移一定的位数。移位的位数由Rs的最低8位bit[7∶0]决定。移出的位由0补齐。当Rs[7∶0]大于0而小于32时,进位标志C由最后移出的位决定,当Rs[7∶0]大于32时,进位标志位为0,当Rs[7∶0]等于0时,进位标志不变。 (2)语法格式 <opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,LSR <Rs> · <Rm>为指令被移位的寄存器; · LSR为逻辑右移操作标识; · <Rs>为包含逻辑右移位数的寄存器。 (3)操作伪代码 if Rs[7:0] = = 0 then shifter_operand = Rm shifter_carry_out = C flag else if Rs[7:0] < 32 then shifter_operand = Rm logical_shift_Right Rs[7:0] shifter_carry_out = Rm[Rs[7:0] - 1] else if Rs[7:0] = = 32 then shifter_operand = 0 shifter_carry_out = Rm[31] else /*Rs的后8位大于零*/ shifter_operand = 0 shifter_carry_out = 0 (4)说明 如果程序计数器r15被用作Rd、Rm、Rn或Rs中的任意一个,则指令的执行结果不可预知。 7.<Rm>, ASR #<shift_imm> (1)编码格式 指令的编码格式如图4.8所示。 图4.8 数据处理指令——立即数算术右移寻址编码格式 指令的操作数为寄存器Rm的数值逻辑右移<shift_imm>位。<shift_imm>的值范围为0~31,当<shift_imm>等于0时,移位位数为32,所以移位位数范围为1~32位。进位移位操作后,空出的位添Rm的最高位Rm[31]。进位标志为Rm最后被移出的数值。 (2)语法格式 <opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,ASR #<shift_imm> · <Rm>为被移位的寄存器; · ASR为算术右移操作标识; · <shift_imm>为算术右移位数,范围为1~32,当shift_imm等于0时移位位数为32。 (3)操作伪代码 if shift_imm == 0 then /*执行寄存器操作*/ if Rm[31] = = 0 then shifter_operand = 0 shifter_carry_out = Rm[31] else /*Rm[31] = = 1*/ shifter_operand = 0xffffffff shifter_carry_out = Rm[31] else /*shift_imm > 0*/ shifter_operand = Rm Arithmetic_shift_Right <shift_imm> shifter_carry_out = Rm[shift_imm - 1] (4)说明 ① 如果指令中的Rm或Rn指定为程序计数器r15,则操作数的值为当前指令地址加8。 8.<Rm>, ASR <Rs> (1)编码格式 指令的编码格式如图4.9所示。 图4.9 数据处理指令——寄存器算术右移寻址编码格式 此操作将寄存器Rm的数值算术右移一定的位数。移位后空缺的位由Rm的符号位(Rm[31])填充。位移的位数由Rs的最低8位bit[7∶0]决定。当Rs[7∶0]大于零而小于32时,指令的操作数为寄存器Rm的数值算术右移Rs[7∶0]位,进位标志C为Rm最后被移出的位。 (2)语法格式 <opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,ASR <Rs> · <Rm>为指令被移位的寄存器; · ASR为算术右移操作标识; · <Rs>为包含算术右移位数的寄存器。 (3)操作伪代码 if Rs[7:0] = = 0 then shifter_operand = Rm shifter_carry_out = C flag else if Rs[7:0] < 32 then shifter_operand = Rm Arithmeticl_shift_Right Rs[7:0] shifter_carry_out = Rm[Rs[7:0] - 1] if Rm[31] = =0 then shifter_operand = 0 shifter_carry_out = Rm[31] shifter_operand = 0xffffffff shifter_carry_out = Rm[31] (4)说明 如果程序计数器r15被用作Rd、Rm、Rn或Rs中的任意一个,则指令的执行结果不可预知。 9.<Rm>, ROR #<shift_imm> (1)编码格式 指令的编码格式如图4.10所示。 图4.10 数据处理指令——立即数循环右移寻址编码格式 指令的操作数由寄存器Rm的数值循环右移一定的位数得到。移位的位数由Rs的最低8位bits[7∶0]决定。当Rs[7∶0]=0时,指令的操作数为寄存器Rm的值,循环器的进位值为CPSR中的C条件标志位;否则,循环器的进位值为Rm最后被移出的位。 (2)语法格式 <opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,ROR #<shift_imm> · <Rm>为被移位的寄存器; · ROR为循环右移操作标识; · <shift_imm>为循环右移位数,范围为1~31,当shift_imm等于0时执行RRX操作。 (3)操作伪代码 if shift_imm == 0 then /*执行寄存器操作*/ 执行RRX操作 shifter_operand = Rm Rotate_Right shift_imm shifter_carry_out = Rm[shift_imm - 1] (4)说明 如果指令中的Rm或Rn指定为程序计数器r15,则操作数的值为当前指令地址加8。 10.<Rm>, ROR <Rs> (1)编码格式 指令的编码格式如图4.11所示。 图4.11 数据处理指令——寄存器循环右移寻址编码格式 指令的操作数由寄存器Rm的数值循环右移一定的位数。移位的位数由Rs的最低8位bits[7∶0]决定。当Rs[7∶0]=0时,指令的操作数为寄存器Rm的值,循环器的进位值为CPSR中的C条件标志位;否则,循环器的进位值为Rm最后被移出的位。 (2)语法格式 <opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,ROR <Rs> · <Rm>为指令被移位的寄存器; · ROR为循环右移操作标识; · <Rs>为包含循环右移位数的寄存器。 (3)操作伪代码 if Rs[7:0] = = 0 then shifter_operand = Rm shifter_carry_out = C flag else if Rs[4:0] == 0 then shifter_operand = Rm shifter_carry_out = Rm[31] shifter_operand = Rm Rotate_Right Rs[4:0] shifter_carry_out = Rm[Rs[4:0] - 1] (4)说明 如果程序计数器r15被用作Rd、Rm、Rn或Rs中的任意一个,则指令的执行结果不可预知。 11.<Rm>, RRX (1)编码格式 指令的编码格式如图4.12所示。 图4.12 数据处理指令——扩展右移寻址编码格式 指令的操作数为寄存器Rm的数值右移一位,并用CPSR中的C条件标志位填补空出的位。CPSR中的C条件标志位则用移出的位代替。 (2)语法格式 <opcode> {<cond>} {S} <Rd>,<Rn>,<Rm>,RRX · <Rm>为指令被移位的寄存器; · RRX为扩展的循环右移操作。 (3)操作伪代码 shifter_operand = (C flag logical_shift_left 31) OR (Rm logical_shift_Right 1) shifter_carry_out = Rm[0] (4)说明 ① 此种寻址方式的编码形式和“ROR #0”一致。 ② 如果程序计数器r15被用作Rd、Rm、Rn或Rs中的任意一个,则指令的执行结果不可预知。 ③ 可以实现ADC指令的功能。 sort 命令对 File 参数指定的文件中的行排序,并将结果写到标准输出。如果 File 参数指定多个文件,那么 sort 命令将这些文件连接起来,并当作一个文件进行排序。 sort语法 [root@www ~]# sort [-fbMnrtuk] [file or stdin] 选项与参数: -f :忽略大小写的差异,例如 A 与 a 视为编码相同; -b :忽略最前面的空格符部分; -M :以月份的名字来排序,例如 JAN, DEC 等等的排序方法; -n :使用『纯数字』进行排序(默认是以文字型态来排序的); -r :反向排序; -u :就是 uniq ,相同的数据中,仅出现一行代表; -t :分隔符,默认是用 [tab] 键来分隔; -k :以那个区间 (field) 来进行排序的意思 对/etc/passwd 的账号进行排序 [root@www ~]# cat /etc/passwd | sort adm:x:3:4:adm:/var/adm:/sbin/nologin apache:x:48:48:Apache:/var/www:/sbin/nologin bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin sort 是默认以第一个数据来排序,而且默认是以字符串形式来排序,所以由字母 a 开始升序排序。 /etc/passwd 内容是以 : 来分隔的,我想以第三栏来排序,该如何 [root@www ~]# cat /etc/passwd | sort -t ':' -k 3 root:x:0:0:root:/root:/bin/bash uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin operator:x:11:0:operator:/root:/sbin/nologin bin:x:1:1:bin:/bin:/sbin/nologin games:x:12:100:games:/usr/games:/sbin/nologin 默认是以字符串来排序的,如果想要使用数字排序: cat /etc/passwd | sort -t ':' -k 3n root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh 默认是升序排序,如果要倒序排序,如下 cat /etc/passwd | sort -t ':' -k 3nr nobody:x:65534:65534:nobody:/nonexistent:/bin/sh ntp:x:106:113::/home/ntp:/bin/false messagebus:x:105:109::/var/run/dbus:/bin/false sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin 如果要对/etc/passwd,先以第六个域的第2个字符到第4个字符进行正向排序,再基于第一个域进行反向排序。 cat /etc/passwd | sort -t':' -k 6.2,6.4 -k 1r sync:x:4:65534:sync:/bin:/bin/sync proxy:x:13:13:proxy:/bin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh sys:x:3:3:sys:/dev:/bin/sh 查看/etc/passwd有多少个shell:对/etc/passwd的第七个域进行排序,然后去重: cat /etc/passwd | sort -t':' -k 7 -u root:x:0:0:root:/root:/bin/bash syslog:x:101:102::/home/syslog:/bin/false daemon:x:1:1:daemon:/usr/sbin:/bin/sh sync:x:4:65534:sync:/bin:/bin/sync sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin uniq命令可以去除排序过的文件中的重复行,因此uniq经常和sort合用。也就是说,为了使uniq起作用,所有的重复行必须是相邻的。 uniq语法 [root@www ~]# uniq [-icu] 选项与参数: -i :忽略大小写字符的不同; -c :进行计数 -u :只显示唯一的行 testfile的内容如下 cat testfile hello world friend hello world hello 直接删除未经排序的文件,将会发现没有任何行被删除 #uniq testfile hello world friend hello world hello 排序文件,默认是去重 #cat words | sort |uniq friend hello world 排序之后删除了重复行,同时在行首位置输出该行重复的次数 #sort testfile | uniq -c 1 friend 3 hello 2 world 仅显示存在重复的行,并在行首显示该行重复的次数 #sort testfile | uniq -dc 3 hello 2 world 仅显示不重复的行 sort testfile | uniq -u friend cut命令可以从一个文本文件或者文本流中提取文本列。 cut语法 [root@www ~]# cut -d'分隔字符' -f fields <==用于有特定分隔字符 [root@www ~]# cut -c 字符区间 <==用于排列整齐的信息 选项与参数: -d :后面接分隔字符。与 -f 一起使用; -f :依据 -d 的分隔字符将一段信息分割成为数段,用 -f 取出第几段的意思; -c :以字符 (characters) 的单位取出固定字符区间; PATH 变量如下 [root@www ~]# echo $PATH /bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/X11R6/bin:/usr/games # 1 | 2 | 3 | 4 | 5 | 6 | 7 将 PATH 变量取出,我要找出第五个路径。 #echo $PATH | cut -d ':' -f 5 /usr/local/bin 将 PATH 变量取出,我要找出第三和第五个路径。 #echo $PATH | cut -d ':' -f 3,5 /sbin:/usr/local/bin 将 PATH 变量取出,我要找出第三到最后一个路径。 echo $PATH | cut -d ':' -f 3- /sbin:/usr/sbin:/usr/local/bin:/usr/X11R6/bin:/usr/games 将 PATH 变量取出,我要找出第一到第三个路径。 #echo $PATH | cut -d ':' -f 1-3 /bin:/usr/bin:/sbin: 将 PATH 变量取出,我要找出第一到第三,还有第五个路径。 echo $PATH | cut -d ':' -f 1-3,5 /bin:/usr/bin:/sbin:/usr/local/bin 实用例子:只显示/etc/passwd的用户和shell #cat /etc/passwd | cut -d ':' -f 1,7 root:/bin/bash daemon:/bin/sh bin:/bin/sh 统计文件里面有多少单词,多少行,多少字符。 [root@www ~]# wc [-lwm] 选项与参数: -l :仅列出行; -w :仅列出多少字(英文单字); -m :多少字符; 默认使用wc统计/etc/passwd #wc /etc/passwd 40 45 1719 /etc/passwd 40是行数,45是单词数,1719是字节数 wc的命令比较简单使用,每个参数使用如下: #wc -l /etc/passwd #统计行数,在对记录数时,很常用 40 /etc/passwd #表示系统有40个账户 #wc -w /etc/passwd #统计单词出现次数 45 /etc/passwd #wc -m /etc/passwd #统计文件的字节数 参考 http://vbird.dic.ksu.edu.tw/linux_basic/0320bash_6.php#pipe_2 http://www.cnblogs.com/stephen-liu74/archive/2011/11/10/2240461.html
grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。 Unix的grep家族包括grep、egrep和fgrep。egrep和fgrep的命令只跟grep有很小不同。egrep是grep的扩展,支持更多的re元字符, fgrep就是fixed grep或fast grep,它们把所有的字母都看作单词,也就是说,正则表达式中的元字符表示回其自身的字面意义,不再特殊。linux使用GNU版本的grep。它功能更强,可以通过-G、-E、-F命令行选项来使用egrep和fgrep的功能。 grep常用用法 [root@www ~]# grep [-acinv] [--color=auto] '搜寻字符串' filename 选项与参数: -a :将 binary 文件以 text 文件的方式搜寻数据 -c :计算找到 '搜寻字符串' 的次数 -i :忽略大小写的不同,所以大小写视为相同 -n :顺便输出行号 -v :反向选择,亦即显示出没有 '搜寻字符串' 内容的那一行! --color=auto :可以将找到的关键词部分加上颜色的显示喔! # grep root /etc/passwd root:x:0:0:root:/root:/bin/bash operator:x:11:0:operator:/root:/sbin/nologin # cat /etc/passwd | grep root root:x:0:0:root:/root:/bin/bash operator:x:11:0:operator:/root:/sbin/nologin # grep -n root /etc/passwd 1:root:x:0:0:root:/root:/bin/bash 30:operator:x:11:0:operator:/root:/sbin/nologin 在关键字的显示方面,grep 可以使用 --color=auto 来将关键字部分使用颜色显示。 这可是个很不错的功能啊!但是如果每次使用 grep 都得要自行加上 --color=auto 又显的很麻烦~ 此时那个好用的 alias 就得来处理一下啦!你可以在 ~/.bashrc 内加上这行:『alias grep='grep --color=auto'』再以『 source ~/.bashrc 』来立即生效即可喔! 这样每次运行 grep 他都会自动帮你加上颜色显示啦 将/etc/passwd,将没有出现 root 的行取出来 # grep -v root /etc/passwd root:x:0:0:root:/root:/bin/bash operator:x:11:0:operator:/root:/sbin/nologin 用 dmesg 列出核心信息,再以 grep 找出内含 eth 那行,要将捉到的关键字显色,且加上行号来表示: [root@www ~]# dmesg | grep -n --color=auto 'eth' 247:eth0: RealTek RTL8139 at 0xee846000, 00:90:cc:a6:34:84, IRQ 10 248:eth0: Identified 8139 chip type 'RTL-8139C' 294:eth0: link up, 100Mbps, full-duplex, lpa 0xC5E1 305:eth0: no IPv6 routers present # 你会发现除了 eth 会有特殊颜色来表示之外,最前面还有行号喔! 在关键字的显示方面,grep 可以使用 --color=auto 来将关键字部分使用颜色显示。 这可是个很不错的功能啊!但是如果每次使用 grep 都得要自行加上 --color=auto 又显的很麻烦~ 此时那个好用的 alias 就得来处理一下啦!你可以在 ~/.bashrc 内加上这行:『alias grep='grep --color=auto'』再以『 source ~/.bashrc 』来立即生效即可喔! 这样每次运行 grep 他都会自动帮你加上颜色显示啦 用 dmesg 列出核心信息,再以 grep 找出内含 eth 那行,在关键字所在行的前两行与后三行也一起捉出来显示 [root@www ~]# dmesg | grep -n -A3 -B2 --color=auto 'eth' 245-PCI: setting IRQ 10 as level-triggered 246-ACPI: PCI Interrupt 0000:00:0e.0[A] -> Link [LNKB] ... 247:eth0: RealTek RTL8139 at 0xee846000, 00:90:cc:a6:34:84, IRQ 10 248:eth0: Identified 8139 chip type 'RTL-8139C' 249-input: PC Speaker as /class/input/input2 250-ACPI: PCI Interrupt 0000:00:01.4[B] -> Link [LNKB] ... 251-hdb: ATAPI 48X DVD-ROM DVD-R-RAM CD-R/RW drive, 2048kB Cache, UDMA(66) # 如上所示,你会发现关键字 247 所在的前两行及 248 后三行也都被显示出来! # 这样可以让你将关键字前后数据捉出来进行分析啦! # grep ‘energywise’ * #在当前目录搜索带'energywise'行的文件 # grep -r ‘energywise’ * #在当前目录及其子目录下搜索'energywise'行的文件 # grep -l -r ‘energywise’ * #在当前目录及其子目录下搜索'energywise'行的文件,但是不显示匹配的行,只显示匹配的文件 这几个命令很使用,是查找文件的利器。 grep与正规表达式 字符类的搜索:如果我想要搜寻 test 或 taste 这两个单字时,可以发现到,其实她们有共通的 't?st' 存在~这个时候,我可以这样来搜寻: [root@www ~]# grep -n 't[ae]st' regular_express.txt 8:I can't finish the test. 9:Oh! The soup taste good. 其实 [] 里面不论有几个字节,他都谨代表某『一个』字节, 所以,上面的例子说明了,我需要的字串是『tast』或『test』两个字串而已! 字符类的反向选择 [^] :如果想要搜索到有 oo 的行,但不想要 oo 前面有 g,如下 [root@www ~]# grep -n '[^g]oo' regular_express.txt 2:apple is my favorite food. 3:Football game is not use feet only. 18:google is the best tools for search keyword. 19:goooooogle yes! 第 2,3 行没有疑问,因为 foo 与 Foo 均可被接受! 但是第 18 行明明有 google 的 goo 啊~别忘记了,因为该行后面出现了 tool 的 too 啊!所以该行也被列出来~ 也就是说, 18 行里面虽然出现了我们所不要的项目 (goo) 但是由於有需要的项目 (too) , 因此,是符合字串搜寻的喔! 至於第 19 行,同样的,因为 goooooogle 里面的 oo 前面可能是 o ,例如: go(ooo)oogle ,所以,这一行也是符合需求的! 字符类的连续:再来,假设我 oo 前面不想要有小写字节,所以,我可以这样写 [^abcd....z]oo , 但是这样似乎不怎么方便,由於小写字节的 ASCII 上编码的顺序是连续的, 因此,我们可以将之简化为底下这样: [root@www ~]# grep -n '[^a-z]oo' regular_express.txt 3:Football game is not use feet only. 也就是说,当我们在一组集合字节中,如果该字节组是连续的,例如大写英文/小写英文/数字等等, 就可以使用[a-z],[A-Z],[0-9]等方式来书写,那么如果我们的要求字串是数字与英文呢? 呵呵!就将他全部写在一起,变成:[a-zA-Z0-9]。 我们要取得有数字的那一行,就这样: [root@www ~]# grep -n '[0-9]' regular_express.txt 5:However, this dress is about $ 3183 dollars. 15:You are the best is mean you are the no. 1. 行首字符:如果我想要让 the 只在行首列出呢? 这个时候就得要使用定位字节了!我们可以这样做: [root@www ~]# grep -n '^the' regular_express.txt 12:the symbol '*' is represented as start. [root@www ~]# grep -n '^[a-z]' regular_express.txt 2:apple is my favorite food. 4:this dress doesn't fit me. 10:motorcycle is cheap than car. 12:the symbol '*' is represented as start. 18:google is the best tools for search keyword. 19:goooooogle yes! 20:go! go! Let's go. [root@www ~]# grep -n '^[^a-zA-Z]' regular_express.txt 1:"Open Source" is a good mechanism to develop programs. 21:# I am VBird ^ 符号,在字符类符号(括号[])之内与之外是不同的! 在 [] 内代表『反向选择』,在 [] 之外则代表定位在行首的意义! 那如果我想要找出来,行尾结束为小数点 (.) 的那一行: [root@www ~]# grep -n '\.$' regular_express.txt 1:"Open Source" is a good mechanism to develop programs. 2:apple is my favorite food. 3:Football game is not use feet only. 4:this dress doesn't fit me. 10:motorcycle is cheap than car. 11:This window is clear. 12:the symbol '*' is represented as start. 15:You are the best is mean you are the no. 1. 16:The world <Happy> is the same with "glad". 17:I like dog. 18:google is the best tools for search keyword. 20:go! go! Let's go. 假设我需要找出 g??d 的字串,亦即共有四个字节, 起头是 g 而结束是 d ,我可以这样做: [root@www ~]# grep -n 'g..d' regular_express.txt 1:"Open Source" is a good mechanism to develop programs. 9:Oh! The soup taste good. 16:The world <Happy> is the same with "glad". 因为强调 g 与 d 之间一定要存在两个字节,因此,第 13 行的 god 与第 14 行的 gd 就不会被列出来啦! 如果我想要列出有 oo, ooo, oooo 等等的数据, 也就是说,至少要有两个(含) o 以上,该如何是好? 因为 * 代表的是『重复 0 个或多个前面的 RE 字符』的意义, 因此,『o*』代表的是:『拥有空字节或一个 o 以上的字节』,因此,『 grep -n 'o*' regular_express.txt 』将会把所有的数据都列印出来终端上! 当我们需要『至少两个 o 以上的字串』时,就需要 ooo* ,亦即是: [root@www ~]# grep -n 'ooo*' regular_express.txt 1:"Open Source" is a good mechanism to develop programs. 2:apple is my favorite food. 3:Football game is not use feet only. 9:Oh! The soup taste good. 18:google is the best tools for search keyword. 19:goooooogle yes! 如果我想要字串开头与结尾都是 g,但是两个 g 之间仅能存在至少一个 o ,亦即是 gog, goog, gooog.... 等等,那该如何? [root@www ~]# grep -n 'goo*g' regular_express.txt 18:google is the best tools for search keyword. 19:goooooogle yes! [root@www ~]# grep -n 'g.*g' regular_express.txt 1:"Open Source" is a good mechanism to develop programs. 14:The gd software is a library for drafting programs. 18:google is the best tools for search keyword. 19:goooooogle yes! 20:go! go! Let's go. 因为是代表 g 开头与 g 结尾,中间任意字节均可接受,所以,第 1, 14, 20 行是可接受的喔! 这个 .* 的 RE 表示任意字符是很常见的. 如果我想要找出『任意数字』的行?因为仅有数字,所以就成为: [root@www ~]# grep -n '[0-9][0-9]*' regular_express.txt 5:However, this dress is about $ 3183 dollars. 15:You are the best is mean you are the no. 1. 我们可以利用 . 与 RE 字符及 * 来配置 0 个到无限多个重复字节, 那如果我想要限制一个范围区间内的重复字节数呢? 举例来说,我想要找出两个到五个 o 的连续字串,该如何作?这时候就得要使用到限定范围的字符 {} 了。 但因为 { 与 } 的符号在 shell 是有特殊意义的,因此, 我们必须要使用字符 \ 来让他失去特殊意义才行。 至於 {} 的语法是这样的,假设我要找到两个 o 的字串,可以是: [root@www ~]# grep -n 'o\{2\}' regular_express.txt 1:"Open Source" is a good mechanism to develop programs. 2:apple is my favorite food. 3:Football game is not use feet only. 9:Oh! The soup taste good. 18:google is the best tools for search ke 19:goooooogle yes! 假设我们要找出 g 后面接 2 到 5 个 o ,然后再接一个 g 的字串,他会是这样: [root@www ~]# grep -n 'go\{2,5\}g' regular_express.txt 18:google is the best tools for search keyword. 如果我想要的是 2 个 o 以上的 goooo....g 呢?除了可以是 gooo*g ,也可以是: [root@www ~]# grep -n 'go\{2,\}g' regular_express.txt 18:google is the best tools for search keyword. 19:goooooogle yes! # egrep 'NW|EA' testfile northwest NW Charles Main 3.0 .98 3 34 eastern EA TB Savage 4.4 .84 5 20 #grep 'NW\|EA' testfile northwest NW Charles Main 3.0 .98 3 34 eastern EA TB Savage 4.4 .84 5 20 # grep '3\+' testfile #这3条命令将会 northwest NW Charles Main 3.0 .98 3 34 western WE Sharon Gray 5.3 .97 5 23 northeast NE AM Main Jr. 5.1 .94 3 13 central CT Ann Stephens 5.7 .94 5 13 # grep -E '2\.?[0-9]' testfile # grep '2\.\?[0-9]' testfile #首先含有2字符,其后紧跟着0个或1个点,后面再是0和9之间的数字。 western WE Sharon Gray 5.3 .97 5 23 southwest SW Lewis Dalsass 2.7 .8 2 18 eastern EA TB Savage 4.4 .84 5 20 # egrep '(no)+' testfile # grep -E '(no)+' testfile # grep '\(no\)\+' testfile #3个命令返回相同结果, northwest NW Charles Main 3.0 .98 3 34 northeast NE AM Main Jr. 5.1 .94 3 13 north NO Margot Weber 4.5 .89 5 9 很多人安装虚拟机的时候,经常遇到不能上网的问题,而vmware有三种网络模式,对初学者来说也比较眼花聊乱,今天我就来基于虚拟机3种网络模式,帮大家普及下虚拟机上网的背景知识。(博文原创自http://www.cnblogs.com/ggjucheng/archive/2012/08/19/2646007.html) VMnet0:虚拟桥接(bridged)网络; VMnet1:虚拟Host-Only网络; VMnet8:虚拟NAT网络; VMware Network Adapter VMnet1:Host用于与Host-Only虚拟网络进行通信的虚拟网卡; VMware Network Adapter VMnet8:Host用于与NAT虚拟网络进行通信的虚拟网卡。 虚拟机网络模式 无论是vmware,virtual box,virtual pc等虚拟机软件,一般来说,虚拟机有三种网络模式: 2.NAT 3.Host-Only 初学者看到虚拟机有三种网络,估计就慌了,笔者也是。哪一种网络是适合自己的虚拟机呢? 桥接网络是指本地物理网卡和虚拟网卡通过VMnet0虚拟交换机进行桥接,物理网卡和虚拟网卡在拓扑图上处于同等地位,那么物理网卡和虚拟网卡就相当于处于同一个网段,虚拟交换机就相当于一台现实网络中的交换机,所以两个网卡的IP地址也要设置为同一网段。 所以当我们要在局域网使用虚拟机,对局域网其他pc提供服务时,例如提供ftp,提供ssh,提供http服务,那么就要选择桥接模式。 例如大学宿舍里有一个路由器,宿舍里四个人连接这个路由器,路由器的wanip就不理会了,这个ip是动态获取的,而lanip默认是192.168.1.1,子网掩码是255.255.255.0。而其他四个人是自动获取ip,假设四个人的ip是: A:192.168.1.100/255.255.255.0, B:192.168.1.101/255.255.255.0, C:192.168.1.102/255.255.255.0, D:192.168.1.103/255.255.255.0 那么虚拟机的ip可以设置的ip地址是192.168.1.2-192.168.1.99,192.168.1.104-192.168.1.254(网络地址全0和全1的除外,再除去ABCD四个人的ip地址) 那么虚拟机的ip地址可以设置为192.168.1.98/255.255.255.0,设置了这个ip地址,ABCD这四个人就可以通过192.168.1.98访问虚拟机了,如果虚拟机需要上外网,那么还需要配置虚拟机的路由地址,就是192.168.1.1了,这样,虚拟机就可以上外网了,但是,上网我们一般是通过域名去访问外网的,所以我们还需要为虚拟机配置一个dns服务器,我们可以简单点,把dns服务器地址配置为google的dns服务器:8.8.8.8,到此,虚拟机就可以上网了。 NAT模式中,就是让虚拟机借助NAT(网络地址转换)功能,通过宿主机器所在的网络来访问公网。 NAT模式中,虚拟机的网卡和物理网卡的网络,不在同一个网络,虚拟机的网卡,是在vmware提供的一个虚拟网络。 NAT和桥接的比较: (1) NAT模式和桥接模式虚拟机都可以上外网。 (2) 由于NAT的网络在vmware提供的一个虚拟网络里,所以局域网其他主机是无法访问虚拟机的,而宿主机可以访问虚拟机,虚拟机可以访问局域网的所有主机,因为真实的局域网相对于NAT的虚拟网络,就是NAT的虚拟网络的外网,不懂的人可以查查NAT的相关知识。 (3) 桥接模式下,多个虚拟机之间可以互相访问;NAT模式下,多个虚拟机之间也可以相互访问。 如果你建一个虚拟机,只是给自己用,不需要给局域网其他人用,那么可以选择NAT,毕竟NAT模式下的虚拟系统的TCP/IP配置信息是由VMnet8(NAT)虚拟网络的DHCP服务器提供的,只要虚拟机的网路配置是DHCP,那么你不需要进行任何其他的配置,只需要宿主机器能访问互联网即可,就可以让虚拟机联网了。 例如你想建多个虚拟机集群,作为测试使用,而宿主机可能是一个笔记本,ip不固定。这种应用场景,我们需要采用nat模式了,但是我们要考虑一个问题,虚拟机之间是需要互访的,默认采用dhcp,虚拟机的ip每次重启,ip都是不固定的,所以我们需要手工设置虚拟机的ip地址。 但是我们对虚拟机网卡所在的虚拟网络的信息还一无所知,例如虚拟机网络的路由地址,子网掩码,所以我们需要先查下nat虚拟网络的信息。 使用vmware,在Edit->Virtual Network Editor中配置好虚拟网络信息后看到下图所示,注意VMnet8,VMnet8相当于是本机的一个路由,虚拟机设置NAT后就通过这个路由进行上网的,可以查看其网络地址,路由地址,子网掩码。 选择VMnet8->NAT设置,可以看到子网ip显示为192.168.233.0,子网掩码是255.255.255.0,那路由地址呢,其实就是网关IP了,都是同个东西,这里是192.168.233.2。 接下来就好办了,在对应的虚拟机设置好ip,子网掩码,路由地址就可以上外网了,至于dns可以设置为8.8.8.8. Host-Only 在Host-Only模式下,虚拟网络是一个全封闭的网络,它唯一能够访问的就是主机。其实Host-Only网络和NAT网络很相似,不同的地方就是Host-Only网络没有NAT服务,所以虚拟网络不能连接到Internet。主机和虚拟机之间的通信是通过VMware Network Adepter VMnet1虚拟网卡来实现的。 Host-Only的宗旨就是建立一个与外界隔绝的内部网络,来提高内网的安全性。这个功能或许对普通用户来说没有多大意义,但大型服务商会常常利用这个功能。如果你想为VMnet1网段提供路由功能,那就需要使用RRAS,而不能使用XP或2000的ICS,因为ICS会把内网的IP地址改为192.168.0.1,但虚拟机是不会给VMnet1虚拟网卡分配这个地址的,那么主机和虚拟机之间就不能通信了。 在VMware的3中网络模式中,NAT模式是最简单的,基本不需要手动配置IP地址等相关参数。至于桥接模式则需要额外的IP地址,如果是在内网环境中还很容易,如果是ADSL宽带就比较麻烦了,ISP一般是不会大方的多提供一个公网IP的。 在桥接模式下,VMware虚拟出来的操作系统就像是局域网中的一独立的主机,它可以访问网内任何一台机器不过你需要多于一个的IP地址,并且需要手工为 虚拟系统配置IP地址子网掩码,而且还要和宿主机器处于同一网段,这样虚拟系统才能和宿主机器进行通信 如果你想利用VMware在局域网内新建一个虚拟服务器,为局域网用户提供网络服务,就应该选择桥接模式 2. NAT(网络地址转换模式) 使用NAT模式,就是让虚拟系统借助NAT(网络地址转换)功能,通过宿主机器所在的网络来访问公网也就是说,使用NAT模式可以实现在虚拟系统里访问互 联网NAT模式下的虚拟系统的TCP/IP配置信息是由VMnet8(NAT)虚拟网络的DHCP服务器提供的,无法进行手工修改,因此虚拟系统也就无法 和本局域网中的其他真实主机进行通讯采用NAT模式最大的优势是虚拟系统接入互联网非常简单,你不需要进行任何其他的配置,只需要宿主机器能访问互联网即 如果你想利用VMware安装一个新的虚拟系统,在虚拟系统中不用进行任何手工配置就能直接访问互联网,建议你采用NAT模式 3. Host-only(主机模式) 在某些特殊的网络调试环境中,要求将真实环境和虚拟环境隔离开,这时你就可采用Host-only模式在Host-only模式中,所有的虚拟系统是可以 相互通信的,但虚拟系统和真实的网络是被隔离开的可以利用Windows XP里面自带的Internet连接共享(实际上是一个简单的路由NAT)来让虚拟机 通过主机真实的网卡进行外网的访问虚拟系统的TCP/IP配置信息(如IP地址网关地址DNS服务器等),都是由VMnet1(Host-only)虚拟 网络的DHCP服务器来动态分配的。 如果你想利用VMware创建一个与网内其他机器相隔离的虚拟系统,进行某些特殊的网络调试工作,可以选择Host-only模式。 以上,就是 虚拟机VMware 3种网络模式(桥接、nat、Host-only)的工作原理。
sed 是一种在线编辑器,它一次处理一行内容。处理时,把当前处理的行存储在临时缓冲区中,称为“模式空间”(pattern space),接着用sed命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕。接着处理下一行,这样不断重复,直到文件末尾。文件内容并没有 改变,除非你使用重定向存储输出。Sed主要用来自动编辑一个或多个文件;简化对文件的反复操作;编写转换程序等。 sed使用参数 [root@www ~]# sed [-nefr] [动作] 选项与参数: -n :使用安静(silent)模式。在一般 sed 的用法中,所有来自 STDIN 的数据一般都会被列出到终端上。但如果加上 -n 参数后,则只有经过sed 特殊处理的那一行(或者动作)才会被列出来。 -e :直接在命令列模式上进行 sed 的动作编辑; -f :直接将 sed 的动作写在一个文件内, -f filename 则可以运行 filename 内的 sed 动作; -r :sed 的动作支持的是延伸型正规表示法的语法。(默认是基础正规表示法语法) -i :直接修改读取的文件内容,而不是输出到终端。 动作说明: [n1[,n2]]function n1, n2 :不见得会存在,一般代表『选择进行动作的行数』,举例来说,如果我的动作是需要在 10 到 20 行之间进行的,则『 10,20[动作行为] 』 function: a :新增, a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行)~ c :取代, c 的后面可以接字串,这些字串可以取代 n1,n2 之间的行! d :删除,因为是删除啊,所以 d 后面通常不接任何咚咚; i :插入, i 的后面可以接字串,而这些字串会在新的一行出现(目前的上一行); p :列印,亦即将某个选择的数据印出。通常 p 会与参数 sed -n 一起运行~ s :取代,可以直接进行取代的工作哩!通常这个 s 的动作可以搭配正规表示法!例如 1,20s/old/new/g 就是啦! 以行为单位的新增/删除 将 /etc/passwd 的内容列出并且列印行号,同时,请将第 2~5 行删除! [root@www ~]# nl /etc/passwd | sed '2,5d' 1 root:x:0:0:root:/root:/bin/bash 6 sync:x:5:0:sync:/sbin:/bin/sync 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown .....(后面省略)..... sed 的动作为 '2,5d' ,那个 d 就是删除!因为 2-5 行给他删除了,所以显示的数据就没有 2-5 行罗~ 另外,注意一下,原本应该是要下达 sed -e 才对,没有 -e 也行啦!同时也要注意的是, sed 后面接的动作,请务必以 '' 两个单引号括住喔! 只要删除第 2 行 nl /etc/passwd | sed '2d' 要删除第 3 到最后一行 nl /etc/passwd | sed '3,$d' 在第二行后(亦即是加在第三行)加上『drink tea?』字样! [root@www ~]# nl /etc/passwd | sed '2a drink tea' 1 root:x:0:0:root:/root:/bin/bash 2 bin:x:1:1:bin:/bin:/sbin/nologin drink tea 3 daemon:x:2:2:daemon:/sbin:/sbin/nologin .....(后面省略)..... 那如果是要在第二行前 nl /etc/passwd | sed '2i drink tea' 如果是要增加两行以上,在第二行后面加入两行字,例如『Drink tea or .....』与『drink beer?』 [root@www ~]# nl /etc/passwd | sed '2a Drink tea or ......\ > drink beer ?' 1 root:x:0:0:root:/root:/bin/bash 2 bin:x:1:1:bin:/bin:/sbin/nologin Drink tea or ...... drink beer ? 3 daemon:x:2:2:daemon:/sbin:/sbin/nologin .....(后面省略)..... 每一行之间都必须要以反斜杠『 \ 』来进行新行的添加喔!所以,上面的例子中,我们可以发现在第一行的最后面就有 \ 存在。 以行为单位的替换与显示 将第2-5行的内容取代成为『No 2-5 number』呢? [root@www ~]# nl /etc/passwd | sed '2,5c No 2-5 number' 1 root:x:0:0:root:/root:/bin/bash No 2-5 number 6 sync:x:5:0:sync:/sbin:/bin/sync .....(后面省略)..... 透过这个方法我们就能够将数据整行取代了! 仅列出 /etc/passwd 文件内的第 5-7 行 [root@www ~]# nl /etc/passwd | sed -n '5,7p' 5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin 6 sync:x:5:0:sync:/sbin:/bin/sync 7 shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown 可以透过这个 sed 的以行为单位的显示功能, 就能够将某一个文件内的某些行号选择出来显示。 数据的搜寻并显示 搜索 /etc/passwd有root关键字的行 nl /etc/passwd | sed '/root/p' 1 root:x:0:0:root:/root:/bin/bash 1 root:x:0:0:root:/root:/bin/bash 2 daemon:x:1:1:daemon:/usr/sbin:/bin/sh 3 bin:x:2:2:bin:/bin:/bin/sh 4 sys:x:3:3:sys:/dev:/bin/sh 5 sync:x:4:65534:sync:/bin:/bin/sync ....下面忽略 如果root找到,除了输出所有行,还会输出匹配行。 使用-n的时候将只打印包含模板的行。 nl /etc/passwd | sed -n '/root/p' 1 root:x:0:0:root:/root:/bin/bash 数据的搜寻并删除 删除/etc/passwd所有包含root的行,其他行输出 nl /etc/passwd | sed '/root/d' 2 daemon:x:1:1:daemon:/usr/sbin:/bin/sh 3 bin:x:2:2:bin:/bin:/bin/sh ....下面忽略 #第一行的匹配root已经删除了 数据的搜寻并执行命令 找到匹配模式eastern的行后, 搜索/etc/passwd,找到root对应的行,执行后面花括号中的一组命令,每个命令之间用分号分隔,这里把bash替换为blueshell,再输出这行: nl /etc/passwd | sed -n '/root/{s/bash/blueshell/;p}' 1 root:x:0:0:root:/root:/bin/blueshell 如果只替换/etc/passwd的第一个bash关键字为blueshell,就退出 nl /etc/passwd | sed -n '/bash/{s/bash/blueshell/;p;q}' 1 root:x:0:0:root:/root:/bin/blueshell 最后的q是退出。 数据的搜寻并替换 除了整行的处理模式之外, sed 还可以用行为单位进行部分数据的搜寻并取代。基本上 sed 的搜寻与替代的与 vi 相当的类似!他有点像这样: sed 's/要被取代的字串/新的字串/g' 先观察原始信息,利用 /sbin/ifconfig 查询 IP [root@www ~]# /sbin/ifconfig eth0 eth0 Link encap:Ethernet HWaddr 00:90:CC:A6:34:84 inet addr:192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0 inet6 addr: fe80::290:ccff:fea6:3484/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 .....(以下省略)..... 本机的ip是192.168.1.100。 将 IP 前面的部分予以删除 [root@www ~]# /sbin/ifconfig eth0 | grep 'inet addr' | sed 's/^.*addr://g' 192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0 接下来则是删除后续的部分,亦即: 192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0 将 IP 后面的部分予以删除 [root@www ~]# /sbin/ifconfig eth0 | grep 'inet addr' | sed 's/^.*addr://g' | sed 's/Bcast.*$//g' 192.168.1.100 一条sed命令,删除/etc/passwd第三行到末尾的数据,并把bash替换为blueshell nl /etc/passwd | sed -e '3,$d' -e 's/bash/blueshell/' 1 root:x:0:0:root:/root:/bin/blueshell 2 daemon:x:1:1:daemon:/usr/sbin:/bin/sh -e表示多点编辑,第一个编辑命令删除/etc/passwd第三行到末尾的数据,第二条命令搜索bash替换为blueshell。 直接修改文件内容(危险动作) sed 可以直接修改文件的内容,不必使用管道命令或数据流重导向! 不过,由於这个动作会直接修改到原始的文件,所以请你千万不要随便拿系统配置来测试! 我们还是使用下载的 regular_express.txt 文件来测试看看吧! 利用 sed 将 regular_express.txt 内每一行结尾若为 . 则换成 ! [root@www ~]# sed -i 's/\.$/\!/g' regular_express.txt 利用 sed 直接在 regular_express.txt 最后一行加入『# This is a test』 [root@www ~]# sed -i '$a # This is a test' regular_express.txt 由於 $ 代表的是最后一行,而 a 的动作是新增,因此该文件最后新增『# This is a test』! sed 的『 -i 』选项可以直接修改文件内容,这功能非常有帮助!举例来说,如果你有一个 100 万行的文件,你要在第 100 行加某些文字,此时使用 vim 可能会疯掉!因为文件太大了!那怎办?就利用 sed 啊!透过 sed 直接修改/取代的功能,你甚至不需要使用 vim 去修订! 参考 http://vbird.dic.ksu.edu.tw/linux_basic/0330regularex_2.php#sed http://www.cnblogs.com/stephen-liu74/archive/2011/11/17/2245130.html
密码学(cryptography):目的是通过将信息编码使其不可读,从而达到安全性。 明文(plain text):发送人、接受人和任何访问消息的人都能理解的消息。 密文(cipher text):明文消息经过某种编码后,得到密文消息。 加密(encryption):将明文消息变成密文消息。 解密(decryption):将密文消息变成明文消息。 算法:取一个输入文本,产生一个输出文本。 加密算法:发送方进行加密的算法。 解密算法:接收方进行解密的算法。 密钥(key):只有发送方和接收方理解的消息 对称密钥加密(Symmetric Key Cryptography):加密与解密使用相同密钥。 非对称密钥加密(Asymmetric Key Cryptography):加密与解密使用不同密钥。 2、相关的加密算法介绍 DES算法即数据加密标准,也称为数据加密算法。加密过程如下: 在SSL中会用到分组DES、三重DES算法等加密算法对数据进行加密。当然可以选用其他非DES加密算法,视情况而定,后面会详细介绍。 3、密钥交换算法 使用对称加密算法时,密钥交换是个大难题,所以Diffie和Hellman提出了著名的Diffie-Hellman密钥交换算法。 Diffie-Hellman密钥交换算法原理: (1)Alice与Bob确定两个大素数n和g,这两个数不用保密 (2)Alice选择另一个大随机数x,并计算A如下:A=gx mod n (3)Alice将A发给Bob (4)Bob选择另一个大随机数y,并计算B如下:B=gy mod n (5)Bob将B发给Alice (6)计算秘密密钥K1如下:K1=Bx mod n (7)计算秘密密钥K2如下:K2=Ay mod n K1=K2,因此Alice和Bob可以用其进行加解密 RSA加密算法是基于这样的数学事实:两个大素数相乘容易,而对得到的乘积求因子则很难。加密过程如下: (1)选择两个大素数P、Q (2)计算N=P*Q (3)选择一个公钥(加密密钥)E,使其不是(P-1)与(Q-1)的因子 (4)选择私钥(解密密钥)D,满足如下条件: (D*E) mod (P-1)(Q-1)=1 (5)加密时,明文PT计算密文CT如下: CT=PTE mod N (6)解密时,从密文CT计算明文PT如下: PT=CTDmodN 这也是SSL中会用一种密钥交换算法。 3、散列算法: 主要用于验证数据的完整性,即保证时消息在发送之后和接收之前没有被篡改对于SSL中使用到的散列算法有MD5、SHA-1。 4、数字证书: 数字证书其实就是一个小的计算机文件,其作用类似于我们的身份证、护照,用于证明身份,在SSL中,使用数字证书来证明自己的身份,而不是伪造的。 5、简单的总结: 在SSL中会使用密钥交换算法交换密钥;使用密钥对数据进行加密;使用散列算法对数据的完整性进行验证,使用数字证书证明自己的身份。好了,下面开始介绍SSL协议。 SSL介绍: 安全套接字(Secure Socket Layer,SSL)协议是Web浏览器与Web服务器之间安全交换信息的协议,提供两个基本的安全服务:鉴别与保密。 SSL是Netscape于1994年开发的,后来成为了世界上最著名的web安全机制,所有主要的浏览器都支持SSL协议。 目前有三个版本:2、3、3.1,最常用的是第3版,是1995年发布的。 SSL协议的三个特性 ① 保密:在握手协议中定义了会话密钥后,所有的消息都被加密。 ② 鉴别:可选的客户端认证,和强制的服务器端认证。 ③ 完整性:传送的消息包括消息完整性检查(使用MAC)。 SSL的位置 SSL介于应用层和TCP层之间。应用层数据不再直接传递给传输层,而是传递给SSL层,SSL层对从应用层收到的数据进行加密,并增加自己的SSL头。 SSL的工作原理 握手协议(Handshake protocol) 记录协议(Record protocol) 警报协议(Alert protocol) 1、握手协议 握手协议是客户机和服务器用SSL连接通信时使用的第一个子协议,握手协议包括客户机与服务器之间的一系列消息。SSL中最复杂的协议就是握手协议。该协议允许服务器和客户机相互验证,协商加密和MAC算法以及保密密钥,用来保护在SSL记录中发送的数据。握手协议是在应用程序的数据传输之前使用的。 每个握手协议包含以下3个字段 (1)Type:表示10种消息类型之一 (2)Length:表示消息长度字节数 (3)Content:与消息相关的参数 握手协议的4个阶段 1.1 建立安全能力 SSL握手的第一阶段启动逻辑连接,建立这个连接的安全能力。首先客户机向服务器发出client hello消息并等待服务器响应,随后服务器向客户机返回server hello消息,对client hello消息中的信息进行确认。 Client hello消息包括Version,Random,Session id,Cipher suite,Compression method等信息。 ClientHello 客户发送CilentHello信息,包含如下内容: (1)客户端可以支持的SSL最高版本号 (2)一个用于生成主秘密的32字节的随机数。(等会介绍主秘密是什么) (3)一个确定会话的会话ID。 (4)一个客户端可以支持的密码套件列表。 密码套件格式:每个套件都以“SSL”开头,紧跟着的是密钥交换算法。用“With”这个词把密钥交换算法、加密算法、散列算法分开,例如:SSL_DHE_RSA_WITH_DES_CBC_SHA, 表示把DHE_RSA(带有RSA数字签名的暂时Diffie-HellMan)定义为密钥交换算法;把DES_CBC定义为加密算法;把SHA定义为散列算法。 (5)一个客户端可以支持的压缩算法列表。 ServerHello服务器用ServerHello信息应答客户,包括下列内容 (1)一个SSL版本号。取客户端支持的最高版本号和服务端支持的最高版本号中的较低者。 (2)一个用于生成主秘密的32字节的随机数。(客户端一个、服务端一个) (3)会话ID (4)从客户端的密码套件列表中选择的一个密码套件 (5)从客户端的压缩方法的列表中选择的压缩方法 这个阶段之后,客户端服务端知道了下列内容: (1)SSL版本 (2)密钥交换、信息验证和加密算法 (3)压缩方法 (4)有关密钥生成的两个随机数。 1.2 服务器鉴别与密钥交换 服务器启动SSL握手第2阶段,是本阶段所有消息的唯一发送方,客户机是所有消息的唯一接收方。该阶段分为4步: (a)证书:服务器将数字证书和到根CA整个链发给客户端,使客户端能用服务器证书中的服务器公钥认证服务器。 (b)服务器密钥交换(可选):这里视密钥交换算法而定 (c)证书请求:服务端可能会要求客户自身进行验证。 (d)服务器握手完成:第二阶段的结束,第三阶段开始的信号 这里重点介绍一下服务端的验证和密钥交换。这个阶段的前面的(a)证书 和(b)服务器密钥交换是基于密钥交换方法的。而在SSL中密钥交换算法有6种:无效(没有密钥交换)、RSA、匿名Diffie-Hellman、暂时Diffie-Hellman、固定Diffie-Hellman、Fortezza。 在阶段1过程客户端与服务端协商的过程中已经确定使哪种密钥交换算法。 如果协商过程中确定使用RSA交换密钥,那么过程如下图: 这个方法中,服务器在它的第一个信息中,发送了RSA加密/解密公钥证书。不过,因为预备主秘密是由客户端在下一个阶段生成并发送的,所以第二个信息是空的。注意,公钥证书会进行从服务器到客户端的验证。当服务器收到预备主秘密时,它使用私钥进行解密。服务端拥有私钥是一个证据,可以证明服务器是一个它在第一个信息发送的公钥证书中要求的实体。 其他的几种密钥交换算法这里就不介绍了。可以参考Behrouz A.Forouzan著的《密码学与网络安全》。 1.3 客户机鉴别与密钥交换: 客户机启动SSL握手第3阶段,是本阶段所有消息的唯一发送方,服务器是所有消息的唯一接收方。该阶段分为3步: (a)证书(可选):为了对服务器证明自身,客户要发送一个证书信息,这是可选的,在IIS中可以配置强制客户端证书认证。 (b)客户机密钥交换(Pre-master-secret):这里客户端将预备主密钥发送给服务端,注意这里会使用服务端的公钥进行加密。 (c)证书验证(可选),对预备秘密和随机数进行签名,证明拥有(a)证书的公钥。 下面也重点介绍一下RSA方式的客户端验证和密钥交换。 这种情况,除非服务器在阶段II明确请求,否则没有证书信息。客户端密钥交换方法包括阶段II收到的由RSA公钥加密的预备主密钥。 阶段III之后,客户要有服务器进行验证,客户和服务器都知道预备主密钥。 1.4 完成 客户机启动SSL握手第4阶段,使服务器结束。该阶段分为4步,前2个消息来自客户机,后2个消息来自服务器。 1.5 密钥生成的过程 这样握手协议完成,下面看下什么是预备主密钥,主密钥是怎么生成的。为了保证信息的完整性和机密性,SSL需要有六个加密秘密:四个密钥和两个IV。为了信息的可信性,客户端需要一个密钥(HMAC),为了加密要有一个密钥,为了分组加密要一个IV,服务也是如此。SSL需要的密钥是单向的,不同于那些在其他方向的密钥。如果在一个方向上有攻击,这种攻击在其他方向是没影响的。生成过程如下: 记录协议在客户机和服务器握手成功后使用,即客户机和服务器鉴别对方和确定安全信息交换使用的算法后,进入SSL记录协议,记录协议向SSL连接提供两个服务: (1)保密性:使用握手协议定义的秘密密钥实现 (2)完整性:握手协议定义了MAC,用于保证消息完整性 记录协议的过程: 3、警报协议 客户机和服务器发现错误时,向对方发送一个警报消息。如果是致命错误,则算法立即关闭SSL连接,双方还会先删除相关的会话号,秘密和密钥。每个警报消息共2个字节,第1个字节表示错误类型,如果是警报,则值为1,如果是致命错误,则值为2;第2个字节制定实际错误类型。 SSL中,使用握手协议协商加密和MAC算法以及保密密钥 ,使用握手协议对交换的数据进行加密和签名,使用警报协议定义数据传输过程中,出现问题如何去解决。 参考网址:http://kb.cnblogs.com/page/162080/
问:如何实现单片以太网微控制器? 答:诀窍是将微控制器、以太网媒体接入控制器(MAC)和物理接口收发器(PHY)整合进同一芯片,这样能去掉许多外接元器件.这种方案可使MAC和PHY实现很好的匹配,同时还可减小引脚数、缩小芯片面积.单片以太网微控制器还降低了功耗,特别是在采用掉电模式的情况下. 问:以太网MAC是什么? 答:MAC即Media Access Control,即媒体访问控制子层协议.该协议位于OSI七层协议中数据链路层的下半部分,主要负责控制与连接物理层的物理介质.在发送数据的时候,MAC协议可以事先判断是否可以发送数据,如果可以发送将给数据加上一些控制信息,最终将数据以及控制信息以规定的格式发送到物理层;在接收数据的时候,MAC协议首先判断输入的信息并是否发生传输错误,如果没有错误,则去掉控制信息发送至LLC层.该层协议是以太网MAC由IEEE-802.3以太网标准定义.最新的MAC同时支持10Mbps和100Mbps两种速率. 以太网数据链路层其实包含MAC(介质访问控制)子层和LLC(逻辑链路控制)子层.一块以太网卡MAC芯片的作用不但要实现MAC子层和LLC子层的功能,还要提供符合规范的PCI界面以实现和主机的数据交换. MAC从PCI总线收到IP数据包(或者其他网络层协议的数据包)后,将之拆分并重新打包成最大1518Byte,最小64Byte的帧.这个帧里面包括了目标MAC地址、自己的源MAC地址和数据包里面的协议类型(比如IP数据包的类型用80表示).最后还有一个DWORD(4Byte)的CRC码. 可是目标的MAC地址是哪里来的呢?这牵扯到一个ARP协议(介乎于网络层和数据链路层的一个协议).第一次传送某个目的IP地址的数据的时候,先会发出一个ARP包,其MAC的目标地址是广播地址,里面说到:”谁是xxx.xxx.xxx.xxx这个IP地址的主人?”因为是广播包,所有这个局域网的主机都收到了这个ARP请求.收到请求的主机将这个IP地址和自己的相比较,如果不相同就不予理会,如果相同就发出ARP响应包.这个IP地址的主机收到这个ARP请求包后回复的ARP响应里说到:”我是这个IP地址的主人”.这个包里面就包括了他的MAC地址.以后的给这个IP地址的帧的目标MAC地址就被确定了.(其它的协议如IPX/SPX也有相应的协议完成这些操作.) IP地址和MAC地址之间的关联关系保存在主机系统里面,叫做ARP表,由驱动程序和操作系统完成.在Microsoft的系统里面可以用arp-a的命令查看ARP表.收到数据帧的时候也是一样,做完CRC以后,如果没有CRC效验错误,就把帧头去掉,把数据包拿出来通过标准的借口传递给驱动和上层的协议客栈,最终正确的达到我们的应用程序. 还有一些控制帧,例如流控帧也需要MAC直接识别并执行相应的行为. 以太网MAC芯片的一端接计算机PCI总线,另外一端就接到PHY芯片上,它们之间是通过MII接口链接的. 问:什么是MII? 答:MII即媒体独立接口,它是IEEE-802.3定义的以太网行业标准."媒体独立"表明在不对MAC硬件重新设计或替换的情况下,任何类型的PHY设备都可以正常工作.它包括一个数据接口,以及一个MAC和PHY之间的管理接口. 数据接口包括分别用于发送器和接收器的两条独立信道.每条信道都有自己的数据,时钟和控制信号.MII数据接口总共需要16个信号,包括TX_ER,TXD<3:0>,TX_EN,TX_CLK, COL,RXD<3:0>,RX_EX,RX_CLK,CRS,RX_DV等.MII以4位半字节方式传送数据双向传输,时钟速率25MHz.其工作速率可达100Mb/s; MII管理接口是个双信号接口,一个是时钟信号,另一个是数据信号.通过管理接口,上层能监视和控制PHY.其管理是使用SMI(Serial Management Interface)总线通过读写PHY的寄存器来完成的.PHY里面的部分寄存器是IEEE定义的,这样PHY把自己的目前的状态反映到寄存器里面,MAC通过SMI总线不断的读取PHY的状态寄存器以得知目前PHY的状态,例如连接速度,双工的能力等.当然也可以通过SMI设置PHY的寄存器达到控制的目的,例如流控的打开关闭,自协商模式还是强制模式等.不论是物理连接的MII总线和SMI总线还是PHY的状态寄存器和控制寄存器都是有IEEE的规范的,因此不同公司的MAC和PHY一样可以协调工作.当然为了配合不同公司的PHY的自己特有的一些功能,驱动需要做相应的修改. MII支持10Mbps和100Mbps的操作,一个接口由14根线组成,它的支持还是比较灵活的,但是有一个缺点是因为它一个端口用的信号线太多,如果一个8端口的交换机要用到112根线,16端口就要用到224根线,到32端口的话就要用到448根线,一般按照这个接口做交换机,是不太现实的,所以现代的交换机的制作都会用到其它的一些从MII简化出来的标准,比如RMII,SMII,GMII等. RMII是简化的MII接口,在数据的收发上它比MII接口少了一倍的信号线,所以它一般要求是50MHz的总线时钟.RMII一般用在多端口的交换机,它不是每个端口安排收,发两个时钟,而是所有的数据端口公用一个时钟用于所有端口的收发,这里就节省了不少的端口数目.RMII的一个端口要求7个数据线,比MII少了一倍,所以交换机能够接入多一倍数据的端口.和MII一样,RMII支持10Mbps和100Mbps的总线接口速度. SMII是由思科提出的一种媒体接口,它有比RMII更少的信号线数目,S表示串行的意思.因为它只用一根信号线传送发送数据,一根信号线传输接受数据,所以为了满足100Mbps的总线接口速度的需求,它的时钟频率就达到了125MHz,为什么用125MHz,是因为数据线里面会传送一些控制信息.SMII一个端口仅用4根信号线完成100Mbps的传输,比起RMII差不多又少了一倍的信号线.SMII在工业界的支持力度是很高的.同理,所有端口的数据收发都公用同一个外部的125MHz时钟. GMII是千兆网的MII接口,这个也有相应的RGMII接口,表示简化了的GMII接口. MII总线 在IEEE802.3中规定的MII总线是一种用于将不同类型的PHY与相同网络控制器(MAC)相连接的通用总线.网络控制器可以用同样的硬件接口与任何PHY . GMII(Gigabit MII) GMII采用8位接口数据,工作时钟125MHz,因此传输速率可达1000Mbps.同时兼容MII所规定的10/100 Mbps工作方式. GMII接口数据结构符合IEEE以太网标准.该接口定义见IEEE 802.3-2000. GTXCLK——吉比特TX..信号的时钟信号(125MHz) TXCLK——10/100Mbps信号时钟 TXD[7..0]——被发送数据 TXEN——发送器使能信号 TXER——发送器错误(用于破坏一个数据包) 注:在千兆速率下,向PHY提供GTXCLK信号,TXD,TXEN,TXER信号与此时钟信号同步.否则,在10/100Mbps速率下,PHY提供TXCLK时钟信号,其它信号与此信号同步.其工作频率为25MHz(100M网络)或2.5MHz(10M网络). RXCLK——接收时钟信号(从收到的数据中提取,因此与GTXCLK无关联) RXD[7..0]——接收数据 RXDV——接收数据有效指示 RXER——接收数据出错指示 COL——冲突检测(仅用于半双工状态) MDC——配置接口时钟 MDIO——配置接口I/O 管理配置接口控制PHY的特性.该接口有32个寄存器地址,每个地址16位.其中前16个已经在"IEEE 802.3,2000-22.2.4 Management Functions"中规定了用途,其余的则由各器件自己指定. RMII(Reduced Media Independant Interface) 简化媒体独立接口 是标准的以太网接口之一,比MII有更少的I/O传输. RMII口是用两根线来传输数据的,MII口是用4根线来传输数据的,GMII是用8根线来传输数据的.MII/RMII只是一种接口,对于10Mbps线速,MII的时钟速率是2.5MHz就可以了,RMII则需要5MHz;对于100Mbps线速,MII需要的时钟速率是25MHz,RMII则是50MHz. MII/RMII用于传输以太网包,在MII/RMII接口是4/2bit的,在以太网的PHY里需要做串并转换,编解码等才能在双绞线和光纤上进行传 输,其帧格式遵循IEEE 802.3(10M)/IEEE 802.3u(100M)/IEEE 802.1q(VLAN).以太网帧的格式为:前导符+开始位+目的mac地址+源mac地址+类型/长度+数据+padding(optional)+32bitCRC 如果有vlan,则要在类型/长度后面加上2个字节的vlan tag,其中12bit来表示vlan id,另外4bit表示数据的优先级! 问:以太网PHY是什么? 答:PHY是物理接口收发器,它实现物理层.IEEE-802.3标准定义了以太网PHY.包括MII/GMII(介质独立接口)子层,PCS(物理编码子层),PMA(物理介质附加)子层,PMD(物理介质相关)子层,MDI子层.它符合IEEE-802.3k中用于10BaseT(第14条)和100BaseTX(第24条和第25条)的规范. PHY在发送数据的时候,收到MAC过来的数据(对PHY来说,没有帧的概念,对它来说,都是数据而不管什么地址,数据还是CRC.对于100BaseTX因为使用4B/5B编码,每4bit就增加1bit的检错码),然后把并行数据转化为串行流数据,再按照物理层的编码规则把数据编码,再变为模拟信号把数据送出去.收数据时的流程反之.PHY还有个重要的功能就是实现CSMA/CD的部分功能.它可以检测到网络上是否有数据在传送,如果有数据在传送中就等待,一旦检测到网络空闲,再等待一个随机时间后将送数据出去.如果两个碰巧同时送出了数据,那样必将造成冲突,这时候,冲突检测机构可以检测到冲突,然后各等待一个随机的时间重新发送数据.这个随机时间很有讲究的,并不是一个常数,在不同的时刻计算出来的随机时间都是不同的,而且有多重算法来应付出现概率很低的同两台主机之间的第二次冲突. 许多网友在接入Internt宽带时,喜欢使用”抢线”强的网卡,就是因为不同的PHY碰撞后计算随机时间的方法设计上不同,使得有些网卡比较”占便宜”.不过,抢线只对广播域的网络而言的,对于交换网络和ADSL这样点到点连接到局端设备的接入方式没什么意义.而且”抢线”也只是相对而言的,不会有质的变化. 现在交换机的普及使得交换网络的普及,使得冲突域网络少了很多,极大地提高了网络的带宽.但是如果用HUB,或者共享带宽接入Internet的时候还是属于冲突域网络,有冲突碰撞的.交换机和HUB最大的区别就是:一个是构建点到点网络的局域网交换设备,一个是构建冲突域网络的局域网互连设备. 除此之外PHY还提供了和对端设备连接的重要功能并通过LED灯显示出自己目前的连接的状态和工作状态让我们知道.当我们给网卡接入网线的时候,PHY不断发出的脉冲信号检测到对端有设备,它们通过标准的”语言”交流,互相协商并却定连接速度、双工模式、是否采用流控等.通常情况下,协商的结果是两个设备中能同时支持的最大速度和最好的双工模式.这个技术被称为AutoNegotiation或者NWAY,它们是一个意思–自动协商. 具体传输过程为,发送数据时,网卡首先侦听介质上是否有载波(载波由电压指示),如果有,则认为其他站点正在传送信息,继续侦听介质.一旦通信介质在一定时间段内(称为帧间缝隙IFG=9.6微秒)是安静的,即没有被其他站点占用,则开始进行帧数据发送,同时继续侦听通信介质,以检测冲突.在发送数据期间,如果检测到冲突,则立即停止该次发送,并向介质发送一个“阻塞”信号,告知其他站点已经发生冲突,从而丢弃那些可能一直在接收的受到损坏的帧数据,并等待一段随机时间(CSMA/CD确定等待时间的算法是二进制指数退避算法).在等待一段随机时间后,再进行新的发送.如果重传多次后(大于16次)仍发生冲突,就放弃发送.接收时,网卡浏览介质上传输的每个帧,如果其长度小于64字节,则认为是冲突碎片.如果接收到的帧不是冲突碎片且目的地址是本地地址,则对帧进行完整性校验,如果帧长度大于1518字节(称为超长帧,可能由错误的LAN驱动程序或干扰造成)或未能通过CRC校验,则认为该帧发生了畸变.通过校验的帧被认为是有效的,网卡将它接收下来进行本地处理. 问:造成以太网MAC和PHY单片整合难度高的原因是什么? 答:PHY整合了大量模拟硬件,而MAC是典型的全数字器件.芯片面积及模拟/数字混合架构是为什么先将MAC集成进微控制器而将PHY留在片外的原因.更灵活、密度更高的芯片技术已经可以实现MAC和PHY的单芯片整合. 问: 网卡上除RJ-45接口外,还需要其它元件吗? 答:PHY和MAC是网卡的主要组成部分,网卡一般用RJ-45插口,10M网卡的RJ-45插口也只用了1,2,3,6四根针,而100M或1000M网卡的则是八根针都是全的.除此以外,还需要其它元件,因为虽然PHY提供绝大多数模拟支持,但在一个典型实现中,仍需外接6,7只分立元件及一个局域网绝缘模块.绝缘模块一般采用一个1:1的变压器.这些部件的主要功能是为了保护PHY免遭由于电气失误而引起的损坏. 另外,一颗CMOS制程的芯片工作的时候产生的信号电平总是大于0V的(这取决于芯片的制程和设计需求),但是这样的信号送到100米甚至更长的地方会有很大的直流分量的损失.而且如果外部网线直接和芯片相连的话,电磁感应(打雷)和静电,很容易造成芯片的损坏.再就是设备接地方法不同,电网环境不同会导致双方的0V电平不一致,这样信号从A传到B,由于A设备的0V电平和B点的0V电平不一样,这样会导致很大的电流从电势高的设备流向电势低的设备. 为了解决以上问题Transformer(隔离变压器)这个器件就应运而生.它把PHY送出来的差分信号用差模耦合的线圈耦合滤波以增强信号,并且通过电磁场的转换耦合到连接网线的另外一端.这样不但使网线和PHY之间没有物理上的连接而换传递了信号,隔断了信号中的直流分量,还可以在不同0V电平的设备中传送数据. 隔离变压器本身就是设计为耐2KV~3KV的电压的.也起到了防雷感应(我个人认为这里用防雷击不合适)保护的作用.有些朋友的网络设备在雷雨天气时容易被烧坏,大都是PCB设计不合理造成的,而且大都烧毁了设备的接口,很少有芯片被烧毁的,就是隔离变压器起到了保护作用. 隔离变压器本身是个被动元件,只是把PHY的信号耦合了到网线上,并没有起到功率放大的作用.那么一张网卡信号的传输的最长距离是谁决定的呢? 一张网卡的传输最大距离和与对端设备连接的兼容性主要是PHY决定的.但是可以将信号送的超过100米的PHY其输出的功率也比较大,更容易产生EMI的问题.这时候就需要合适的Transformer与之配合.作PHY的老大公司Marvell的PHY,常常可以传送180~200米的距离,远远超过IEEE的100米的标准. RJ-45的接头实现了网卡和网线的连接.它里面有8个铜片可以和网线中的4对双绞(8根)线对应连接.其中100M的网络中1,2是传送数据的,3,6是接收数据的.1,2之间是一对差分信号,也就是说它们的波形一样,但是相位相差180度,同一时刻的电压幅度互为正负.这样的信号可以传递的更远,抗干扰能力强.同样的,3,6也一样是差分信号. 网线中的8根线,每两根扭在一起成为一对.我们制作网线的时候,一定要注意要让1,2在其中的一对,3,6在一对.否则长距离情况下使用这根网线的时候会导致无法连接或连接很不稳定. 现在新的PHY支持AUTO MDI-X功能(也需要Transformer支持).它可以实现RJ-45接口的1,2上的传送信号线和3,6上的接收信号线的功能自动互相交换.有的PHY甚至支持一对线中的正信号和负信号的功能自动交换.这样我们就不必为了到底连接某个设备需要使用直通网线还是交叉网线而费心了.这项技术已经被广泛的应用在交换机和SOHO路由器上. 在1000Basd-T网络中,其中最普遍的一种传输方式是使用网线中所有的4对双绞线,其中增加了4,5和7,8来共同传送接收数据.由于1000Based-T网络的规范包含了AUTOMDI-X功能,因此不能严格确定它们的传出或接收的关系,要看双方的具体的协商结果. 一片网卡主要功能的实现就基本上是上面这些器件了. 其他的,还有一颗EEPROM芯片,通常是一颗93C46.里面记录了网卡芯片的供应商ID,子系统供应商ID,网卡的MAC地址,网卡的一些配置,如SMI总线上PHY的地址,BOOTROM的容量,是否启用BOOTROM引导系统等东西. 很多网卡上还有BOOTROM这个东西.它是用于无盘工作站引导操作系统的.既然无盘,一些引导用必需用到的程序和协议栈就放到里面了,例如RPL,PXE等.实际上它就是一个标准的PCI ROM.所以才会有一些硬盘写保护卡可以通过烧写网卡的BootRom来实现.其实PCI设备的ROM是可以放到主板BIOS里面的.启动电脑的时候一样可以检测到这个ROM并且正确识别它是什么设备的.AGP在配置上和PCI很多地方一样,所以很多显卡的BIOS也可以放到主板BIOS里面.这就是为什么板载的网卡我们从来没有看到过BOOTROM的原因. 最后就是电源部分了.大多数网卡现在都使用3.3V或更低的电压.有的是双电压的.因此需要电源转换电路. 而且网卡为了实现Wake on line功能,必须保证全部的PHY和MAC的极少一部分始终处于有电的状态,这需要把主板上的5V Standby电压转换为PHY工作电压的电路.在主机开机后,PHY的工作电压应该被从5V转出来的电压替代以节省5V Standby的消耗.(许多劣质网卡没有这么做). 有Wake on line功能的网卡一般还有一个WOL的接口.那是因为PCI2.1以前没有PCI设备唤醒主机的功能,所以需要着一根线通过主板上的WOL的接口连到南桥里面以实现WOL的功能.新的主板合网卡一般支持PCI2.2/2.3,扩展了PME#信号功能,不需要那个接口而通过PCI总线就可以实现唤醒功能. 我们现在来看两个图 MAC和PHY分开的以太网卡 MAC和PHY集成在一颗芯片的以太网卡 上图中各部件为: ①RJ-45接口 ②Transformer(隔离变压器) ③PHY芯片 ④MAC芯片 ⑤EEPROM ⑥BOOTROM插槽 ⑦WOL接头 ⑨电压转换芯片 ⑩LED指示灯 网卡的功能主要有两个:一是将电脑的数据封装为帧,并通过网线(对无线网络来说就是电磁波)将数据发送到网络上去;二是接收网络上其它设备传过来的帧,并将帧重新组合成数据,发送到所在的电脑中.网卡能接收所有在网络上传输的信号,但正常情况下只接受发送到该电脑的帧和广播帧,将其余的帧丢弃.然后,传送到系统CPU做进一步处理.当电脑发送数据时,网卡等待合适的时间将分组插入到数据流中.接收系统通知电脑消息是否完整地到达,如果出现问题,将要求对方重新发送. 问:10BaseT和100BaseTX PHY实现方式不同的原因何在? 答:两种实现的分组描述本质上是一样的,但两者的信令机制完全不同.其目的是阻止一种硬件实现容易地处理两种速度.10BaseT采用曼彻斯特编码,100BaseTX采用4B/5B编码. 问:什么是曼彻斯特编码? 答:曼彻斯特编码又称曼彻斯特相位编码,它通过相位变化来实现每个位(图2).通常,用一个时钟周期中部的上升沿表示“1”,下降沿表示“0”.周期末端的相位变化可忽略不计,但有时又可能需要将这种相位变化计算在内,这取决于前一位的值. 问:什么是4B/5B编码? 答:4B/5B编码是一种块编码方式.它将一个4位的块编码成一个5位的块.这就使5位块内永远至少包含2个“1”转换,所以在一个5位块内总能进行时钟同步.该方法需要25%的额外开销. 问:网卡的MAC和PHY间的关系? 答:网卡工作在osi的最后两层,物理层和数据链路层,物理层定义了数据传送与接收所需要的电与光信号、线路状态、时钟基准、数据编码和电路等,并向数据链路层设备提供标准接口.物理层的芯片称之为PHY.数据链路层则提供寻址机构、数据帧的构建、数据差错检查、传送控制、向网络层提供标准的数据接口等功能.以太网卡中数据链路层的芯片称之为MAC控制器.很多网卡的这两个部分是做到一起的.他们之间的关系是pci总线接mac总线,mac接phy,phy接网线(当然也不是直接接上的,还有一个变压装置). PHY和MAC之间是如何传送数据和相互沟通的.通过IEEE定义的标准的MII/GigaMII(Media Independed Interfade,介质独立界面)界面连接MAC和PHY.这个界面是IEEE定义的.MII界面传递了网络的所有数据和数据的控制.ETHERNET的接口实质是MAC通过MII总线控制PHY的过程. 问:网线上传输的是模拟信号还是数字信号? 答:是模拟信号.因为它传出和接收是采用的模拟的技术.虽然它传送的信息是数字的(并不是传送的信息是数字的信号就可以叫做数字信号). 简单的例子:我们知道电话是模拟信号,但是当我们拨号上网的时候,电话线里传送的是数字信息,但信号本身依旧是模拟的.然而ADSL同样是通过电话线传送的,却是数字信号.这取决于它传出和接受采用的技术. 问:若操作系统没有加载网卡驱动,网卡虽然在系统设备树上,但网卡接口创建不了,那网卡实际能不能接收到数据? 答:这里面有很多细节, 我根据Intel网卡的Spec大概写了写, 想尽量写的通俗一些,所以没有刻意用Spec里的术语,另外本文虽然讲的是MAC/PHY,但光口卡的(SERDES)也是类似的. PCI设备做reset以后进入D0uninitialized(非初始化的D0状态, 参考PCI电源管理规范),此时网卡的MAC和DMA都不工作,PHY是工作在一个特殊的低电源状态的; 操作系统创建设备树时,初始化这个设备,PCI命令寄存器的 Memory Access Enable or the I/O Access Enable bit会被enable, 这就是D0active.此时PHY/MAC就使能了; PHY被使能应该就可以接收物理链路上的数据了,否则不能收到FLP/NLP, PHY就不能建立物理连接.但这类包一般是流量间歇发送的; 驱动程序一般要通过寄存器来控制PHY, 比如自动协商speed/duplex, 查询物理链路的状态Link up/down; MAC被使能后, 如果没有驱动设置控制寄存器的一个位(CTRL.SLU )的话, MAC和PHY是不能通讯的, 就是说MAC不知道PHY的link已经ready, 所以收不到任何数据的.这位设置以后, PHY完成自协商, 网卡才会有个Link change的中断,知道物理连接已经Link UP了; 即使Link已经UP, MAC还需要enable接收器的一个位(RCTL.RXEN ),包才可以被接收进来,如果网卡被reset,这位是0,意味着所有的包都会被直接drop掉,不会存入网卡的 FIFO.老网卡在驱动退出前利用这位关掉接收.Intel的最新千兆网卡发送接收队列的动态配置就是依靠这个位的,重新配置的过程一定要关掉流量; 无论驱动加载与否, 发生reset后,网卡EEPOM里的mac地址会写入网卡的MAC地址过滤寄存器, 驱动可以去修改这个寄存器,现代网卡通常支持很多MAC地址,也就是说,MAC地址是可以被软件设置的.例如,Intel的千兆网卡就支持16个单播 MAC地址,但只有1个是存在EEPROM里的,其它是软件声称和设置的; 但如果驱动没有加载,网卡已经在设备树上,操作系统完成了步骤1-2的初始化,此时网卡的PHY应该是工作的,但因为没有人设置控制位(CTRL.SLU)来让MAC和PHY建立联系,所以MAC是不收包的.这个控制位在reset时会再设置成0; PHY可以被软件设置加电和断电, 断电状态除了接收管理命令以外,不会接收数据.另外,PHY还能工作在Smart Power Down模式下,link down就进入省电状态; 有些多口网卡,多个网口共享一个PHY, 所以BIOS里设置disbale了某个网口, 也未必会把PHY的电源关掉,反过来,也要小心地关掉PHY的电源; 要详细了解PHY,最终还是要熟悉IEEE以太网的相关协议. 本文引用:http://blog.csdn.net/woodstar123/article/details/3324368 http://blog.csdn.net/yayong/article/details/5334565
网络适配器又称网卡或网络接口卡(NIC),英文名Network Interface Card.它是使计算机联网的设备.平常所说的网卡就是将PC机和LAN连接的网络适配器.网卡(NIC) 插在计算机主板插槽中,负责将用户要传递的数据转换为网络上其它设备能够识别的格式,通过网络介质传输.数据在计算机总线中传输是并行方式即数据是肩并肩传输的,而在网络的物理缆线中说数据以串行的比特流方式传输的,网卡承担串行数据和并行数据间的转换.网卡在发送数据前要同接收网卡进行对话以确定最大可发送数据的大小,发送的数据量的大小,两次发送数据间的间隔,等待确认的时间,每个网卡在溢出前所能承受的最大数据量,数据传输的速度. 它的主要技术参数为带宽,总线方式,电气接口方式等.它的基本功能为:从并行到串行的数据转换,包的装配和拆装,网络存取控制,数据缓存和网络信号. 网卡的主要工作原理:发送数据时, 计算机把要传输的数据并行写到网卡的缓存,网卡对要传输的数据进编码(10M以太网使用曼切斯特码,100M 以太网使用差分曼切斯特码), 串行发到传输介质上.接收数据时, 则相反. 1. 网卡的基本构造 以最常见的PCI 接口的网卡为例,一块网卡主要由 PCB 线路板,主芯片,数据汞,金手指(总线插槽接口) ,BOOTROM,EEPROM,晶振,RJ45接口,指示灯,固定片等等,以及一些二极管,电阻电容等组成.网卡包括硬件和固件程序(只读存储器中的软件例程),该固件程序实现逻辑链路控制和媒体访问控制的功能,还记录唯一的硬件地址即mac地址,网卡上一般有缓存.网卡须分配中断irq及基本i/o端口地址,同时还须设置基本内存地址(base memory address)和收发器(transceiver) 网卡的控制芯片:网卡中最重要元件,是网卡的控制中心,有如电脑的cpu,控制着整个网卡的工作,负责数据的传送和连接时的信号侦测.早期的10/100Mbps的双速网卡会采用两个控制芯片(单元)分别用来控制两个不同速率环境下的运算,而目前较先进的产品通常只有一个芯片控制两种速度. 常见的 10/100/1000M bps自适应网卡芯片有 Intel 的8254* 系列,Broadcom 的BCM57**系列,Marvell的 88E8001/88E8053/88E806*系列,Realtek的RTL8169S-32/64,RTL8110S-32/64(LOM),RTL8169SB,RTL8110SB(LOM) ,RTL8168(PCI Express) ,RTL8111(LOM,PCI Express) 系列,VIA 的VT612*系列等等. 晶体震荡器:负责产生网卡所有芯片的运算时钟,其原理就象主板上的晶体震荡器一样,通常网卡是使用20或25hz的晶体震荡器.千兆网卡使用62.5MHz或者125MHz晶振. boot rom插槽:如无特殊要求网卡中的这个插槽处在空置状态.一般是和boot rom芯片搭配使用,其主要作用是引导电脑通过服务器引导进入操作系统.boot rom就是启动芯片,让电脑可以在不具备硬盘,软驱和光驱的情况下,直接通过服务器开机,成为一个无硬盘无软驱的工作站.没有软驱就无法将资料输出,这样也可以达到资料保密的功能.同时,还可以节省下购买这些电脑部件的费用.在使用boot rom时要注意自己使用何种网络操作系统,通常有boot rom for nt,boot rom for unix,boot rom for netware等,boot rom启动芯片要自行购买. eeprom:从前的老式网卡都要靠设置跳线或是dip开关来设定irq,dma和i/o port等值,而现在的网卡则都使用软件设定,几乎看不见跳线的存在.各种网卡的状态和网卡的信息等数据都存在这颗小小的eeprom里,通过它来自动设置.里面记录了网卡芯片的供应商ID,子系 统供应商ID,网卡的MAC地址,网卡的一些配置,如SMI总线上PHY的地址,BOOTROM的容量, 是否启用BOOTROM引导系统等东西 数据汞:这是消费级PCI 网卡上都具备的设备,数据汞也被叫做网络变压器或可称为网络隔离变压器.它在一块网卡上所起的作用主要有两个,一是传输数据,它把 PHY 送出来的差分信号用差模耦合的线圈耦合滤波以增强信号,并且通过电磁场的转换耦合到不同电平的连接网线的另外一端;一是隔离网线连接的不同网络设备间的不同电平,以防止不同电压通过网线传输损坏设备.除此而外,数据汞还能对设备起到一定的防雷保护作用. rj-45和bnc接头: rj-45是采用双绞线作为传输媒介的一种网卡接口,在100mbps网中最常应用.bnc是采用细同轴电缆作为传输媒介. 信号指示灯:在网卡后方会有二到三个不等的信号灯,其作用是显示目前网络的连线状态,通常具有tx和rx两个信息.tx代表正在送出资料,rx代表正在接收资料,若看到两个灯同时亮则代表目前是处于全双工的运作状态,也可由此来辨别全双工的网卡是否处于全双工的网络环境中.也有部分低速网卡只用一个灯来表示信号,通过不同的灯光变换来表示网络是否导通. WOL:有些网卡会有WOL的功能, WOL网络开机的功能(wake on line).它可由另外一台电脑,使用软件制作特殊格式的信息包发送至一台装有具wol功能网卡的电脑,而该网卡接收到这些特殊格式的信息包后,就会命令电脑打开电源,目前已有越来越多的网卡支持网络开机的功能. 2. 网卡的分类 以传输速率可分为: 10Mbps网卡,100Mbps网卡,1000Mbps网卡,10GMbps网卡.目前常见的三种架构有10baset,100basetx与base2,前两者是以rj-45双绞线为传输媒介,传输速率分别为10Mbps和100Mbps.而双绞线又分为category 1至category 5五种规格,分别有不同的用途以及频宽,category通常简称cat,只要使用cat5规格的双绞线皆可用于10/100mbps的网卡上.而10base2架构则是使用细同轴电缆作为传输媒介,传输速率只有10Mbps.这里提到的10Mbps或100Mbps是指网卡上的最大传送速率,而并不等于网络上实际的传送速度,实际速度要考虑到传送的距离,线路的品质,和网络上是否拥挤等因素,这里所谈的bps指的是每秒传送的bit(1个byte=8个bit).而100Mbps则称为高速以太网卡(fast ethernet),多为PCI/PCI-E接口.当前市面上的pci网卡多具有10/100/1000Mbps自动切换的功能,会根据所在的网络连线环境来自动调节网络速度.1000 Mbps以太网卡多用于交换机或交换机与服务器之间的高速链路或backbone. 以接口类型可分为: ISA接口网卡,PCI/ PCI-X/ PCI-E接口网卡,USB接口网卡和笔记本电脑专用的PCMCIA接口.现在的ISA接口的网卡均采用16bit的总线宽度,其特性是采用programmed i/o的模式传送资料,传送数据时必须通过cpu在i/o上开出一个小窗口,作为网卡与pc之间的沟通管道,需要占用较高的cpu使用率,在传送大量数据时效率较差. PCI接口的网卡则采用32bit的总线频宽,采用bus master的数据传送方式,传送数据是由网卡上的控制芯片来控制,不必通过i/o端口和cpu,可大幅降低cpu的占用率,目前产品多为10/100Mbps双速自动侦测切换网卡. 以传输方式可分为: 半双工网卡,全双工网卡.半双工网卡无法同一时间内完成接收与传送数据的动作,如10base2使用细同轴电缆的网络架构就是半双工网络,同一时间内只能进行传送或接收数据的工作,效率较低.要使用全双工的网络就必须要使用双绞线作为传输线才能达到,并且也要搭配使用全双工的集线器,要使用10base或100basetx的网络架构,网卡当然也要是全双工的产品. 以传输介质可分为: rj-45双绞线的网卡与bnc的同轴电缆两种,有的网卡同时具有两种接头,可适用于两种网络线,但无法两个接头同时使用.另外还有光纤接口的网卡,通常带宽在1000 Mbps. 其它网卡: 从网络传输的物理媒介上还有无线网卡,利用2.4GHz的无线电波来传输数据.目前ieee有两种规范802.11和802.11b,最高传输速率分别为2Mbps和11Mbps,接口有PCI,USB和PCMCIA几种.
diff [选项] 源文件 目标文件 在最简单的情况是, diff 比较两个文件的内容 (源文件 和 目标文件). 文件名可以是 - 由标准输入设备读入的文本. 作为特别的情况是, diff - - 比较一份标准输入的它自己的拷贝如果 源文件 是一个目录和 目标文件 不是(目录), diff 会比较在 源文件(目录) 里的文件的中和 目标文件同名的(文件), 反过来也一样. 非目录文件不能是 -. 如果 源文件 和 目标文件 都是目录, diff 比较两个目录中相应的文件,依照字母次序排序;这个比较是不会递归的,除非给出 -r 或者 --recursive. diff 不把一个目录的内容看为它是一个文件来比较。被指定的文件不能是标准的输入, 因为标准的输入是无名的并且"有一样的名字的文件"的观点不适用。 diff 的选项由 -, 开始所以正常地 源文件(名) 和 目标文件(名) 不可以用 - 开头. 然而, -- 可以被它视为保留的即使作为文件名的开头( they begin with -.) 下面是 GNU所接受的 diff 的所有选项的概要. 大多数的选项有两个相同的名字,一个是单个的跟在 - 后面字母, 另一个是由 -- 引出的长名字. 多个单字母选项(除非它们产生歧义)能够组合为单行的命令行语法-ac 是等同于 -a -c. 长命名的选项能被缩短到他们的名字的任何唯一的前缀. 用 ([ 和 ]) 括起来显示选项产生歧义的选项 -行数(一个整数) 显示上下文 行数 (一个整数). 这个选项自身没有指定输出格式,这是没有效果的,除非和 -c 或者 -u 组合使用. 这是已废置的选项,对于正确的操作, 上下文至少要有两行。 所有的文件都视为文本文件来逐行比较,甚至他们似乎不是文本文件. 忽略空格引起的变化. 忽略插入删除空行引起的变化. --brief 仅报告文件是否相异,在乎差别的细节. 使用上下文输出格式. -C 行数(一个整数) --context[=lines] 使用上下文输出格式,显示以指定 行数 (一个整数), 或者是三行(当 行数 没有给出时. 对于正确的操作, 上下文至少要有两行. --changed-group-format=format 使用 format 输出一组包含两个文件的不同处的行,其格式是 if-then-else . 改变算法也许发现变化的一个更小的集合.这会使 diff 变慢 (有时更慢). -D name 合并 if-then-else 格式输出, 预处理宏(由name参数提供)条件. 输出为一个有效的 ed 脚本. --exclude=pattern 比较目录的时候,忽略和目录中与 pattern(样式) 相配的. --exclude-from=file 比较目录的时候,忽略和目录中与任何包含在 file(文件) 的样式相配的文件和目录. --expand-tabs 在输出时扩展tab为空格,保护输入文件的tab对齐方式 产生一个很象 ed 脚本的输出,但是但是在他们在文件出现的顺序有改变 -F regexp 在上下文和统一格式中,对于每一大块的不同,显示出匹配 regexp. 的一些前面的行. --forward-ed 产生象 ed 脚本的输出,但是它们在文件出现的顺序有改变。 这选项现在已没作用,它呈现Unix的兼容性. 使用启发规则加速操作那些有许多离散的小差异的大文件. --horizon-lines=lines 比较给定行数的有共同前缀的最后行,和有共同或缀的最前行. 忽略大小写. -I regexp 忽略由插入,删除行(由regexp 参数提供参考)带来的改变. --ifdef=name 合并 if-then-else 格式输出, 预处理宏(由name参数提供)条件. --ignore-all-space 在比较行的时候忽略空白. --ignore-blank-lines 忽略插入和删除空行 --ignore-case 忽略大小写. --ignore-matching-lines=regexp 忽略插入删除行(由regexp 参数提供参考). --ignore-space-change 忽略空白的数量. --initial-tab 在文本行(无论是常规的或者格式化的前后文关系)前输出tab代替空格. 引起的原因是tab对齐方式看上去象是常规的一样. 产生通过 pr 编码的输出. -L label --label=label 使用 label 给出的字符在文件头代替文件名输出. --left-column 以并列方式印出两公共行的左边 --line-format=format 使用 format 输出所有的行,在 if-then-else 格式中. --minimal 改变算法也许发现变化的一个更小的集合.这会使 diff 变慢 (有时更慢). 输出 RC-格式 diffs; 除了每条指令指定的行数受影响外 象 -f 一样。 --new-file 在目录比较中,如果那个文件只在其中的一个目录中找到,那么它被视为在另一个目录中是一个空文件. --new-group-format=format 使用 format 以if-then-else 格式输出只在第二个文件中取出的一个行组 --new-line-format=format 使用 format 以if-then-else 格式输出只在第二个文件中取出的一行 --old-group-format=format 使用 format 以if-then-else 格式输出只在第一个文件中取出的一个行组 --old-line-format=format 使用 format 使用 format 以if-then-else 格式输出只在第一个文件中取出的一行 显示带有c函数的改变. 在目录比较中,如果那个文件只在其中的一个目录中找到,那么它被视为在另一个目录中是一个空文件. --paginate 产生通过 pr 编码的输出. 仅报告文件是否相异,不报告详细的差异. 当比较目录时,递归比较任何找到的子目录. --rcs 输出 RC-格式 diffs; 除了每条指令指定的行数受影响外 象 -f 一样。 --recursive 当比较目录时,递归比较任何找到的子目录. --report-identical-files 报告两个文件相同. -S file 当比较目录时,由 file 开始. 这用于继续中断了的比较. --sdiff-merge-assist 打印附加的信息去帮助 sdiff. sdiff 在运行 diff 时使用这些选项. 这些选项不是特意为使用者直接使用而准备的。 --show-c-function 显示带有c函数的改变. --show-function-line=regexp 在上下文和统一的格式,对于每一大块的差别,显示出匹配 regexp. 的一些前面的行 --side-by-side 使用并列的输出格式. --speed-large-files 使用启发规则加速操作那些有许多离散的小差异的大文件. --starting-file=file 当比较目录时,由 file 开始. 这用于继续中断了的比较. --suppress-common-lines 在并列格式中不印出公共行。 在输出时扩展tab为空格,保护输入文件的tab对齐方式 在文本行(无论是常规的或者格式化的前后文关系)前输出tab代替空格.引起的原因是tab对齐方式看上去象是常规的一样. --text 所有的文件都视为文本文件来逐行比较,甚至他们似乎不是文本文件. 使用统一的输出格式. --unchanged-group-format=format 使用 format 输出两个文件的公共行组,其格式是if-then-else. --unchanged-line-format=format 使用 format 输出两个文件的公共行,其格式是if-then-else. --unidirectional-new-file 在目录比较中,如果那个文件只在其中的一个目录中找到,那么它被视为在另一个目录中是一个空文件. -U lines --unified[=lines] 使用前后关系格式输出,显示以指定 行数 (一个整数), 或者是三行(当 行数 没有给出时. 对于正确的操作, 上下文至少要有两行. --version 输出 diff 版本号. 在比较行时忽略空格 -W columns --width=columns 在并列格式输出时,使用指定的列宽. -x pattern 比较目录的时候,忽略和目录中与 pattern(样式) 相配的. -X file 比较目录的时候,忽略和目录中与任何包含在 file(文件) 的样式相配的文件和目录. 使用并列格式输出 实例:有这样两个文件:程序清单1 :hello.c#include int main(void) char msg[] = "Hello world!"; puts(msg); printf("Welcome to use diff commond.\n"); return 0; }程序清单2:hello_diff.c#include #include int main(void) char msg[] = "Hello world,fome hello_diff.c"; puts(msg); printf("hello_diff.c says,'Here you are,using diff.'\n"); return 0; }我们使用diff命令来查看这两个文件的不同之处,有一下几种方便的方法: 1、普通格式输出:[root@localhost diff]# diff hello.c hello_diff.c > #include char msg[] = "Hello world,fome hello_diff.c"; printf("hello_diff.c says,'Here you are,using diff.'\n"); [root@localhost diff]# 上面的“1a2”表示后面的一个文件"hello_diff.c"比前面的一个文件"hello.c"多了一行 "5c6"表示第一个文件的第5行与第二个文件的第6行有区别 2、并排格式输出[root@localhost diff]# diff hello.c hello_diff.c -y -W 130 #include #include > #include int main(void) int main(void) { { char msg[] = "Hello world!"; | char msg[] = "Hello world,fome hello_diff.c"; puts(msg); puts(msg); printf("Welcome to use diff commond.\n"); | printf("hello_diff.c says,'Here you are,using diff.'\ return 0; return 0; } } [root@localhost diff]# 这种并排格式的对比一目了然,可以快速找到不同的地方。 -W选择可以指定输出列的宽度,这里指定输出列宽为130 3、上下文输出格式[root@localhost diff]# diff hello.c hello_diff.c -c *** hello.c 2007-09-25 17:54:51.000000000 +0800 --- hello_diff.c 2007-09-25 17:56:00.000000000 +0800 *************** *** 1,11 **** #include int main(void) ! char msg[] = "Hello world!"; puts(msg); ! printf("Welcome to use diff commond.\n"); return 0; --- 1,12 ---- #include + #include int main(void) ! char msg[] = "Hello world,fome hello_diff.c"; puts(msg); ! printf("hello_diff.c says,'Here you are,using diff.'\n"); return 0; [root@localhost diff]# 这种方式在开头两行作了比较文件的说明,这里有三中特殊字符:+ 比较的文件的后者比前着多一行- 比较的文件的后者比前着少一行 ! 比较的文件两者有差别的行 4、统一输出格式[root@localhost diff]# diff hello.c hello_diff.c -u --- hello.c 2007-09-25 17:54:51.000000000 +0800 +++ hello_diff.c 2007-09-25 17:56:00.000000000 +0800 @@ -1,11 +1,12 @@ #include +#include int main(void) - char msg[] = "Hello world!"; + char msg[] = "Hello world,fome hello_diff.c"; puts(msg); - printf("Welcome to use diff commond.\n"); + printf("hello_diff.c says,'Here you are,using diff.'\n"); return 0; [root@localhost diff]# 正如看到的那样,统一格式的输出更加紧凑,所以更易于理解,更易于修改。 5、其他假如你想查看两个文件是否不同又不想显示差异之处的话,可以加上-q选项:[root@localhost diff]# diff hello.c hello_diff.c -q Files hello.c and hello_diff.c differ [root@localhost diff]# 另外你还可以提供一些匹配规则来忽略某中差别,可以用 -I regexp[root@localhost diff]# diff hello.c hello_diff.c -c -I include *** hello.c 2007-09-25 17:54:51.000000000 +0800 --- hello_diff.c 2007-09-25 17:56:00.000000000 +0800 *************** *** 2,11 **** int main(void) ! char msg[] = "Hello world!"; puts(msg); ! printf("Welcome to use diff commond.\n"); return 0; --- 3,12 ---- int main(void) ! char msg[] = "Hello world,fome hello_diff.c"; puts(msg); ! printf("hello_diff.c says,'Here you are,using diff.'\n"); return 0; [root@localhost diff]# 这里通过“ -I include”选项来忽略带有“ include”字样的行 部分内容来自:http://blog.chinaunix.net/u1/48325/showart_433936.html http://hi.baidu.com/ilotus_y/blog/item/b94231d6f55a1e2a07088b91.html 1. Webbench Webbench是一个在linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL,测试网站在压力下工作的性能,最多可以模拟3万个并发连接去测试网站的负载能力。Webbench使用C语言编写, 代码实在太简洁,源码加起来不到600行。 下载链接:http://home.tiscali.cz/~cz210552/webbench.html 2. Tinyhttpd tinyhttpd是一个超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client,可以通过阅读这段代码理解一个 Http Server 的本质。 下载链接:http://sourceforge.net/projects/tinyhttpd/ 3. cJSON cJSON是C语言中的一个JSON编解码器,非常轻量级,C文件只有500多行,速度也非常理想。 cJSON也存在几个弱点,虽然功能不是非常强大,但cJSON的小身板和速度是最值得赞赏的。其代码被非常好地维护着,结构也简单易懂,可以作为一个非常好的C语言项目进行学习。 项目主页: http://sourceforge.net/projects/cjson/ 4. CMockery cmockery是google发布的用于C单元测试的一个轻量级的框架。它很小巧,对其他开源包没有依赖,对被测试代码侵入性小。cmockery的源代码行数不到3K,你阅读一下will_return和mock的源代码就一目了然了。 主要特点: 免费且开源,google提供技术支持; 轻量级的框架,使测试更加快速简单; 避免使用复杂的编译器特性,对老版本的编译器来讲,兼容性好; 并不强制要求待测代码必须依赖C99标准,这一特性对许多嵌入式系统的开发很有用 下载链接:http://code.google.com/p/cmockery/downloads/list 5. Libev libev是一个开源的事件驱动库,基于epoll,kqueue等OS提供的基础设施。其以高效出名,它可以将IO事件,定时器,和信号统一起来,统一放在事件处理这一套框架下处理。基于Reactor模式,效率较高,并且代码精简(4.15版本8000多行),是学习事件驱动编程的很好的资源。 下载链接:http://software.schmorp.de/pkg/libev.html 6. Memcached Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提供动态数据库驱动网站的速度。Memcached 基于一个存储键/值对的 hashmap。Memcached-1.4.7的代码量还是可以接受的,只有10K行左右。 下载地址:http://memcached.org/ 7. Lua Lua很棒,Lua是巴西人发明的,这些都令我不爽,但是还不至于脸红,最多眼红。 让我脸红的是Lua的源代码,百分之一百的ANSI C,一点都不掺杂。在任何支持ANSI C编译器的平台上都可以轻松编译通过。我试过,真是一点废话都没有。Lua的代码数量足够小,5.1.4仅仅1.5W行,去掉空白行和注释估计能到1W行。 下载地址:http://www.lua.org/ 8. SQLite SQLite是一个开源的嵌入式关系数据库,实现自包容、零配置、支持事务的SQL数据库引擎。 其特点是高度便携、使用方便、结构紧凑、高效、可靠。足够小,大致3万行C代码,250K。 下载地址:http://www.sqlite.org/ 。 9. UNIX v6 UNIX V6 的内核源代码包括设备驱动程序在内 约有1 万行,这个数量的源代码,初学者是能够充分理解的。有一种说法是一个人所能理解的代码量上限为1 万行,UNIX V6的内核源代码从数量上看正好在这个范围之内。看到这里,大家是不是也有“如果只有1万行的话没准儿我也能学会”的想法呢? 另一方面,最近的操作系统,例如Linux 最新版的内核源代码据说超过了1000 万行。就算不是初学者,想完全理解全部代码基本上也是不可能的。 下载地址:http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6 10. NETBSD NetBSD是一个免费的,具有高度移植性的UNIX-like操作系统,是现行可移植平台最多的操作系统,可以在许多平台上执行,从64bit alpha服务器到手持设备和嵌入式设备。NetBSD计划的口号是:”Of course it runs NetBSD”。它设计简洁,代码规范,拥有众多先进特性,使得它在业界和学术界广受好评。由于简洁的设计和先进的特征,使得它在生产和研究方面,都有卓越的表现,而且它也有受使用者支持的完整的源代码。许多程序都可以很容易地通过NetBSD Packages Collection获得。LE little-endian 最符合人的思维的字节序。地址低位存储值的低位,地址高位存储值的高位。怎么讲是最符合人的思维的字节序,是因为从人的第一观感来说低位值小,就应该放在内存地址小的地方,也即内存地址低位;反之,高位值就应该放在内存地址大的地方,也即内存地址高位。 BE big-endian 最直观的字节序。地址低位存储值的高位,地址高位存储值的低位。为什么说直观,不要考虑对应关系。只需要把内存地址从左到右按照由低到高的顺序写出,把值按照通常的高位到低位的顺序写出,两者对照,一个字节一个字节的填充进去。 例子:在内存中双字0x01020304(DWORD)的存储方式: 4000 4001 4002 4003 LE 04 03 02 01 BE 01 02 03 04 例子:1.如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为: big-endian little-endian 0x0000 0x12 0xcd 0x0001 0x23 0xab 0x0002 0xab 0x34 0x0003 0xcd 0x12 x86系列CPU都是little-endian的字节序。 二、网络序 网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。 为了进行转换 bsd socket提供了转换的函数有下面四个: 1. htons 把unsigned short类型从主机序转换到网络序。 2. htonl 把unsigned long类型从主机序转换到网络序。 3. ntohs 把unsigned short类型从网络序转换到主机序。 4. ntohl 把unsigned long类型从网络序转换到主机序。 在使用little endian的系统中这些函数会把字节序进行转换,在使用big endian类型的系统中这些函数会定义成空宏。同样在网络程序开发时或是跨平台开发时,也应该注意保证只用一种字节序,不然两方的解释不一样就会产生bug。 1. 网络与主机字节转换函数:htons ntohs htonl ntohl (s就是short,l是long,h是host,n是network) 2. 不同的CPU上运行不同的操作系统,字节序也是不同的,参见下表 处理器 操作系统 字节排序 Alpha 全部 Little endian HP-PA NT Little endian HP-PA UNIX Big endian Intelx86 全部 Little endian <-----x86系统是小端字节序系统 Motorola680x() 全部 Big endian MIPS NT Little endian MIPS UNIX Big endian PowerPC NT Little endian PowerPC 非NT Big endian <-----PPC系统是大端字节序系统RS/6000 UNIX Big endian SPARC UNIX Big endian IXP1200 ARM核心 全部 Little endian 三、一些注意点 1.BIG-ENDIAN、LITTLE-ENDIAN跟多字节类型的数据有关的比如int,short,long型,而对单字节数据byte却没有影响。BIG-ENDIAN就是低位字节排放在内存的高端,高位字节排放在内存的低端。而LITTLE-ENDIAN正好相反。 比如 int a = 0x05060708 在BIG-ENDIAN的情况下存放为: 字节号 0 1 2 3 数据 05 06 07 08 在LITTLE-ENDIAN的情况下存放为: 字节号 0 1 2 3 数据 08 07 06 05 2.BIG-ENDIAN、LITTLE-ENDIAN、跟CPU有关的,每一种CPU不是BIG-ENDIAN就是LITTLE- ENDIAN、。IA架构的CPU中是Little-Endian,而PowerPC 、SPARC和Motorola处理器。这其实就是所谓的主机字节序。而网络字节序是指数据在网络上传输时是大头还是小头的,在Internet的网络字节序是BIG-ENDIAN。所谓的JAVA字节序指的是在JAVA虚拟机中多字节类型数据的存放顺序,JAVA字节序也是BIG-ENDIAN。 3.所以在用C/C++写通信程序时,在发送数据前务必用htonl和htons去把整型和短整型的数据进行从主机字节序到网络字节序的转换,而接收数据后对于整型和短整型数据则必须调用ntohl和ntohs实现从网络字节序到主机字节序的转换。如果通信的一方是JAVA程序、一方是 C/C++程序时,则需要在C/C++一侧使用以上几个方法进行字节序的转换,而JAVA一侧,则不需要做任何处理,因为JAVA字节序与网络字节序都是 BIG-ENDIAN,只要C/C++一侧能正确进行转换即可(发送前从主机序到网络序,接收时反变换)。如果通信的双方都是JAVA,则根本不用考虑字节序的问题了。 4.如果网络上全部是PowerPC,SPARC和Motorola CPU的主机那么不会出现任何问题,但由于实际存在大量的IA架构的CPU,所以经常出现数据传输错误。 5.如果程序运行在X86架构的PC SERVER上,发送数据的一端用C实现的,接收一端是用JAVA实现的,而发送端在发送数据前未进行从主机字节序到网络字节序的转换,这样接收端接收到的是LITTLE-ENDIAN的数据,数据解释自然出错。 四、引用他人的 http://blog.csdn.net/xuyanbo2008/article/details/7458007 一、大端模式&小端模式 所谓的“大端模式”,是指数据的低位(就是权值较小的后面那几位)保存在内存的高地址中,而数据的高位,保存在内存的低地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放; 所谓的“小端模式”,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。 如果将一个32位的整数0x12345678 存放到一个整型变量(int)中,这个整型变量采用大端或者小端模式在内存中的存储由下表所示。为简单起见,本文使用OP0表示一个32位数据的最高字节MSB(Most Significant Byte),使用OP3表示一个32位数据最低字节LSB(Least Significant Byte)。 大端模式 小端模式 0x00 12(OP0) 78(OP3) 0x01 34(OP1) 56(OP2) 0x02 56(OP2) 34(OP1) 0x03 78(OP3) 12(OP0) 小端:较高的有效字节存放在较高的存储器地址,较低的有效字节存放在较低的存储器地址。 大端:较高的有效字节存放在较低的存储器地址,较低的有效字节存放在较高的存储器地址。采用大小模式对数据进行存放的主要区别在于在存放的字节顺序,大端方式将高位存放在低地址,小端方式将高位存放在高地址。采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。到目前为止,采用大端或者小端进行数据存放,其孰优孰劣也没有定论。 下面这段代码可以用来测试一下你的编译器是大端模式还是小端模式: short int x; char x0,x1; x=0x1122; x0=((char*)&x)[0]; //低地址单元 x1=((char*)&x)[1]; //高地址单元 若x0=0x11,则是大端; 若x0=0x22,则是小端...... 上面的程序还可以看出,数据寻址时,用的是低位字节的地址 二、主机序&网络序 CPU 有不同的字节序类型这些字节序是指整数在内存中保存的顺序这个叫做主机序,最常见的有两种: 1、Little endian :将低序字节存储在起始地址 2、Big endian :将高序字节存储在起始地址 网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。 为了进行转换 bsd socket提供了转换的函数 有下面四个:htons 把unsigned short类型从主机序转换到网络序 htonl 把unsigned long类型从主机序转换到网络序 ntohs 把unsigned short类型从网络序转换到主机序 ntohl 把unsigned long类型从网络序转换到主机序在使用little endian的系统中,这些函数会把字节序进行转换 在使用big endian类型的系统中,这些函数会定义成空宏 同样,在网络程序开发时,或是跨平台开发时,也应该注意保证只用一种字节序,不然两方的解释不一样就会产生BUG。 1、网络与主机字节转换函数:htons ntohs htonl ntohl (s 就是short l是long h是host n是network) 2、不同的CPU上运行不同的操作系统,字节序也是不同的,参见下表: 处理器 操作系统 字节排序Alpha 全部 Little endian HP-PA NT Little endian HP-PA UNIX Big endianIntelx86 全部 Little endian <-----x86系统是小端字节序系统 Motorola680x() 全部 Big endian MIPS NT Little endian MIPS UNIX Big endian PowerPC NT Little endian PowerPC 非NT Big endian <-----PPC系统是大端字节序系统 RS/6000 UNIX Big endian SPARC UNIX Big endian IXP1200 ARM核心 全部 Little endian 下面是一个检验本机字节序的简便方法: //判断本机的字节序 //返回true表为小段序。返回false表示为大段序bool am_little_endian (){ unsigned short i=1; return (int)*((char *)(&i)) ? true : false; int main() if(am_little_endian()) printf("本机字节序为小段序!\n"); printf("本机字节序为大段序!\n"); return 0; 三、入栈地址高低问题 堆栈是在内存中指定的一段特殊存储区,存起始单元的地址叫栈底,当前存储单元地址叫栈顶,堆栈存储区一旦指定,栈底就固定不变了,而栈顶是随入栈、出栈操作呈动态。而不同机型的堆栈设计,有两种情况:一是每入栈一个数,栈顶地址加1,每出栈一个数,栈顶地址减1,即堆栈区是由内存的低地址向高地址。另一种是每入栈一个数,栈顶地址减1,每出栈一个数,栈顶地址加1,即堆栈区是由内存的高地址向低地址。高地址、低地址是相对而言,即相对地址编码的大小而言。
[<request-body>] 在HTTP请求中,第一行必须是一个请求行(request line),用来说明请求类型、要访问的资源以及使用的HTTP版本。紧接着是一个首部(header)小节,用来说明服务器要使用的附加信息。在首部之后是一个空行,再此之后可以添加任意的其他数据[称之为主体(body)]。 2.GET与POST区别 HTTP 定义了与服务器交互的不同方法,最基本的方法是 GET 和 POST(Ajax开发,关心的只有GET请求和POST请求)。 GET与POST方法有以下区别: (1) 在客户端,Get方式在通过URL提交数据,数据在URL中可以看到;POST方式,数据放置在HTML HEADER内提交。 (2) GET方式提交的数据最多只能有1024字节,而POST则没有此限制。 (3) 安全性问题。正如在(1)中提到,使用 Get 的时候,参数会显示在地址栏上,而 Post 不会。所以,如果这些数据是中文数据而且是非敏感数据,那么使用 get;如果用户输入的数据不是中文字符而且包含敏感数据,那么还是使用 post为好。 (4) 安全的和幂等的。所谓安全的意味着该操作用于获取信息而非修改信息。幂等的意味着对同一 URL 的多个请求应该返回同样的结果。完整的定义并不像看起来那样严格。换句话说,GET 请求一般不应产生副作用。从根本上讲,其目标是当用户打开一个链接时,她可以确信从自身的角度来看没有改变资源。比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。反之亦然。POST 请求就不那么轻松了。POST 表示可能改变服务器上的资源的请求。仍然以新闻站点为例,读者对文章的注解应该通过 POST 请求实现,因为在注解提交之后站点已经不同了(比方说文章下面出现一条注解)。http://www.cnblogs.com/stu-acer/archive/2006/08/28/488802.html GET与POST方法实例: GET实例 POST实例 User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 Connection: Keep-Alive User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 40 Connection: Keep-Alive (此处空一行) name=Professional%20Ajax&publisher=Wiley (1)get是从服务器上获取数据,post是向服务器传送数据。 (2)对于表单的提交方式,在服务器端只能用Request.QueryString来获取Get方式提交来的数据,用Post方式提交的数据只能用Request.Form来获取。 (3)一般来说,尽量避免使用Get方式提交表单,因为有可能会导致安全问题。比如说在登陆表单中用Get方式,用户输入的用户名和密码将在地址栏中暴露无遗。但是在分页程序中,用Get方式就比用Post好。 二.HTTP响应 1.HTTP响应格式: <status line> <headers> <blank line> [<response-body>] 在响应中唯一真正的区别在于第一行中用状态信息代替了请求信息。状态行(status line)通过提供一个状态码来说明所请求的资源情况。 HTTP响应实例: ◆304 (NOT MODIFIED): 该资源在上次请求之后没有任何修改。这通常用于浏览器的缓存机制。 ◆401 (UNAUTHORIZED): 客户端无权访问该资源。这通常会使得浏览器要求用户输入用户名和密码,以登录到服务器。 ◆403 (FORBIDDEN): 客户端未能获得授权。这通常是在401之后输入了不正确的用户名或密码。 ◆404 (NOT FOUND): 在指定的位置不存在所申请的资源。 参考网址:http://www.blogjava.net/honeybee/articles/164008.html 参考网址: Author :Jeffrey My Blog:http://blog.csdn.net/gueter/ HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中,而且HTTP-NG(Next Generation of HTTP)的建议已经提出。 HTTP协议的主要特点可概括如下: 1.支持客户/服务器模式。 2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。 3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。 4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。 5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。 一、HTTP协议详解之URL篇 http(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。 HTTP URL (URL是一种特殊类型的URI,包含了用于查找某个资源的足够的信息)的格式如下:http://host[":"port][abs_path] http表示要通过HTTP协议来定位网络资源;host表示合法的Internet主机域名或者IP地址;port指定一个端口号,为空则使用缺省端口80;abs_path指定请求资源的URI;如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。 1、输入:www.guet.edu.cn浏览器自动转换成:http://www.guet.edu.cn/2、http:192.168.0.116:8080/index.jsp http请求由三部分组成,分别是:请求行、消息报头、请求正文 1、请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本,格式如下:Method Request-URI HTTP-Version CRLF 其中 Method表示请求方法;Request-URI是一个统一资源标识符;HTTP-Version表示请求的HTTP协议版本;CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)。 请求方法(所有方法全为大写)有多种,各个方法的解释如下: GET 请求获取Request-URI所标识的资源 POST 在Request-URI所标识的资源后附加新的数据 HEAD 请求获取由Request-URI所标识的资源的响应消息报头 PUT 请求服务器存储一个资源,并用Request-URI作为其标识 DELETE 请求服务器删除Request-URI所标识的资源 TRACE 请求服务器回送收到的请求信息,主要用于测试或诊断 CONNECT 保留将来使用 OPTIONS 请求查询服务器的性能,或者查询与资源相关的选项和需求 应用举例: GET方法:在浏览器的地址栏中输入网址的方式访问网页时,浏览器采用GET方法向服务器获取资源,eg:GET /form.html HTTP/1.1 (CRLF) POST方法要求被请求服务器接受附在请求后面的数据,常用于提交表单。 eg:POST /reg.jsp HTTP/ (CRLF) Accept:image/gif,image/x-xbit,... (CRLF) HOST:www.guet.edu.cn (CRLF) Content-Length:22 (CRLF) Connection:Keep-Alive (CRLF) Cache-Control:no-cache (CRLF) (CRLF) //该CRLF表示消息报头已经结束,在此之前为消息报头 user=jeffrey&pwd=1234 //此行以下为提交的数据 HEAD方法与GET方法几乎是一样的,对于HEAD请求的回应部分来说,它的HTTP头部中包含的信息与通过GET请求所得到的信息是相同的。利用这个方法,不必传输整个资源内容,就可以得到Request-URI所标识的资源的信息。该方法常用于测试超链接的有效性,是否可以访问,以及最近是否更新。 2、请求报头后述 3、请求正文(略) 三、HTTP协议详解之响应篇 在接收和解释请求消息后,服务器返回一个HTTP响应消息。 HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文 1、状态行格式如下: HTTP-Version Status-Code Reason-Phrase CRLF 其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。 状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值: 1xx:指示信息--表示请求已接收,继续处理 2xx:成功--表示请求已被成功接收、理解、接受 3xx:重定向--要完成请求必须进行更进一步的操作 4xx:客户端错误--请求有语法错误或请求无法实现 5xx:服务器端错误--服务器未能实现合法的请求 常见状态代码、状态描述、说明: 200 OK //客户端请求成功 400 Bad Request //客户端请求有语法错误,不能被服务器所理解 401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报 //头域一起使用 403 Forbidden //服务器收到请求,但是拒绝提供服务 404 Not Found //请求资源不存在,eg:输入了错误的URL 500 Internal Server Error //服务器发生不可预期的错误 503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后, //可能恢复正常 eg:HTTP/1.1 200 OK (CRLF) 2、响应报头后述 3、响应正文就是服务器返回的资源的内容 四、HTTP协议详解之消息报头篇 HTTP消息由客户端到服务器的请求和服务器到客户端的响应组成。请求消息和响应消息都是由开始行(对于请求消息,开始行就是请求行,对于响应消息,开始行就是状态行),消息报头(可选),空行(只有CRLF的行),消息正文(可选)组成。 HTTP消息报头包括普通报头、请求报头、响应报头、实体报头。 每一个报头域都是由名字+“:”+空格+值 组成,消息报头域的名字是大小写无关的。 1、普通报头 在普通报头中,有少数报头域用于所有的请求和响应消息,但并不用于被传输的实体,只用于传输的消息。 Cache-Control 用于指定缓存指令,缓存指令是单向的(响应中出现的缓存指令在请求中未必会出现),且是独立的(一个消息的缓存指令不会影响另一个消息处理的缓存机制),HTTP1.0使用的类似的报头域为Pragma。 请求时的缓存指令包括:no-cache(用于指示请求或响应消息不能缓存)、no-store、max-age、max-stale、min-fresh、only-if-cached; 响应时的缓存指令包括:public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age、s-maxage. eg:为了指示IE浏览器(客户端)不要缓存页面,服务器端的JSP程序可以编写如下:response.sehHeader("Cache-Control","no-cache"); //response.setHeader("Pragma","no-cache");作用相当于上述代码,通常两者//合用 这句代码将在发送的响应消息中设置普通报头域:Cache-Control:no-cache Date普通报头域表示消息产生的日期和时间 Connection普通报头域允许发送指定连接的选项。例如指定连接是连续,或者指定“close”选项,通知服务器,在响应完成后,关闭连接 2、请求报头 请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息。 常用的请求报头 Accept Accept请求报头域用于指定客户端接受哪些类型的信息。eg:Accept:image/gif,表明客户端希望接受GIF图象格式的资源;Accept:text/html,表明客户端希望接受html文本。 Accept-Charset Accept-Charset请求报头域用于指定客户端接受的字符集。eg:Accept-Charset:iso-8859-1,gb2312.如果在请求消息中没有设置这个域,缺省是任何字符集都可以接受。 Accept-Encoding Accept-Encoding请求报头域类似于Accept,但是它是用于指定可接受的内容编码。eg:Accept-Encoding:gzip.deflate.如果请求消息中没有设置这个域服务器假定客户端对各种内容编码都可以接受。 Accept-Language Accept-Language请求报头域类似于Accept,但是它是用于指定一种自然语言。eg:Accept-Language:zh-cn.如果请求消息中没有设置这个报头域,服务器假定客户端对各种语言都可以接受。 Authorization Authorization请求报头域主要用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到服务器的响应代码为401(未授权),可以发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。 Host(发送请求时,该报头域是必需的) Host请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的,eg: 我们在浏览器中输入:http://www.guet.edu.cn/index.html浏览器发送的请求消息中,就会包含Host请求报头域,如下: Host:www.guet.edu.cn此处使用缺省端口号80,若指定了端口号,则变成:Host:www.guet.edu.cn:指定端口号 User-Agent 我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,你所使用的浏览器的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息。User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。不过,这个报头域不是必需的,如果我们自己编写一个浏览器,不使用User-Agent请求报头域,那么服务器端就无法得知我们的信息了。 请求报头举例: GET /form.html HTTP/1.1 (CRLF) Accept:image/gif,image/x-xbitmap,image/jpeg,application/x-shockwave-flash,application/vnd.ms-excel,application/vnd.ms-powerpoint,application/msword,*/* (CRLF) Accept-Language:zh-cn (CRLF) Accept-Encoding:gzip,deflate (CRLF) If-Modified-Since:Wed,05 Jan 2007 11:21:25 GMT (CRLF) If-None-Match:W/"80b1a4c018f3c41:8317" (CRLF) User-Agent:Mozilla/4.0(compatible;MSIE6.0;Windows NT 5.0) (CRLF) Host:www.guet.edu.cn (CRLF) Connection:Keep-Alive (CRLF) (CRLF) 3、响应报头 响应报头允许服务器传递不能放在状态行中的附加响应信息,以及关于服务器的信息和对Request-URI所标识的资源进行下一步访问的信息。 常用的响应报头 Location Location响应报头域用于重定向接受者到一个新的位置。Location响应报头域常用在更换域名的时候。 Server Server响应报头域包含了服务器用来处理请求的软件信息。与User-Agent请求报头域是相对应的。下面是 Server响应报头域的一个例子: Server:Apache-Coyote/1.1 WWW-Authenticate WWW-Authenticate响应报头域必须被包含在401(未授权的)响应消息中,客户端收到401响应消息时候,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报头就包含该报头域。 eg:WWW-Authenticate:Basic realm="Basic Auth Test!" //可以看出服务器对请求资源采用的是基本验证机制。 4、实体报头 请求和响应消息都可以传送一个实体。一个实体由实体报头域和实体正文组成,但并不是说实体报头域和实体正文要在一起发送,可以只发送实体报头域。实体报头定义了关于实体正文(eg:有无实体正文)和请求所标识的资源的元信息。 常用的实体报头 Content-Encoding Content-Encoding实体报头域被用作媒体类型的修饰符,它的值指示了已经被应用到实体正文的附加内容的编码,因而要获得Content-Type报头域中所引用的媒体类型,必须采用相应的解码机制。Content-Encoding这样用于记录文档的压缩方法,eg:Content-Encoding:gzip Content-Language Content-Language实体报头域描述了资源所用的自然语言。没有设置该域则认为实体内容将提供给所有的语言阅读 者。eg:Content-Language:da Content-Length Content-Length实体报头域用于指明实体正文的长度,以字节方式存储的十进制数字来表示。 Content-Type Content-Type实体报头域用语指明发送给接收者的实体正文的媒体类型。eg: Content-Type:text/html;charset=ISO-8859-1 Content-Type:text/html;charset=GB2312 Last-Modified Last-Modified实体报头域用于指示资源的最后修改日期和时间。 Expires Expires实体报头域给出响应过期的日期和时间。为了让代理服务器或浏览器在一段时间以后更新缓存中(再次访问曾访问过的页面时,直接从缓存中加载,缩短响应时间和降低服务器负载)的页面,我们可以使用Expires实体报头域指定页面过期的时间。eg:Expires:Thu,15 Sep 2006 16:23:12 GMT HTTP1.1的客户端和缓存必须将其他非法的日期格式(包括0)看作已经过期。eg:为了让浏览器不要缓存页面,我们也可以利用Expires实体报头域,设置为0,jsp中程序如下:response.setDateHeader("Expires","0"); 实验目的及原理: 利用MS的telnet工具,通过手动输入http请求信息的方式,向服务器发出请求,服务器接收、解释和接受请求后,会返回一个响应,该响应会在telnet窗口上显示出来,从而从感性上加深对http协议的通讯过程的认识。 实验步骤: 1、打开telnet 1.1 打开telnet 运行-->cmd-->telnet 1.2 打开telnet回显功能 set localecho 2、连接服务器并发送请求 2.1 open www.guet.edu.cn 80 //注意端口号不能省略 HEAD /index.asp HTTP/1.0 Host:www.guet.edu.cn /*我们可以变换请求方法,请求桂林电子主页内容,输入消息如下*/ open www.guet.edu.cn 80 GET /index.asp HTTP/1.0 //请求资源的内容 Host:www.guet.edu.cn 2.2 open www.sina.com.cn 80 //在命令提示符号下直接输入telnet www.sina.com.cn 80 HEAD /index.asp HTTP/1.0 Host:www.sina.com.cn 3 实验结果: 3.1 请求信息2.1得到的响应是: HTTP/1.1 200 OK //请求成功 Server: Microsoft-IIS/5.0 //web服务器 Date: Thu,08 Mar 200707:17:51 GMT Connection: Keep-Alive Content-Length: 23330 Content-Type: text/html Expries: Thu,08 Mar 2007 07:16:51 GMT Set-Cookie: ASPSESSIONIDQAQBQQQB=BEJCDGKADEDJKLKKAJEOIMMH; path=/ Cache-control: private //资源内容省略 3.2 请求信息2.2得到的响应是: HTTP/1.0 404 Not Found //请求失败 Date: Thu, 08 Mar 2007 07:50:50 GMT Server: Apache/2.0.54 <Unix> Last-Modified: Thu, 30 Nov 2006 11:35:41 GMT ETag: "6277a-415-e7c76980" Accept-Ranges: bytes X-Powered-By: mod_xlayout_jh/0.0.1vhs.markII.remix Vary: Accept-Encoding Content-Type: text/html X-Cache: MISS from zjm152-78.sina.com.cn Via: 1.0 zjm152-78.sina.com.cn:80<squid/2.6.STABLES-20061207> X-Cache: MISS from th-143.sina.com.cn Connection: close 失去了跟主机的连接 按任意键继续... 4 .注意事项:1、出现输入错误,则请求不会成功。 2、报头域不分大小写。 3、更深一步了解HTTP协议,可以查看RFC2616,在http://www.letf.org/rfc上找到该文件。 4、开发后台程序必须掌握http协议 六、HTTP协议相关技术补充 1、基础: 高层协议有:文件传输协议FTP、电子邮件传输协议SMTP、域名系统服务DNS、网络新闻传输协议NNTP和HTTP协议等 中介由三种:代理(Proxy)、网关(Gateway)和通道(Tunnel),一个代理根据URI的绝对格式来接受请求,重写全部或部分消息,通过 URI的标识把已格式化过的请求发送到服务器。网关是一个接收代理,作为一些其它服务器的上层,并且如果必须的话,可以把请求翻译给下层的服务器协议。一 个通道作为不改变消息的两个连接之间的中继点。当通讯需要通过一个中介(例如:防火墙等)或者是中介不能识别消息的内容时,通道经常被使用。 代理(Proxy):一个中间程序,它可以充当一个服务器,也可以充当一个客户机,为其它客户机建立请求。请求是通过可能的翻译在内部或经过传递到其它的 服务器中。一个代理在发送请求信息之前,必须解释并且如果可能重写它。代理经常作为通过防火墙的客户机端的门户,代理还可以作为一个帮助应用来通过协议处 理没有被用户代理完成的请求。 网关(Gateway):一个作为其它服务器中间媒介的服务器。与代理不同的是,网关接受请求就好象对被请求的资源来说它就是源服务器;发出请求的客户机并没有意识到它在同网关打交道。 网关经常作为通过防火墙的服务器端的门户,网关还可以作为一个协议翻译器以便存取那些存储在非HTTP系统中的资源。 通道(Tunnel):是作为两个连接中继的中介程序。一旦激活,通道便被认为不属于HTTP通讯,尽管通道可能是被一个HTTP请求初始化的。当被中继 的连接两端关闭时,通道便消失。当一个门户(Portal)必须存在或中介(Intermediary)不能解释中继的通讯时通道被经常使用。 2、协议分析的优势—HTTP分析器检测网络攻击 以模块化的方式对高层协议进行分析处理,将是未来入侵检测的方向。 HTTP及其代理的常用端口80、3128和8080在network部分用port标签进行了规定 3、HTTP协议Content Lenth限制漏洞导致拒绝服务攻击 使用POST方法时,可以设置ContentLenth来定义需要传送的数据长度,例如ContentLenth:999999999,在传送完成前,内 存不会释放,攻击者可以利用这个缺陷,连续向WEB服务器发送垃圾数据直至WEB服务器内存耗尽。这种攻击方法基本不会留下痕迹。 http://www.cnpaf.net/Class/HTTP/0532918532667330.html 4、利用HTTP协议的特性进行拒绝服务攻击的一些构思 服务器端忙于处理攻击者伪造的TCP连接请求而无暇理睬客户的正常请求(毕竟客户端的正常请求比率非常之小),此时从正常客户的角度看来,服务器失去响应,这种情况我们称作:服务器端受到了SYNFlood攻击(SYN洪水攻击)。 而Smurf、TearDrop等是利用ICMP报文来Flood和IP碎片攻击的。本文用“正常连接”的方法来产生拒绝服务攻击。 19端口在早期已经有人用来做Chargen攻击了,即Chargen_Denial_of_Service,但是!他们用的方法是在两台Chargen 服务器之间产生UDP连接,让服务器处理过多信息而DOWN掉,那么,干掉一台WEB服务器的条件就必须有2个:1.有Chargen服务2.有HTTP 服务 方法:攻击者伪造源IP给N台Chargen发送连接请求(Connect),Chargen接收到连接后就会返回每秒72字节的字符流(实际上根据网络实际情况,这个速度更快)给服务器。 5、Http指纹识别技术 Http指纹识别的原理大致上也是相同的:记录不同服务器对Http协议执行中的微小差别进行识别.Http指纹识别比TCP/IP堆栈指纹识别复杂许 多,理由是定制Http服务器的配置文件、增加插件或组件使得更改Http的响应信息变的很容易,这样使得识别变的困难;然而定制TCP/IP堆栈的行为 需要对核心层进行修改,所以就容易识别. 要让服务器返回不同的Banner信息的设置是很简单的,象Apache这样的开放源代码的Http服务器,用户可以在源代码里修改Banner信息,然 后重起Http服务就生效了;对于没有公开源代码的Http服务器比如微软的IIS或者是Netscape,可以在存放Banner信息的Dll文件中修 改,相关的文章有讨论的,这里不再赘述,当然这样的修改的效果还是不错的.另外一种模糊Banner信息的方法是使用插件。 常用测试请求: 1:HEAD/Http/1.0发送基本的Http请求 2:DELETE/Http/1.0发送那些不被允许的请求,比如Delete请求 3:GET/Http/3.0发送一个非法版本的Http协议请求 4:GET/JUNK/1.0发送一个不正确规格的Http协议请求 Http指纹识别工具Httprint,它通过运用统计学原理,组合模糊的逻辑学技术,能很有效的确定Http服务器的类型.它可以被用来收集和分析不同Http服务器产生的签名。 6、其他:为了提高用户使用浏览器时的性能,现代浏览器还支持并发的访问方式,浏览一个网页时同时建立多个连接,以迅速获得一个网页上的多个图标,这样能更快速完成整个网页的传输。 HTTP1.1中提供了这种持续连接的方式,而下一代HTTP协议:HTTP-NG更增加了有关会话控制、丰富的内容协商等方式的支持,来提供 更高效率的连接。
1.1 介绍 HTTP是Hyper Text Transfer Protocol(超文本传输协议)的缩写。它的发展是万维网协会(World Wide Web Consortium)和Internet工作小组IETF(Internet Engineering Task Force)合作的结果,(他们)最终发布了一系列的RFC,RFC 1945定义了HTTP/1.0版本。其中最著名的就是RFC 2616。RFC 2616定义了今天普遍使用的一个版本——HTTP 1.1。 HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。 HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。HTTP是一个无状态的协议。 1.2 在TCP/IP协议栈中的位置 HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示: 默认HTTP的端口号为80,HTTPS的端口号为443。 1.3 HTTP的请求响应模型 HTTP协议永远都是客户端发起请求,服务器回送响应。见下图: 这样就限制了使用HTTP协议,无法实现在客户端没有发起请求的时候,服务器将消息推送给客户端。 HTTP协议是一个无状态的协议,同一个客户端的这次请求和上次请求是没有对应关系。 1.4 工作流程 一次HTTP操作称为一个事务,其工作过程可分为四步: 1)首先客户机与服务器需要建立连接。只要单击某个超级链接,HTTP的工作开始。 2)建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。 3)服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。 4)客户端接收服务器所返回的信息通过浏览器显示在用户的显示屏上,然后客户机与服务器断开连接。 如果在以上过程中的某一步出现错误,那么产生错误的信息将返回到客户端,有显示屏输出。对于用户来说,这些过程是由HTTP自己完成的,用户只要用鼠标点击,等待信息显示就可以了。 1.5 使用Wireshark抓TCP、http包 打开Wireshark,选择工具栏上的“Capture”->“Options”,界面选择如图1所示: 图1 设置Capture选项 一般读者只需要选择最上边的下拉框,选择合适的Device,而后点击“Capture Filter”,此处选择的是“HTTP TCP port(80)”,选择后点击上图的“Start”开始抓包。 图2 选择Capture Filter 例如在浏览器中打开http://image.baidu.com/,抓包如图3所示: http://www.blogjava.net/images/blogjava_net/amigoxie/40799/o_http%e5%8d%8f%e8%ae%ae%e5%ad%a6%e4%b9%a0-%e6%a6%82%e5%bf%b5-3.jpg 图3 抓包 在上图中,可清晰的看到客户端浏览器(ip为192.168.2.33)与服务器的交互过程: 1)No1:浏览器(192.168.2.33)向服务器(220.181.50.118)发出连接请求。此为TCP三次握手第一步,此时从图中可以看出,为SYN,seq:X (x=0) 2)No2:服务器(220.181.50.118)回应了浏览器(192.168.2.33)的请求,并要求确认,此时为:SYN,ACK,此时seq:y(y为0),ACK:x+1(为1)。此为三次握手的第二步; 3)No3:浏览器(192.168.2.33)回应了服务器(220.181.50.118)的确认,连接成功。为:ACK,此时seq:x+1(为1),ACK:y+1(为1)。此为三次握手的第三步; 4)No4:浏览器(192.168.2.33)发出一个页面HTTP请求; 5)No5:服务器(220.181.50.118)确认; 6)No6:服务器(220.181.50.118)发送数据; 7)No7:客户端浏览器(192.168.2.33)确认; 8)No14:客户端(192.168.2.33)发出一个图片HTTP请求; 9)No15:服务器(220.181.50.118)发送状态响应码200 OK 1.6 头域 每个头域由一个域名,冒号(:)和域值三部分组成。域名是大小写无关的,域值前可以添加任何数量的空格符,头域可以被扩展为多行,在每行开始处,使用至少一个空格或制表符。 在抓包的图中,No14点开可看到如图4所示: http://www.blogjava.net/images/blogjava_net/amigoxie/40799/o_http%e5%8d%8f%e8%ae%ae%e5%ad%a6%e4%b9%a0-%e6%a6%82%e5%bf%b5-4.jpg 图4 http请求消息 回应的消息如图5所示: 图5 http状态响应信息 1.6.1 host头域 Host头域指定请求资源的Intenet主机和端口号,必须表示请求url的原始服务器或网关的位置。HTTP/1.1请求必须包含主机头域,否则系统会以400状态码返回。 图5中host那行为: 1.6.2 Referer头域 Referer头域允许客户端指定请求uri的源资源地址,这可以允许服务器生成回退链表,可用来登陆、优化cache等。他也允许废除的或错误的连接由于维护的目的被追踪。如果请求的uri没有自己的uri地址,Referer不能被发送。如果指定的是部分uri地址,则此地址应该是一个相对地址。 在图4中,Referer行的内容为: 1.6.3 User-Agent头域 User-Agent头域的内容包含发出请求的用户信息。 在图4中,User-Agent行的内容为: http://www.blogjava.net/images/blogjava_net/amigoxie/40799/o_http%e5%8d%8f%e8%ae%ae%e5%ad%a6%e4%b9%a0-%e6%a6%82%e5%bf%b5-8.jpg 1.6.4 Cache-Control头域 Cache-Control指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置Cache-Control并不会修改另一个消息处理过程中的缓存处理过程。请求时的缓存指令包括no-cache、no-store、max-age、max-stale、min-fresh、only-if-cached,响应消息中的指令包括public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age。 在图5中的该头域为: 1.6.5 Date头域 Date头域表示消息发送的时间,时间的描述格式由rfc822定义。例如,Date:Mon,31Dec200104:25:57GMT。Date描述的时间表示世界标准时,换算成本地时间,需要知道用户所在的时区。 图5中,该头域如下图所示: 1.7 HTTP的几个重要概念 1.7.1连接:Connection 一个传输层的实际环流,它是建立在两个相互通讯的应用程序之间。 在http1.1,request和reponse头中都有可能出现一个connection的头,此header的含义是当client和server通信时对于长链接如何进行处理。 在http1.1中,client和server都是默认对方支持长链接的, 如果client使用http1.1协议,但又不希望使用长链接,则需要在header中指明connection的值为close;如果server方也不想支持长链接,则在response中也需要明确说明connection的值为close。不论request还是response的header中包含了值为close的connection,都表明当前正在使用的tcp链接在当天请求处理完毕后会被断掉。以后client再进行新的请求时就必须创建新的tcp链接了。 1.7.2消息:Message HTTP通讯的基本单位,包括一个结构化的八元组序列并通过连接传输。 1.7.3请求:Request 一个从客户端到服务器的请求信息包括应用于资源的方法、资源的标识符和协议的版本号。 1.7.4响应:Response 一个从服务器返回的信息包括HTTP协议的版本号、请求的状态(例如“成功”或“没找到”)和文档的MIME类型。 1.7.5资源:Resource 由URI标识的网络数据对象或服务。 1.7.6实体:Entity 数据资源或来自服务资源的回映的一种特殊表示方法,它可能被包围在一个请求或响应信息中。一个实体包括实体头信息和实体的本身内容。 1.7.7客户机:Client 一个为发送请求目的而建立连接的应用程序。 1.7.8用户代理:UserAgent 初始化一个请求的客户机。它们是浏览器、编辑器或其它用户工具。 1.7.9服务器:Server 一个接受连接并对请求返回信息的应用程序。 1.7.10源服务器:Originserver 是一个给定资源可以在其上驻留或被创建的服务器。 1.7.11代理:Proxy 一个中间程序,它可以充当一个服务器,也可以充当一个客户机,为其它客户机建立请求。请求是通过可能的翻译在内部或经过传递到其它的服务器中。一个代理在发送请求信息之前,必须解释并且如果可能重写它。 代理经常作为通过防火墙的客户机端的门户,代理还可以作为一个帮助应用来通过协议处理没有被用户代理完成的请求。 1.7.12网关:Gateway 一个作为其它服务器中间媒介的服务器。与代理不同的是,网关接受请求就好象对被请求的资源来说它就是源服务器;发出请求的客户机并没有意识到它在同网关打交道。 网关经常作为通过防火墙的服务器端的门户,网关还可以作为一个协议翻译器以便存取那些存储在非HTTP系统中的资源。 1.7.13通道:Tunnel 是作为两个连接中继的中介程序。一旦激活,通道便被认为不属于HTTP通讯,尽管通道可能是被一个HTTP请求初始化的。当被中继的连接两端关闭时,通道便消失。当一个门户(Portal)必须存在或中介(Intermediary)不能解释中继的通讯时通道被经常使用。 1.7.14缓存:Cache 反应信息的局域存储。 附录:参考资料 《http_百度百科》:http://baike.baidu.com/view/9472.htm 《结果编码和http状态响应码》:http://blog.tieniu1980.cn/archives/377 《分析TCP的三次握手》: http://cache.baidu.com/c?m=9f65cb4a8c8507ed4fece763104c8c711923d030678197027fa3c215cc7905141130a8e5747e0d548d98297a5ae91e03f7f63772315477e3cacdd94cdbbdc42225d82c36734f844315c419d891007a9f34d507a9f916a2e1b065d2f48193864353bb15543897f1fb4d711edd1b86033093b1e94e022e67adec40728e2e605f983431c5508fe4&p=c6769a46c5820efd08e2973b42&user=baidu 《使用Wireshark来检测一次HTTP连接过程》: http://blog.163.com/wangbo_tester/blog/static/12806792120098174162288/ 《http协议的几个重要概念》:http://nc.mofcom.gov.cn/news/10819972.html 《http协议中connection头的作用》: http://blog.csdn.net/barfoo/archive/2008/06/05/2514667.aspx 2. 协议详解篇 2.1 HTTP/1.0和HTTP/1.1的比较 RFC 1945定义了HTTP/1.0版本,RFC 2616定义了HTTP/1.1版本。 笔者在blog上提供了这两个RFC中文版的下载地址。 RFC1945下载地址: http://www.blogjava.net/Files/amigoxie/RFC1945(HTTP)中文版.rar RFC2616下载地址: http://www.blogjava.net/Files/amigoxie/RFC2616(HTTP)中文版.rar 2.1.1建立连接方面 HTTP/1.0 每次请求都需要建立新的TCP连接,连接不能复用。HTTP/1.1 新的请求可以在上次请求建立的TCP连接之上发送,连接可以复用。优点是减少重复进行TCP三次握手的开销,提高效率。 注意:在同一个TCP连接中,新的请求需要等上次请求收到响应后,才能发送。 2.1.2 Host域 HTTP1.1在Request消息头里头多了一个Host域, HTTP1.0则没有这个域。 GET /pub/WWW/TheProject.html HTTP/1.1 Host: www.w3.org 可能HTTP1.0的时候认为,建立TCP连接的时候已经指定了IP地址,这个IP地址上只有一个host。 2.1.3日期时间戳 (接收方向) 无论是HTTP1.0还是HTTP1.1,都要能解析下面三种date/time stamp: Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format (发送方向) HTTP1.0要求不能生成第三种asctime格式的date/time stamp; HTTP1.1则要求只生成RFC 1123(第一种)格式的date/time stamp。 2.1.4状态响应码 状态响应码100 (Continue) 状态代码的使用,允许客户端在发request消息body之前先用request header试探一下server,看server要不要接收request body,再决定要不要发request body。 客户端在Request头部中包含 Expect: 100-continue Server看到之后呢如果回100 (Continue) 这个状态代码,客户端就继续发request body。这个是HTTP1.1才有的。 另外在HTTP/1.1中还增加了101、203、205等等性状态响应码 2.1.5请求方式 HTTP1.1增加了OPTIONS, PUT, DELETE, TRACE, CONNECT这些Request方法. Method = "OPTIONS" ; Section 9.2 | "GET" ; Section 9.3 | "HEAD" ; Section 9.4 | "POST" ; Section 9.5 | "PUT" ; Section 9.6 | "DELETE" ; Section 9.7 | "TRACE" ; Section 9.8 | "CONNECT" ; Section 9.9 | extension-method extension-method = token 2.2 HTTP请求消息 2.2.1请求消息格式 请求消息格式如下所示: 通用信息头|请求头|实体头 CRLF(回车换行) 其中“请求行”为:请求行 = 方法 [空格] 请求URI [空格] 版本号 [回车换行] 请求行实例: GET /index.html HTTP/1.1 POST http://192.168.2.217:8080/index.jsp HTTP/1.1 HTTP请求消息实例: GET /hello.htm HTTP/1.1Accept: */*Accept-Language: zh-cnAccept-Encoding: gzip, deflateIf-Modified-Since: Wed, 17 Oct 2007 02:15:55 GMTIf-None-Match: W/"158-1192587355000"User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)Host: 192.168.2.162:8080Connection: Keep-Alive 2.2.2请求方法 HTTP的请求方法包括如下几种: q GET q POST q HEAD q PUT q DELETE q OPTIONS q TRACE q CONNECT 2.3 HTTP响应消息 2.3.1响应消息格式 HTTP响应消息的格式如下所示: 通用信息头|响应头|实体头 其中:状态行 = 版本号 [空格] 状态码 [空格] 原因 [回车换行] 状态行举例: HTTP/1.0 200 OK HTTP/1.1 400 Bad Request HTTP响应消息实例如下所示: HTTP/1.1 200 OKETag: W/"158-1192590101000"Last-Modified: Wed, 17 Oct 2007 03:01:41 GMTContent-Type: text/htmlContent-Length: 158Date: Wed, 17 Oct 2007 03:01:59 GMTServer: Apache-Coyote/1.1 2.3.2 http的状态响应码 2.3.2.1 1**:请求收到,继续处理 100——客户必须继续发出请求 101——客户要求服务器根据请求转换HTTP协议版本 2.3.2.2 2**:操作成功收到,分析、接受 200——交易成功 201——提示知道新文件的URL 202——接受和处理、但处理未完成 203——返回信息不确定或不完整 204——请求收到,但返回信息为空 205——服务器完成了请求,用户代理必须复位当前已经浏览过的文件 206——服务器已经完成了部分用户的GET请求 2.3.2.3 3**:完成此请求必须进一步处理 300——请求的资源可在多处得到 301——删除请求数据 302——在其他地址发现了请求数据 303——建议客户访问其他URL或访问方式 304——客户端已经执行了GET,但文件未变化 305——请求的资源必须从服务器指定的地址得到 306——前一版本HTTP中使用的代码,现行版本中不再使用 307——申明请求的资源临时性删除 2.3.2.4 4**:请求包含一个错误语法或不能完成 400——错误请求,如语法错误 401——未授权 HTTP 401.1 - 未授权:登录失败 HTTP 401.2 - 未授权:服务器配置问题导致登录失败 HTTP 401.3 - ACL 禁止访问资源 HTTP 401.4 - 未授权:授权被筛选器拒绝 HTTP 401.5 - 未授权:ISAPI 或 CGI 授权失败 402——保留有效ChargeTo头响应 403——禁止访问 HTTP 403.1 禁止访问:禁止可执行访问 HTTP 403.2 - 禁止访问:禁止读访问 HTTP 403.3 - 禁止访问:禁止写访问 HTTP 403.4 - 禁止访问:要求 SSL HTTP 403.5 - 禁止访问:要求 SSL 128 HTTP 403.6 - 禁止访问:IP 地址被拒绝 HTTP 403.7 - 禁止访问:要求客户证书 HTTP 403.8 - 禁止访问:禁止站点访问 HTTP 403.9 - 禁止访问:连接的用户过多 HTTP 403.10 - 禁止访问:配置无效 HTTP 403.11 - 禁止访问:密码更改 HTTP 403.12 - 禁止访问:映射器拒绝访问 HTTP 403.13 - 禁止访问:客户证书已被吊销 HTTP 403.15 - 禁止访问:客户访问许可过多 HTTP 403.16 - 禁止访问:客户证书不可信或者无效 HTTP 403.17 - 禁止访问:客户证书已经到期或者尚未生效 404——没有发现文件、查询或URl 405——用户在Request-Line字段定义的方法不允许 406——根据用户发送的Accept拖,请求资源不可访问 407——类似401,用户必须首先在代理服务器上得到授权 408——客户端没有在用户指定的饿时间内完成请求 409——对当前资源状态,请求不能完成 410——服务器上不再有此资源且无进一步的参考地址 411——服务器拒绝用户定义的Content-Length属性请求 412——一个或多个请求头字段在当前请求中错误 413——请求的资源大于服务器允许的大小 414——请求的资源URL长于服务器允许的长度 415——请求资源不支持请求项目格式 416——请求中包含Range请求头字段,在当前请求资源范围内没有range指示值,请求也不包含If-Range请求头字段 417——服务器不满足请求Expect头字段指定的期望值,如果是代理服务器,可能是下一级服务器不能满足请求长。 2.3.2.5 5**:服务器执行一个完全有效请求失败 HTTP 500 - 内部服务器错误 HTTP 500.100 - 内部服务器错误 - ASP 错误 HTTP 500-11 服务器关闭 HTTP 500-12 应用程序重新启动 HTTP 500-13 - 服务器太忙 HTTP 500-14 - 应用程序无效 HTTP 500-15 - 不允许请求 global.asa Error 501 - 未实现 HTTP 502 - 网关错误 2.4 使用telnet进行http测试 在Windows下,可使用命令窗口进行http简单测试。 输入cmd进入命令窗口,在命令行键入如下命令后按回车: telnet www.baidu.com 80 而后在窗口中按下“Ctrl+]”后按回车可让返回结果回显。 接着开始发请求消息,例如发送如下请求消息请求baidu的首页消息,使用的HTTP协议为HTTP/1.1: GET /index.html HTTP/1.1 注意:copy如上的消息到命令窗口后需要按两个回车换行才能得到响应的消息,第一个回车换行是在命令后键入回车换行,是HTTP协议要求的。第二个是确认输入,发送请求。 可看到返回了200 OK的消息,如下图所示: 可看到,当采用HTTP/1.1时,连接不是在请求结束后就断开的。若采用HTTP1.0,在命令窗口键入: GET /index.html HTTP/1.0 此时可以看到请求结束之后马上断开。 读者还可以尝试在使用GET或POST等时,带上头域信息,例如键入如下信息: GET /index.html HTTP/1.1connection: closeHost: www.baidu.com 2.5 常用的请求方式 常用的请求方式是GET和POST. l GET方式:是以实体的方式得到由请求URI所指定资源的信息,如果请求URI只是一个数据产生过程,那么最终要在响应实体中返回的是处理过程的结果所指向的资源,而不是处理过程的描述。 l POST方式:用来向目的服务器发出请求,要求它接受被附在请求后的实体,并把它当作请求队列中请求URI所指定资源的附加新子项,Post被设计成用统一的方法实现下列功能: 1:对现有资源的解释; 2:向电子公告栏、新闻组、邮件列表或类似讨论组发信息; 3:提交数据块; 4:通过附加操作来扩展数据库 。 从上面描述可以看出,Get是向服务器发索取数据的一种请求;而Post是向服务器提交数据的一种请求,要提交的数据位于信息头后面的实体中。 GET与POST方法有以下区别: (1) 在客户端,Get方式在通过URL提交数据,数据在URL中可以看到;POST方式,数据放置在HTML HEADER内提交。 (2) GET方式提交的数据最多只能有1024字节,而POST则没有此限制。 (3) 安全性问题。正如在(1)中提到,使用 Get 的时候,参数会显示在地址栏上,而 Post 不会。所以,如果这些数据是中文数据而且是非敏感数据,那么使用 get;如果用户输入的数据不是中文字符而且包含敏感数据,那么还是使用 post为好。 (4) 安全的和幂等的。所谓安全的意味着该操作用于获取信息而非修改信息。幂等的意味着对同一 URL 的多个请求应该返回同样的结果。完整的定义并不像看起来那样严格。换句话说,GET 请求一般不应产生副作用。从根本上讲,其目标是当用户打开一个链接时,她可以确信从自身的角度来看没有改变资源。比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。反之亦然。POST 请求就不那么轻松了。POST 表示可能改变服务器上的资源的请求。仍然以新闻站点为例,读者对文章的注解应该通过 POST 请求实现,因为在注解提交之后站点已经不同了(比方说文章下面出现一条注解)。 2.6 请求头 HTTP最常见的请求头如下: l Accept:浏览器可接受的MIME类型; l Accept-Charset:浏览器可接受的字符集; l Accept-Encoding:浏览器能够进行解码的数据编码方式,比如gzip。Servlet能够向支持gzip的浏览器返回经gzip编码的HTML页面。许多情形下这可以减少5到10倍的下载时间; l Accept-Language:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到; l Authorization:授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中; l Connection:表示是否需要持久连接。如果Servlet看到这里的值为“Keep-Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),它就可以利用持久连接的优点,当页面包含多个元素时(例如Applet,图片),显著地减少下载所需要的时间。要实现这一点,Servlet需要在应答中发送一个Content-Length头,最简单的实现方法是:先把内容写入ByteArrayOutputStream,然后在正式写出内容之前计算它的大小; l Content-Length:表示请求消息正文的长度; l Cookie:这是最重要的请求头信息之一; l From:请求发送者的email地址,由一些特殊的Web客户程序使用,浏览器不会用到它; l Host:初始URL中的主机和端口; l If-Modified-Since:只有当所请求的内容在指定的日期之后又经过修改才返回它,否则返回304“Not Modified”应答; l Pragma:指定“no-cache”值表示服务器必须返回一个刷新后的文档,即使它是代理服务器而且已经有了页面的本地拷贝; l Referer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。 l User-Agent:浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用; l UA-Pixels,UA-Color,UA-OS,UA-CPU:由某些版本的IE浏览器所发送的非标准的请求头,表示屏幕大小、颜色深度、操作系统和CPU类型。 2.7 响应头 HTTP最常见的响应头如下所示: l Allow:服务器支持哪些请求方法(如GET、POST等); l Content-Encoding:文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader("Accept-Encoding"))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面; l Content-Length:表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入ByteArrayOutputStram,完成后查看其大小,然后把该值放入Content-Length头,最后通过byteArrayStream.writeTo(response.getOutputStream()发送内容; l Content-Type: 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentTyep。 可在web.xml文件中配置扩展名和MIME类型的对应关系; l Date:当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦; l Expires:指明应该在什么时候认为文档已经过期,从而不再缓存它。 l Last-Modified:文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置; l Location:表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302; l Refresh:表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过setHeader("Refresh", "5; URL=http://host/path")让浏览器读取指定的页面。注意这种功能通常是通过设置HTML页面HEAD区的<META HTTP-EQUIV="Refresh" CONTENT="5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用CGI或Servlet的HTML编写者十分重要。但是,对于Servlet来说,直接设置Refresh头更加方便。注意Refresh的意义是“N秒之后刷新本页面或访问指定页面”,而不是“每隔N秒刷新本页面或访问指定页面”。因此,连续刷新要求每次都发送一个Refresh头,而发送204状态代码则可以阻止浏览器继续刷新,不管是使用Refresh头还是<META HTTP-EQUIV="Refresh" ...>。注意Refresh头不属于HTTP 1.1正式规范的一部分,而是一个扩展,但Netscape和IE都支持它。 2.8实体头 实体头用坐实体内容的元信息,描述了实体内容的属性,包括实体信息类型,长度,压缩方法,最后一次修改时间,数据有效性等。 l Allow:GET,POST l Content-Encoding:文档的编码(Encode)方法,例如:gzip,见“2.5 响应头”; l Content-Language:内容的语言类型,例如:zh-cn; l Content-Length:表示内容长度,eg:80,可参考“2.5响应头”; l Content-Location:表示客户应当到哪里去提取文档,例如:http://www.dfdf.org/dfdf.html,可参考“2.5响应头”; l Content-MD5:MD5 实体的一种MD5摘要,用作校验和。发送方和接受方都计算MD5摘要,接受方将其计算的值与此头标中传递的值进行比较。Eg1:Content-MD5: <base64 of 128 MD5 digest>。Eg2:dfdfdfdfdfdfdff==; l Content-Range:随部分实体一同发送;标明被插入字节的低位与高位字节偏移,也标明此实体的总长度。Eg1:Content-Range: 1001-2000/5000,eg2:bytes 2543-4532/7898 l Content-Type:标明发送或者接收的实体的MIME类型。Eg:text/html; charset=GB2312 主类型/子类型; l Expires:为0证明不缓存; l Last-Modified:WEB 服务器认为对象的最后修改时间,比如文件的最后修改时间,动态页面的最后产生时间等等。例如:Last-Modified:Tue, 06 May 2008 02:42:43 GMT. 2.8扩展头 在HTTP消息中,也可以使用一些再HTTP1.1正式规范里没有定义的头字段,这些头字段统称为自定义的HTTP头或者扩展头,他们通常被当作是一种实体头处理。 现在流行的浏览器实际上都支持Cookie,Set-Cookie,Refresh和Content-Disposition等几个常用的扩展头字段。 l Refresh:1;url=http://www.dfdf.org //过1秒跳转到指定位置; l Content-Disposition:头字段,可参考“2.5响应头”; l Content-Type:WEB 服务器告诉浏览器自己响应的对象的类型。 eg1:Content-Type:application/xml ; eg2:applicaiton/octet-stream; Content-Disposition:attachment; filename=aaa.zip。 附录:参考资料 《HTTP1.1和HTTP1.0的区别》: http://blog.csdn.net/yanghehong/archive/2009/05/28/4222594.aspx 《HTTP请求(GET和POST区别)和响应》: http://www.blogjava.net/honeybee/articles/164008.html 《HTTP请求头概述_百度知道》: http://zhidao.baidu.com/question/32517427.html 《实体头和扩展头》: http://www.cnblogs.com/tongzhiyong/archive/2008/03/16/1108776.html 3. 深入了解篇 3.1 Cookie和Session Cookie和Session都为了用来保存状态信息,都是保存客户端状态的机制,它们都是为了解决HTTP无状态的问题而所做的努力。 Session可以用Cookie来实现,也可以用URL回写的机制来实现。用Cookie来实现的Session可以认为是对Cookie更高级的应用。 3.1.1两者比较 Cookie和Session有以下明显的不同点: 1)Cookie将状态保存在客户端,Session将状态保存在服务器端; 2)Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器。Cookie最早在RFC2109中实现,后续RFC2965做了增强。网络服务器用HTTP头向客户端发送cookies,在客户终端,浏览器解析这些cookies并将它们保存为一个本地文件,它会自动将同一服务器的任何请求缚上这些cookies。Session并没有在HTTP的协议中定义; 3)Session是针对每一个用户的,变量的值保存在服务器上,用一个sessionID来区分是哪个用户session变量,这个值是通过用户的浏览器在访问的时候返回给服务器,当客户禁用cookie时,这个值也可能设置为由get来返回给服务器; 4)就安全性来说:当你访问一个使用session 的站点,同时在自己机子上建立一个cookie,建议在服务器端的SESSION机制更安全些.因为它不会任意读取客户存储的信息。 3.1.2 Session机制 Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。 当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为 session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个 session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id将被在本次响应中返回给客户端保存。 3.1.6 Session的实现方式 3.1.6.1 使用Cookie来实现 服务器给每个Session分配一个唯一的JSESSIONID,并通过Cookie发送给客户端。 当客户端发起新的请求的时候,将在Cookie头中携带这个JSESSIONID。这样服务器能够找到这个客户端对应的Session。 流程如下图所示: 3.1.6.2 使用URL回显来实现 URL回写是指服务器在发送给浏览器页面的所有链接中都携带JSESSIONID的参数,这样客户端点击任何一个链接都会把JSESSIONID带会服务器。 如果直接在浏览器输入服务端资源的url来请求该资源,那么Session是匹配不到的。 Tomcat对Session的实现,是一开始同时使用Cookie和URL回写机制,如果发现客户端支持Cookie,就继续使用Cookie,停止使用URL回写。如果发现Cookie被禁用,就一直使用URL回写。jsp开发处理到Session的时候,对页面中的链接记得使用response.encodeURL() 。 3.1.3在J2EE项目中Session失效的几种情况 1)Session超时:Session在指定时间内失效,例如30分钟,若在30分钟内没有操作,则Session会失效,例如在web.xml中进行了如下设置: <session-config> <session-timeout>30</session-timeout> //单位:分钟 </session-config> 2)使用session.invalidate()明确的去掉Session。 3.1.4与Cookie相关的HTTP扩展头 1)Cookie:客户端将服务器设置的Cookie返回到服务器; 2)Set-Cookie:服务器向客户端设置Cookie; 3)Cookie2 (RFC2965)):客户端指示服务器支持Cookie的版本; 4)Set-Cookie2 (RFC2965):服务器向客户端设置Cookie。 3.1.5Cookie的流程 服务器在响应消息中用Set-Cookie头将Cookie的内容回送给客户端,客户端在新的请求中将相同的内容携带在Cookie头中发送给服务器。从而实现会话的保持。 流程如下图所示: 3.2 缓存的实现原理 3.2.1什么是Web缓存 WEB缓存(cache)位于Web服务器和客户端之间。 缓存会根据请求保存输出内容的副本,例如html页面,图片,文件,当下一个请求来到的时候:如果是相同的URL,缓存直接使用副本响应访问请求,而不是向源服务器再次发送请求。 HTTP协议定义了相关的消息头来使WEB缓存尽可能好的工作。 3.2.2缓存的优点 q 减少相应延迟:因为请求从缓存服务器(离客户端更近)而不是源服务器被相应,这个过程耗时更少,让web服务器看上去相应更快。 q 减少网络带宽消耗:当副本被重用时会减低客户端的带宽消耗;客户可以节省带宽费用,控制带宽的需求的增长并更易于管理。 3.2.3与缓存相关的HTTP扩展消息头 q Expires:指示响应内容过期的时间,格林威治时间GMT q Cache-Control:更细致的控制缓存的内容 q Last-Modified:响应中资源最后一次修改的时间 q ETag:响应中资源的校验值,在服务器上某个时段是唯一标识的。 q Date:服务器的时间 q If-Modified-Since:客户端存取的该资源最后一次修改的时间,同Last-Modified。 q If-None-Match:客户端存取的该资源的检验值,同ETag。 3.2.4客户端缓存生效的常见流程 服务器收到请求时,会在200OK中回送该资源的Last-Modified和ETag头,客户端将该资源保存在cache中,并记录这两个属性。当客户端需要发送相同的请求时,会在请求中携带If-Modified-Since和If-None-Match两个头。两个头的值分别是响应中Last-Modified和ETag头的值。服务器通过这两个头判断本地资源未发生变化,客户端不需要重新下载,返回304响应。常见流程如下图所示: 3.2.5 Web缓存机制 HTTP/1.1中缓存的目的是为了在很多情况下减少发送请求,同时在许多情况下可以不需要发送完整响应。前者减少了网络回路的数量;HTTP利用一个“过期(expiration)”机制来为此目的。后者减少了网络应用的带宽;HTTP用“验证(validation)”机制来为此目的。 HTTP定义了3种缓存机制: 1)Freshness:允许一个回应消息可以在源服务器不被重新检查,并且可以由服务器和客户端来控制。例如,Expires回应头给了一个文档不可用的时间。Cache-Control中的max-age标识指明了缓存的最长时间; 2)Validation:用来检查以一个缓存的回应是否仍然可用。例如,如果一个回应有一个Last-Modified回应头,缓存能够使用If-Modified-Since来判断是否已改变,以便判断根据情况发送请求; 3)Invalidation: 在另一个请求通过缓存的时候,常常有一个副作用。例如,如果一个URL关联到一个缓存回应,但是其后跟着POST、PUT和DELETE的请求的话,缓存就会过期。 3.3 断点续传和多线程下载的实现原理 q HTTP协议的GET方法,支持只请求某个资源的某一部分; q 206 Partial Content 部分内容响应; q Range 请求的资源范围; q Content-Range 响应的资源范围; q 在连接断开重连时,客户端只请求该资源未下载的部分,而不是重新请求整个资源,来实现断点续传。 分块请求资源实例: Eg1:Range: bytes=306302- :请求这个资源从306302个字节到末尾的部分; Eg2:Content-Range: bytes 306302-604047/604048:响应中指示携带的是该资源的第306302-604047的字节,该资源共604048个字节; 客户端通过并发的请求相同资源的不同片段,来实现对某个资源的并发分块下载。从而达到快速下载的目的。目前流行的FlashGet和迅雷基本都是这个原理。 多线程下载的原理: q 下载工具开启多个发出HTTP请求的线程; q 每个http请求只请求资源文件的一部分:Content-Range: bytes 20000-40000/47000; q 合并每个线程下载的文件。 3.4 https通信过程 3.4.1什么是https HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容请看SSL。 https所用的端口号是443。 3.4.2 https的实现原理 有两种基本的加解密算法类型: 1)对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有DES、AES等; 2)非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有RSA、DSA等。 下面看一下https的通信过程: https通信的优点: 1)客户端产生的密钥只有客户端和服务器端能得到; 2)加密的数据只有客户端和服务器端才能得到明文; 3)客户端到服务端的通信是安全的。 3.5 http代理 3.5.1 http代理服务器 代理服务器英文全称是Proxy Server,其功能就是代理网络用户去取得网络信息。形象的说:它是网络信息的中转站。 代理服务器是介于浏览器和Web服务器之间的一台服务器,有了它之后,浏览器不是直接到Web服务器去取回网页而是向代理服务器发出请求,Request信号会先送到代理服务器,由代理服务器来取回浏览器所需要的信息并传送给你的浏览器。 而且,大部分代理服务器都具有缓冲的功能,就好象一个大的Cache,它有很大的存储空间,它不断将新取得数据储存到它本机的存储器上,如果浏览器所请求的数据在它本机的存储器上已经存在而且是最新的,那么它就不重新从Web服务器取数据,而直接将存储器上的数据传送给用户的浏览器,这样就能显著提高浏览速度和效率。 更重要的是:Proxy Server(代理服务器)是Internet链路级网关所提供的一种重要的安全功能,它的工作主要在开放系统互联(OSI)模型的对话层。 3.5.2 http代理服务器的主要功能 主要功能如下: 1)突破自身IP访问限制,访问国外站点。如:教育网、169网等网络用户可以通过代理访问国外网站; 2)访问一些单位或团体内部资源,如某大学FTP(前提是该代理地址在该资源的允许访问范围之内),使用教育网内地址段免费代理服务器,就可以用于对教育 网开放的各类FTP下载上传,以及各类资料查询共享等服务; 3)突破中国电信的IP封锁:中国电信用户有很多网站是被限制访问的,这种限制是人为的,不同Serve对地址的封锁是不同的。所以不能访问时可以换一个国 外的代理服务器试试; 4)提高访问速度:通常代理服务器都设置一个较大的硬盘缓冲区,当有外界的信息通过时,同时也将其保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度; 5)隐藏真实IP:上网者也可以通过这种方法隐藏自己的IP,免受攻击。 3.5.3 http代理图示 http代理的图示见下图: 对于客户端浏览器而言,http代理服务器相当于服务器。 而对于Web服务器而言,http代理服务器又担当了客户端的角色。 3.6 虚拟主机的实现 3.6.1什么是虚拟主机 虚拟主机:是在网络服务器上划分出一定的磁盘空间供用户放置站点、应用组件等,提供必要的站点功能与数据存放、传输功能。 所谓虚拟主机,也叫“网站空间”就是把一台运行在互联网上的服务器划分成多个“虚拟”的服务器,每一个虚拟主机都具有独立的域名和完整的Internet服务器(支持WWW、FTP、E-mail等)功能。一台服务器上的不同虚拟主机是各自独立的,并由用户自行管理。但一台服务器主机只能够支持一定数量的虚拟主机,当超过这个数量时,用户将会感到性能急剧下降。 3.6.2虚拟主机的实现原理 虚拟主机是用同一个WEB服务器,为不同域名网站提供服务的技术。Apache、Tomcat等均可通过配置实现这个功能。 相关的HTTP消息头:Host。 例如:Host: www.baidu.com 客户端发送HTTP请求的时候,会携带Host头,Host头记录的是客户端输入的域名。这样服务器可以根据Host头确认客户要访问的是哪一个域名。 附录:参考资料 《理解Cookie和Session机制》: http://sumongh.javaeye.com/blog/82498 《浅析HTTP协议》: http://203.208.39.132/search?q=cache:CdXly_88gjIJ:www.cnblogs.com/gpcuster/archive/2009/05/25/1488749.html+http%E5%8D%8F%E8%AE%AE+web%E7%BC%93%E5%AD%98&cd=27&hl=zh-CN&ct=clnk&gl=cn&st_usg=ALhdy2-vzOcP8XTG1h7lcRr2GJrkTbH2Cg 《http代理_百度百科》: http://baike.baidu.com/view/1159398.htm 《虚拟主机_百度百科》: http://baike.baidu.com/view/7383.htm 《https_百度百科》: http://baike.baidu.com/view/14121.htm
——有感于实际编程和开源项目研究。 我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的。本文的主要内容如下: 1、网络中进程之间如何通信? 本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类: 消息传递(管道、FIFO、消息队列) 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量) 共享内存(匿名的和具名的) 远程过程调用(Solaris门和Sun RPC) 但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。 使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。 2、什么是Socket? 上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。 socket一词的起源 在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。” 3、socket的基本操作 既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。 3.1、socket()函数 int socket(int domain, int type, int protocol); socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。 正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为: domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。 type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。 protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。 注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。 当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。 3.2、bind()函数 正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 函数的三个参数分别为: sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。 addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是: struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ ipv6对应的是: struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ Unix域对应的是: #define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ addrlen:对应的是地址的长度。 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。 网络字节序与主机字节序 主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下: a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。 b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。 网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。 所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。 3.3、listen()、connect()函数 如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。 int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。 connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。 3.4、accept()函数 TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。 注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。 3.5、read()、write()等函数 万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组: read()/write() recv()/send() readv()/writev() recvmsg()/sendmsg() recvfrom()/sendto() 我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下: #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); #include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。 write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。 其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。 3.6、close()函数 在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。 #include <unistd.h> int close(int fd); close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。 注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。 4、socket中TCP的三次握手建立连接详解 我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下: 客户端向服务器发送一个SYN J 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1 客户端再想服务器发一个确认ACK K+1 只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图: 图1、socket中发送的TCP三次握手 从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。 总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。 5、socket中TCP的四次握手释放连接详解 上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图: 图2、socket中发送的TCP四次握手 图示过程如下: 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M; 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据; 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N; 接收到这个FIN的源发送端TCP对它进行确认。 这样每个方向上都有一个FIN和ACK。 6、一个例子(实践一下) 说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。 服务器端代码: if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){ printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666); if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){ printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); if( listen(listenfd, 10) == -1){ printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); printf("======waiting for client's request======\n"); while(1){ if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){ printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; n = recv(connfd, buff, MAXLINE, 0); buff[n] = '\0'; printf("recv msg from client: %s\n", buff); close(connfd); close(listenfd); if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ printf("create socket error: %s(errno: %d)\n", strerror(errno),errno); exit(0); memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(6666); if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){ printf("inet_pton error for %s\n",argv[1]); exit(0); if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){ printf("connect error: %s(errno: %d)\n",strerror(errno),errno); exit(0); printf("send msg to server: \n"); fgets(sendline, 4096, stdin); if( send(sockfd, sendline, strlen(sendline), 0) < 0) printf("send msg error: %s(errno: %d)\n", strerror(errno), errno); exit(0); close(sockfd); exit(0); 当然上面的代码很简单,也有很多缺点,这就只是简单的演示socket的基本函数使用。其实不管有多复杂的网络程序,都使用的这些基本函数。上面的服务器使用的是迭代模式的,即只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力是很弱的,现实中的服务器都需要有并发处理能力!为了需要并发处理,服务器需要fork()一个新的进程或者线程去处理请求等。 7、动动手 留下一个问题,欢迎大家回帖回答!!!是否熟悉Linux下网络编程?如熟悉,编写如下程序完成如下功能: 服务器端: 接收地址192.168.100.2的客户端信息,如信息为“Client Query”,则打印“Receive Query” 向地址192.168.100.168的服务器端顺序发送信息“Client Query test”,“Cleint Query”,“Client Query Quit”,然后退出。 题目中出现的ip地址可以根据实际情况定。 目的:通过全面的分析Android的鼠标和键盘事件。了解Android中如何接收和处理键盘和鼠标事件,以及如何用代码来产生事件。 主要学习内容: 1. 接收并处理鼠标事件:按下、弹起、移动、双击、长按、滑动、滚动 2. 接收并处理按键事件:按下、弹起 3. 模拟鼠标/按键事件 1. Android事件 现代的用户界面,都是以事件来驱动的来实现人机交换的,而Android上的一套UI控件,无非就是派发鼠标和键盘事件,然后每个控件收到相应的事件之后,做相应的处理。如Button控件,就只需要处理Down、move、up这几个事件,Down的时候重绘控件,move的时候一般也需要重绘控件,当up的时候,重绘控件,然后产生onClick事件。在Android中通过实现OnClickListener接口的onClick方法来实现对Button控件的处理。 对于触摸屏事件(鼠标事件)有按下有:按下、弹起、移动、双击、长按、滑动、滚动。按下、弹起、移动(down、move、up)是简单的触摸屏事件,而双击、长按、滑动、滚动需要根据运动的轨迹来做识别的。在Android中有专门的类去识别,android.view.GestureDetector。 对于按键(keyevent),无非就是按下、弹起、长按等。 2. Android事件处理 Android手机的坐标系是以左上定点为原点坐标(0,0), 向右为X抽正方形,向下为Y抽正方向。 2.1 简单触摸屏事件 在Android中任何一个控件和Activity都是间接或者直接继承于android.view.View。一个View对象可以处理测距、布局、绘制、焦点变换、滚动条,以及触屏区域自己表现的按键和手势。当我们重写View中的onTouchEvent(MotionEvent)方法后,就可以处理简单的触摸屏事件。 代码如下: [java] view plaincopy public boolean onTouchEvent(MotionEvent event) int events[] = {MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_OUTSIDE, MotionEvent.ACTION_POINTER_DOWN,MotionEvent.ACTION_POINTER_UP, MotionEvent.EDGE_TOP,MotionEvent.EDGE_BOTTOM,MotionEvent.EDGE_LEFT,MotionEvent.EDGE_RIGHT}; String szEvents[]={"ACTION_DOWN", "ACTION_MOVE", "ACTION_UP", "ACTION_MOVE", "ACTION_CANCEL", "ACTION_OUTSIDE", "ACTION_POINTER_DOWN","ACTION_POINTER_UP", "EDGE_TOP","EDGE_BOTTOM","EDGE_LEFT","EDGE_RIGHT"}; for(int i=0; i < events.length; i++) if(events[i] == event.getAction()) if(oldevent != event.getAction()) DisplayEventType(szEvents[i]); oldevent = event.getAction(); break; return super.onTouchEvent(event); 2.2手势识别 很多时候,一个好的用户界面能够吸引用户的眼球。现在我们经常看到一些好的界面都带有滑动、滚动等效果。但是触摸屏是不可能产生滚动、滑动的消息的,需要根据其运动的轨迹用算法去判断实现。在Android系统中,android.view.GestureDetector来实现手势的识别,我们只需要实现其GestureDetector.OnGestureListener接口来侦听GestureDetector识别后的事件。我们需要在onTouchEvent,GestureDetector的onTouchEvent方法是进行轨迹识别。 代码如下: [java] view plaincopy import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; public class TestEvent extends Activity { /** Called when the activity is first created. */ TextView m_eventType; int oldevent = -1; private GestureDetector gestureDetector= new GestureDetector(new OnGestureListener() // 鼠标按下的时候,会产生onDown。由一个ACTION_DOWN产生。 public boolean onDown(MotionEvent event) { DisplayEventType("mouse down" + " " + event.getX() + "," + event.getY()); return false; // 用户按下触摸屏、快速移动后松开,这个时候,你的手指运动是有加速度的。 // 由1个MotionEvent ACTION_DOWN, // 多个ACTION_MOVE, 1个ACTION_UP触发 // e1:第1个ACTION_DOWN MotionEvent // e2:最后一个ACTION_MOVE MotionEvent // velocityX:X轴上的移动速度,像素/秒 // velocityY:Y轴上的移动速度,像素/秒 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { DisplayEventType("onFling"); return false; // 用户长按触摸屏,由多个MotionEvent ACTION_DOWN触发 public void onLongPress(MotionEvent event) { DisplayEventType("on long pressed"); // 滚动事件,当在触摸屏上迅速的移动,会产生onScroll。由ACTION_MOVE产生 // e1:第1个ACTION_DOWN MotionEvent // e2:最后一个ACTION_MOVE MotionEvent // distanceX:距离上次产生onScroll事件后,X抽移动的距离 // distanceY:距离上次产生onScroll事件后,Y抽移动的距离 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { DisplayEventType("onScroll" + " " + distanceX + "," + distanceY); return false; //点击了触摸屏,但是没有移动和弹起的动作。onShowPress和onDown的区别在于 //onDown是,一旦触摸屏按下,就马上产生onDown事件,但是onShowPress是onDown事件产生后, //一段时间内,如果没有移动鼠标和弹起事件,就认为是onShowPress事件。 public void onShowPress(MotionEvent event) { DisplayEventType("pressed"); // 轻击触摸屏后,弹起。如果这个过程中产生了onLongPress、onScroll和onFling事件,就不会 // 产生onSingleTapUp事件。 public boolean onSingleTapUp(MotionEvent event) { DisplayEventType("Tap up"); return false; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); m_eventType = (TextView)this.findViewById(R.id.eventtype); @Override public boolean onTouchEvent(MotionEvent event) if(gestureDetector.onTouchEvent(event)) return true; return false; 2.3键盘事件 键盘事件比较简单,直接重写原来的方法就可以了。 代码如下: [java] view plaincopy public boolean onKeyDown(int keyCode, KeyEvent event) switch(keyCode) case KeyEvent.KEYCODE_HOME: DisplayEventType("Home down"); break; case KeyEvent.KEYCODE_BACK: DisplayEventType("Back down"); break; case KeyEvent.KEYCODE_DPAD_LEFT: DisplayEventType("Left down"); break; //return true; return super.onKeyDown(keyCode, event); @Override public boolean onKeyUp(int keyCode, KeyEvent event) switch(keyCode) case KeyEvent.KEYCODE_HOME: DisplayEventType("Home up"); break; case KeyEvent.KEYCODE_BACK: DisplayEventType("Back up"); break; case KeyEvent.KEYCODE_DPAD_LEFT: DisplayEventType("Left up"); break; //return true; return super.onKeyUp(keyCode, event); 3. 模拟鼠标/按键事件 Instrumentation发送键盘鼠标事件:Instrumentation提供了丰富的以send开头的函数接口来实现模拟键盘鼠标,如下所述: sendCharacterSync(int keyCode) //用于发送指定KeyCode的按键 sendKeyDownUpSync(int key) //用于发送指定KeyCode的按键 sendPointerSync(MotionEvent event) //用于模拟Touch sendStringSync(String text) //用于发送字符串 Instrumentation inst=new Instrumentation(); inst.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(),SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 10, 10, 0)); inst.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(),SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 10, 10, 0)); 参考网址:http://blog.csdn.net/jinhaijian/article/details/6013985 Android提供了强大的事件处理机制,它包括两套处理机制: 1.基于监听的事件处理 2.基于回调的事件处理 对于Android基于监听的事件处理,主要的做法是为Android界面组件绑定特定的事件监听器。 对于Android基于回调的事件处理,主要的方法是重写Android组件特定的回调方法或者重写 Activity的回调方法 一、基于监听的事件处理 在事件监听的处理模型中,主要涉及如下三类对象: 1.Event Source(事件源):事件发生的场所,通常就是各个组件、例如按钮、窗口、菜单等。 2.Event(事件):事件封装了界面组件上发生的特定事情(通常就是一次用户操作)。 3.Event Listener(事件监听器):扶着监听事件源所发生的事件,并对各种事件做出相应的响应。 事件处理流程示意图如下: 作为事件监听器类,还有一种是匿名内部类作为事件监听器类,这里就不详细介绍了。 Android还中还有一种更简单的绑定事件监听器的方式,直接在界面布局中为指定的标签绑定事件处理方法。 如:android:onClick="clickHandler",这样就意味着开发者需要在该界面布局对应的Activity中定义一个void clickHandler(View source),该方法将会处理该按钮上的单击事件。 下面我们来看一段Java代码: [java] public class Ex003_01Activity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //定义一个事件的处理方法 //轻重source参数代表事件源 public void clickHandler(View source){ TextView show=(TextView)findViewById(R.id.tv); show.setText("bn按钮被点击了"); 二、基于回调事件处理 从代码的实现的角度来看,基于回调的事件处理模型更加简单。如果说事件监听制是一种委托式的事件处理,那么回调机制恰好与之相反:对于基于回调机制的事件处理模型来说,事件源与事件监听器是统一的。为了使用回调机制类处理GUI组件上所发生的事件,我们需要为该组件提供对应的事件处理方法--而Java又是一种静态语言,我们无法为某个对象动态的添加方法,因此只能继承GUI组件类,并重写该组件类的事件处理方法来实现。 为了实现回调机制的事件处理,Android为所有的GUI组件都提供了一些事件处理的回调方法,以View为例,该类包含如下方法: boolean onKeyDown(int keyCode,KeyEvent event):当用户在该组件上按下某个键时触发的方法。 boolean onKeyLongPress(int keyCode,KeyEvent event):当用户在该组件上长按某个按钮时触发该方法。 boolean onKeyShortcut(int keyCode,KeyEvent event): 当一个快捷键事件发生时触发该放过。 boolean onKeyUp(int keyCode,KeyEvent event):当用户在该组件上松开某个按键时触发该方法 boolean onTouchEvent(MotionEvent event):当用户在该组件上触发触摸屏事件时触发该方法。 boolean onTrackballEventI(MotionEvent event):当用户在该组件上触发轨迹球屏事件时触发该事件。 下面我们来看一段代码: [java] public class MyButton extends Button public MyButton(Context context , AttributeSet set) super(context , set); // TODO Auto-generated constructor stub @Override public boolean onKeyDown(int keyCode, KeyEvent event) super.onKeyDown(keyCode , event); Log.v("-crazyit.org-" , "the onKeyDown in MyButton"); //返回true,表明该事件不会向外扩散 return true; 上面的代码我们重写了Button类的onKeyDown(int keyCode,KeyEvent event)方法,该方法将会负责处理按钮上的键盘事件。 基于回调的事件传播 几乎所有的基于回调的事件处理方法都有一个boolean类型的返回值,该返回值用于标识该处理方法是否能完全处理该事件: 1.如果返回true,则表明该处理方法已完全处理了该事件,该事件不会被传播出去。 2.如果返回false,表明该处理方法未完全处理该事件,该事件会传播出去。 对于基于回调的事件处理传播而言,某组件上所发生的事情不仅激发该组件上的回调方法,也会触发该组件所在的Activity的回调方法——只要事件能传播到该Activity。 参考网址:http://www.2cto.com/kf/201302/190400.html (一) 事件使我们在于UI交互式发生的,我们点击一个按键时,可能就已经除非好几个事件,例如我们点击数字键“0”,他会涉及到按下事件,和一个弹起(松开)事件,在我们android中还可能涉及到触摸屏事件,所以在android系统中,事件是作为常用的功能之一; 在android下,事件的发生是在监听器下进行,android系统可以响应按键事件和触摸屏事件,事件说明如下: l onClick(View v) 一个普通的点击按钮事件 l boolean onKeyMultiple(int keyCode,int repeatCount,KeyEvent event)用于在多个事件连续时发生,用于按键重复,必须重载@Override实现 l boolean onKeyDown(int keyCode,KeyEvent event) 用于在按键进行按下时发生 l boolean onKeyUp(int keyCode,KeyEvent event) 用于在按键进行释放时发生 l onTouchEvent(MotionEvent event)触摸屏事件,当在触摸屏上有动作时发生 l boolean onKeyLongPress(int keyCode, KeyEvent event)当你长时间按时发生(疑问?) (二) 首先我们建立一个android项目,当项目建立好之后,直接在默认的main.xml文件中拖放一个button 按钮,其他的不需要在这里做什么了,然后就可以到命名好的.java文件中进行先关代码的书写; 1. 对要使用的控件进行引用,当然你也可以用到的时候再在相关类控件添加引用 import android.app.Activity; import android.os.Bundle; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.widget.Button; import android.widget.Toast; 2. 获得相关对象,设置控件监听器 Button button=(Button) findViewById(R.id.button1); //设置监听 button.setOnClickListener(new Button.OnClickListener() @Override public void onClick(View // TODO Auto-generated method stub DisplayToast("事件触发成功"); 请注意这里末尾使用的是分号“;这里就是获得button的实例,然后对他进行监听,当用户点击时就会发生onClick事件,这里还用到一个方法,就是显示一个短消息,在屏幕停留几秒钟就会自动消失,其方法如下: public void DisplayToast(String Toast.makeText(this, str, Toast.LENGTH_SHORT).show(); 当然你也可以设置显示长点,即Toast.LENGTH_SHORT改为Toast.LENGTH_LONG 3. 当按键按下是发生的事件 public boolean onKeyDown(int keyCode,KeyEvent event) switch(keyCode) case KeyEvent.KEYCODE_0: DisplayToast("你按下数字键0"); break; case KeyEvent.KEYCODE_DPAD_CENTER: DisplayToast("你按下中间键"); break;sss case KeyEvent.KEYCODE_DPAD_DOWN: DisplayToast("你按下下方向键"); break; case KeyEvent.KEYCODE_DPAD_LEFT: DisplayToast("你按下左方向键"); break; case KeyEvent.KEYCODE_DPAD_RIGHT: DisplayToast("你按下右方向键"); break; case KeyEvent.KEYCODE_DPAD_UP: DisplayToast("你按下上方向键"); break; case KeyEvent.KEYCODE_ALT_LEFT: DisplayToast("你按下组合键alt+←"); break; return super.onKeyDown(keyCode, event); 这里所有的keyCode都囊括了,这只是几个比较典型的例子,效果如下: