目录
1.linux中断分层概念
2.linux系统的驱动-设备-总线模型
3.如何在Python中创建一个线程
4.操作系统的最小调度单位:线程
5.原子操作:开始执行到结束期间都不会打断的操作单元。
6.信号量、互斥体、自旋锁的概念
7.IIc协议和SPI总线协议
8.串口通讯协议
9.socket
10.字符设备驱动程序框架
11.块设备驱动程序框架
12.TCP的三次握手与四次挥手
13.TCP报文
14.TCP与UDP的区别
15.进程间的通信
16.myuvc框架(很重要)
17.网络协议的层次
18.位置无关码,位置相关码的概念
19.static变量的作用
20.const的作用
1.linux中断分层概念
中断会打断内核中进程的正常调度和运行,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。所以中断处理程序中所有不要求立即完成的,在开中断的环境下,由中断后半段完成。
中断前半段主要完成尽可能少的比较紧急的功能,例如简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。这样,顶半部执行的速度就会很快,可以服务更多的中断请求。上半部处理硬件相关和非常紧急的操作(如读硬件数据),而下半部处理没有那么紧急的操作(将硬件数据放到队列等)。低优先级中断的上半部可以抢占高优先级中断的下半部。
Linux 系统实现底半部的机制主要有:三种:tasklet,工作队列和软中断。
软中断不同于软件中断,软中断发生的时机是从中断、调用或者异常返回用户空间之前,按照软中断在结构数组中定义的顺序依次执行。同一个软中断可以在不同CPU上并发执行。软中断执行过程中也不允许睡眠和进程切换。
tasklet基于软中断实现,在软中断结构数组中占用两项。当软中断执行到这两项时就会跳转到tasklet函数入口处,依次执行队列中的tasklet函数。同一个tasklet不能在不同CPU上并发执行,但是不同tasklet可以在不同CPU上执行。tasklet始终运行在被初始提交的同一处理器上。
工作队列是一种将任务推后执行的形式,他把推后的任务交由一个内核线程去执行。这样如果在中断函数中使用中断分层(工作队列方式),中断函数的第二部分会在进程上下文执行,它允许重新调度甚至睡眠。每个被推后的任务叫做“工作”,由这些工作组成的队列称为工作队列。工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以推荐使用工作队列。
2.linux系统的驱动-设备-总线模型
从Linux2.6开始Linux加入了一套驱动管理和注册机制—platform平台总线驱动模型。
platform平台总线是一条虚拟总线,platform_device为相应的设备,platform_driver为相应的驱动。与传统的bus/device/driver机制相比,platform由内核统一进行管理,提高了代码的可移植性和安全性。所谓的platform_device并不是与字符设备、块设备和网络设备并列的概念,而是Linux系统提供的一种附加手段。Linux总线设备驱动模型的框架如下图所示:
从图中我们可以很清楚的看出Linux平台总线设备驱动模型的整体架构。在总线设备驱动模型中,需关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。当系统向内核注册每一个驱动程序时,都要通过调用platform_driver_register函数将驱动程序注册到总线,并将其放入所属总线的drv链表中,注册驱动的时候会调用所属总线的match函数寻找该总线上与之匹配的每一个设备,如果找到与之匹配的设备则会调用相应的probe函数将相应的设备和驱动进行绑定;同样的当系统向内核注册每一个设备时,都要通过调用platform_device_register函数将设备注册到总线,并将其放入所属总线的dev链表中,注册设备的时候同样也会调用所属总线的match函数寻找该总线上与之匹配的每一个驱动程序,如果找到与之匹配的驱动程序时会调用相应的probe函数将相应的设备和驱动进行绑定;而这一匹配的过程是由总线自动完成的。
3.如何在Python中创建一个线程
方法1:用theading.Thread直接返回一个thread对象,然后运行它的start方法
import threading
thread = threading.Thread(target=self.threadFunction,args=(args,))
# 第一个是线程调用的函数,第二个是函数的参数
thread.start
方法2:继承自threading.Thread模块,重写run()函数
import threading
class MyThread(threading.Thread):
def __init__(self, target_function,args):
threading.Thread.__init__(self)
self.targetFunction = target_function
self.args = args
def run(self):
thread = threading.Thread(target=target_function, args=(args,))
thread.start()
t = Mythread(target_function,args)
t.start()
4.操作系统的最小调度单位:线程
5.原子操作:开始执行到结束期间都不会打断的操作单元。
6.信号量、互斥体、自旋锁的概念
信号量
是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。在多线程中,可以利用信号量来控制最大的线程数,将信号量的值设置为最大的线程数,当一个线程开始的时候,信号量减1,当线程结束后,信号量加1。当信号量为0,则该资源目前不可用,新的线程进入睡眠状态,直至信号量值大于0,线程被唤醒。
互斥体
互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex))。互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section)。因此,在任意时刻,只有一个线程被允许进入这样的代码保护区。
任何线程在进入临界区之前,必须获取(acquire)与此区域相关联的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放(release)该互斥体。
什么时候需要使用互斥体呢?互斥体用于保护共享的易变代码,也就是,全局或静态数据。这样的数据必须通过互斥体进行保护,以防止它们在多个线程同时访问时损坏
自旋锁
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
7.IIc协议和SPI总线协议
属于同步通信,同步通信是双方有一个共同的时钟,当发送时,接收方同时准备接收。
IIc总线协议(半双工):
SDA为数据线,SCL时钟线。
起始信号(S):SCL为高电平时,SDA从高电平变为低电平,开始传输数据。
结束信号(P):SCL为高电平时,SDA从低电平变为高电平,结束传输数据。
数据传输过程中:在SCL时钟的高电平周期内,SDA线上的数据必须保持稳定,数据线仅可以在时钟SCL为低电平时改变。
响应信号(ACK):接收器在收到8位数据后,在第九个时钟周期,拉低SDA电平。若没有ACK,SDA会被置高,这会引起主机发生RESTART或STOP流程。
写寄存器的标准流程为:
1. 主机发起START
2. 主机发送I2C addr(7bit)和w操作0(1bit),等待ACK
3. 从机发送ACK
4. 主机发送reg addr(8bit),等待ACK
5. 从机发送ACK
6. 主机发送data(8bit),即要写入寄存器中的数据,等待ACK
7. 从机发送ACK
8. 第6步和第7步可以重复多次,即顺序写多个寄存器
9. 主机发起STOP
读寄存器的标准流程为:
1. 主机发送I2C addr(7bit)和w操作1(1bit),等待ACK
2. 从机发送ACK
3. 主机发送reg addr(8bit),等待ACK
4. 从机发送ACK
5. 主机发起START
6. 主机发送I2C addr(7bit)和r操作1(1bit),等待ACK
7. 从机发送ACK
8. 从机发送data(8bit),即寄存器里的值
9. 主机发送ACK
10. 第8步和第9步可以重复多次,即顺序读多个寄存器
SPI总线协议(全双工):
SDO/MOSI – 主设备数据输出,从设备数据输入;SDI/MISO – 主设备数据输入,从设备数据输出;SCLK – 时钟信号,由主设备产生;CS/SS – 其中CS是控制芯片是否被选中的。
8.串口通讯协议
属于异步通信,异步通信是双方不需要共同的时钟,也就是接收方不知道发送方什么时候发送,所以在发送的信息中就要有提示接收方开始接收的信息,如开始位,结束时有停止位。异步通信一般发送单位是字符,同步通信发送单位是比特流(数据帧),但是这不是绝对的,异步通信有时也使用帧来通信。
异步通信时,主机跟从机的工作频率是分别由主机和从机的波特率发生器得到的,而同步通信是由主机提供工作时序。异步通信可以存在5%以下的波特率误差。但是理想状态是要相等的。
异步串行通信的数据格式:
(1)1位的起始位,规定为低电0;
(2)5-8位数据位,既要传送的有效信息;
(3)1位奇偶校验位;
(4)1-2位停止位,规定为高电平。
9.socket
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
socket通讯的基本过程(基于TCP协议)
(1)服务器端建立一个Socket,
(2)服务器端通过bind()绑定Socket端口,并通过调用listen()对端口进行监听;
(3)服务器端Socket通过调用accept()方法,使之处于阻塞,等待客户端连接。
(4)客户端创建一个Socket,并设置好服务器的IP和端口。
(5)客户端调用connect()发送连接请求,建立连接。
(6)建立连接后服务器和客户端通过send(),recv()进行发送和接收操作
(7)通信结束后服务器和客户端都通过调用close()将套接字关闭。
基于UDP协议
(1)服务端建立socket,
(2)服务端通过bind()进行绑定到本地地址和端口
(3)服务器端调用recvfrom()进行等待接收数据
(4)客户端建立socket,调用sendto()向服务器
(5)完成数据传输后,close()关闭套接字
10.字符设备驱动程序框架
通过init函数注册设备,建立设备节点,然后通过module_init宏来保证加载模块时会调用init函数。
而设备的操作是通过调用file_operation结构体的open、read、write函数。
probe函数跟init函数的区别在于,当不使用平台总线模型时,是不会使用probe函数的,当使用平台总线设备驱动模时,init函数只是向总线设备列表中添加了设备,当检测到相同名字的驱动程序,probe函数就会接着init函数的工作完成设备注册。
11.块设备驱动程序框架
12.TCP的三次握手与四次挥手
TCP是面向连接的字节流协议,是全双工的。全双工指在发送数据的同时也能够接收数据,两者同步进行。目前的网卡一般都支持全双工。所谓半双工就是指一个时间段内只有一个动作发生,早期的对讲机、以及早期集线器等设备都是基于半双工的产品。
TCP的标志位
- SYN(请求建立连接)
- ACK(确认)
- PSH(传送)
- FIN(结束)
- RST(重置)
- URG(紧急)
三次握手,第一次握手:客户端发送建立连接请求(SYN位置1),第二次握手:服务器端接收到连接请求,发送确认(SYN位置1,ACK位置1),第三次握手:客户端收到服务器端的确认,发出一个ACK给服务器(ACK位置1)。
为什么需要三次握手?
建立三次握手主要是因为客户端发送了再一次的确认,那么A为什么会再确认一次呢,主要是为了防止已失效的连接请求报文段又突然传送给服务器,从而产生了错误。
所谓“已失效的连接请求报文”是这样产生的,正常情况下,客户端发出连接请求,但是因为连接报文请求丢失而未收到确认,于是客户端再重传一次连接请求,后来收到了请求,并收到了确认,建立了连接,数据传输完毕后,就释放链接,客户端共发送了两次连接请求报文段,其中第一个丢失,第二个到达了服务器,没有“已失效的连接请求报文段”,但是还有异常情况下,客户端发送的请求报文连接段并没有丢失,而是在某个网络节点滞留较长时间,以致延误到请求释放后的某个时间到达服务器,本来是一个早已失效的报文段,但是服务器收到了此失效连接请求报文段后,就误以为客户端又重新发送的连接请求报文段,并发送确认报文段给客户端,同意建立连接,如果没有三次握手,那么服务器发送确认后,连接就建立了,而此时客户端没有发送建立连接的请求报文段,于是不理会服务器的确认,也不会给服务器发送数据,而服务器却一直等待客户端发送数据,因此服务器的许多资源就浪费了,采用三次握手的方式就可以防止这种事情发生。
四次挥手:第一次挥手:客户端的数据到达尾部,像服务器端发送断开请求(FIN位置为1)。第二次挥手:服务器端收到断开请求后,发送一个ACK信号确认收到请求。第三次挥手:服务器端接收数据完成后,向客户端再发送断开请求(FIN位置1)。第四次挥手:当客户端收到服务端断开的请求后,会发送ACK信号,表示已经收到。并把自己设置成TIME_WAIT状态,并启动计时器。如果服务端没有收到ACK, 服务端的TCP的定时器到达后,会要求客户端重新发送ACK, 服务端收到ACK就断开连接,当客户端等待2MSL(2倍报文最大生存时间)后,没有收到服务端的重传请求后,就知道服务端已经接收到ACK,此时关闭自己的连接。
13.TCP报文
例如一个 100kb 的 HTML 文档需要传送到另外一台计算机,并不会整个文档直接传送过去,可能会切割成几个部分,比如四个分别为 25kb 的数据段。
而每个数据段再加上一个 TCP 首部,就组成了 TCP 报文。 一共四个 TCP 报文,发送到另外一个端。 另外一端收到数据包,然后再剔除 TCP 首部,组装起来。 等到四个数据包都收到了,就能还原出来一个完整的 HTML 文档了。
TCP 报文 (Segment),包括首部和数据部分。
而 TCP 的全部功能都体现在它首部中各字段的作用,只有弄清 TCP 首部各字段的作用才能掌握 TCP 的工作原理。
TCP 报文段首部的前20个字节是固定的,后面有 4N 字节是根据需要而增加的。
(1)源端口和目的端口 Port
各占 2 个 字节,共 4 个字节。 用来告知主机该报文段是来自哪里以及传送给哪个应用程序(应用程序绑定了端口)的。
进行 TCP 通讯时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知名服务端口号。
(2)序号 Sequence Number
占 4 个字节。 TCP 是面向字节流的,在一个 TCP 连接中传输的字节流中的每个字节都按照顺序编号。
例如 100 kb 的 HTML 文档数据,一共 102400 (100 * 1024) 个字节,那么每一个字节就都有了编号,整个文档的编号的范围是 0 ~ 102399。
序号字段值指的是
本报文段所发送的数据的第一个字节的序号。 那么 100 的 HTML 文档分割成四个等分之后, 第一个 TCP 报文段包含的是第一个 25kb 的数据,0 ~ 25599 字节, 该报文的序号的值就是:0 第二个 TCP 报文段包含的是第二个 25kb 的数据,25600 ~ 51199 字节,该报文的序号的值就是:25600
......
根据 8 位 = 1 字节,那么 4 个字节可以表示的数值范围:[0, 2^32],一共 2^32 (4294967296) 个序号。 序号增加到最大值的时候,下一个序号又回到了 0. 也就是说 TCP 协议可对 4GB 的数据进行编号,在一般情况下可保证当序号重复使用时,旧序号的数据早已经通过网络到达终点或者丢失了。
(3)标志位 TCP Flags
标志位,一共有 6 个,分别占 1 位,共 6 位 。 每一位的值只有 0 和 1,分别表达不同意思。
紧急 URG (Urgent)
当 URG = 1 的时候,表示紧急指针(Urgent Pointer)有效。 它告诉系统此报文段中有紧急数据,应尽快传送,而不要按原来的排队顺序来传送。 URG 要与首部中的 紧急指针 字段配合使用。
确认 ACK (Acknowlegemt)
当 ACK = 1 的时候,确认号(Acknowledgemt Number)有效。 一般称携带 ACK 标志的 TCP 报文段为「确认报文段」。 TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 设置为 1。
推送 PSH (Push)
当 PSH = 1 的时候,表示该报文段高优先级,接收方 TCP 应该尽快推送给接收应用程序,而不用等到整个 TCP 缓存都填满了后再交付。
复位 RST (Reset)
当 RST = 1 的时候,表示 TCP 连接中出现严重错误,需要释放并重新建立连接。 一般称携带 RST 标志的 TCP 报文段为「复位报文段」。
同步 SYN (SYNchronization)
当 SYN = 1 的时候,表明这是一个请求连接报文段。 一般称携带 SYN 标志的 TCP 报文段为「同步报文段」。 在 TCP 三次握手中的第一个报文就是同步报文段,在连接建立时用来同步序号。 对方若同意建立连接,则应在响应的报文段中使 SYN = 1 和 ACK = 1。
终止 FIN (Finis)
当 FIN = 1 时,表示此报文段的发送方的数据已经发送完毕,并要求释放 TCP 连接。 一般称携带 FIN 的报文段为「结束报文段」。 在 TCP 四次挥手释放连接的时候,就会用到该标志。
TCP报文和IP报文之间的关系:
IP报文的格式:
14.TCP与UDP的区别
TCP 是可靠的但传输速度慢 ,UDP 是不可靠的但传输速度快。因此在选用具体协议通信时,应该根据通信数据的要求而决定。 若通信数据完整性需让位与通信实时性,则应该选用 TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)。
UDP协议
UDP 是一种面向无连接,且不可靠的协议,在通信过程中,它并不像 TCP 那样需要先建立一个连接,只要(目的地址,端口号,源地址,端口号)确定了,就可以直接发送信息报文,并且不需要确保服务端一定能收到或收到完整的数据。它仅仅提供了校验和机制来保障一个报文是否完整,若校验失败,则直接丢弃报文,不做任何处理。
15.进程间的通信
1.信号 : 内核实现,及时传递状态信号
2.管道: 在同步机制下实现数据的传递
3.信号量:用于进程间同步控制
4.消息队列:可选择地接收含有不同类型的数据结构。
5.共享存储(共享内存): “共用内存”,快速传递数据
6.套接字(socket):用于网络间通讯
16.myuvc框架(很重要)
17.网络协议的层次
层次
功能
协议
设备
应用层
操作系统或网络应用程序提供访问网络服务的接口
Telnet、
FTP、HTTP、SNMP
表示层
定义数据格式及加密数据,包括数据的加密、压缩、格式转换
会话层
会话层管理主机之间的会话进程,即负责建立、管理、终止进程之间的会话
传输层
传输层负责将上层数据分段并提供端到端的、可靠的或不可靠的传输。此外,传输层还要处理端到端的差错控制和流量控制问题
TCP、UDP、SPX
网络层
网络层负责对子网间的数据包进行路由选择。网络层还可以实现拥塞控制、网际互连等功能
IP、IPX、RIP、OSPF
路由器
数据链路层
以数据帧的形式进行传输,在不可靠的物理介质上提供可靠的传输,提供物理地址寻址、数据的成帧、流量控制、数据检错、重发等功能。
ARP、RARP、SDLC、
HDLC、PPP、STP
交换机、网桥
物理层
规定了激活、维持、关闭通信端点之间的机械特性、电气特性、功能特性以及过程特性
中继器、网卡、集线器
18.位置无关码,位置相关码的概念
位置无关代码:即该段代码无论放在内存的哪个地址,都能正确运行。究其原因,是因为代码里没有使用绝对地址,都是相对地址。
位置相关码:即它的地址与代码处于的位置相关,是绝对地址,如:mov PC ,#0xff;ldr pc,=0xffff等。
为什么需要
位置无关码?
通常情况下,将bootloader程序下载到ROM的0x0地址进行启动(比如固化到NorFlash中)。然而在很多的设计中,比如将bootloader固化在NAND中,在系统复位后S3C2440A中NAND控制器自动读取NAND中存储的前4K的代码到s3c2440a中称之为steppingstone(垫脚石)的SRAM中,steppingstone中的代码用进行一些非核心的硬件初始化,再将NAND中剩下的bootloader代码拷贝到SDRAM中运行。一般境况下两者的地址并不相同,程序在SDRAM中的地址重定位过程必须由程序员来完成。这样就有了位置无关代码的概念,指代码不在连接时制定的运行地址空间,也可以执行,它一段加载到任意地址空间都能执行的特殊代码。这样在steppingstone设计的代码要用位置无关设计。一些裸板程序中也可是需要。如果你的这段代码需要实现位置无关,那么你就不能使用绝对寻址指令,否则的话就是位置有关了。
位置无关码的
使用场合
1. 程序在运行期间动态加载到内存;
2. 程序在不同场合与不同程序组合后加载到内存(共享的动态链接库);
3. 在运行期间不同地址相互之间的映射(如bootloader)
19.static变量的作用
在C语言中,关键字static的意思是静态的,有3个明显的作用:
1. 在函数体内,静态变量具有记忆作用,即一个被声明为静态的变量在这一函数被调用的过程中其值维持不变。
2. 在模块内(但在函数体外),它的作用域范围是有限制的,如果一个变量被声明为静态的,那么该变量可以被模块内所有的函数访问,但不能被模块外的其他函数访问。
3. 内部函数应该在当前源文件中说明和定义,对于可在当前源文件以外使用的函数,应该在一个头文件中说明,使用这些函数的源文件要包含这个头文件。
static全局变量和普通全局变量的区别:static全局变量只初始化一次,这是为了防止它在其他文件单元中引用。
static局部变量和普通局部变量的区别:static局部变量只初始化一次,下次的运算依据是上一次的结果值。
static函数与普通函数的区别在与作用域不一样,static()函数只在一个源文件中有效,不能被其它源文件使用。
#include"stdio.h"
int sum(int a)
{
auto int c = 0;
static int b =3;
c += 1;
b +=2;
return (a+b+c);
}
int main()
{
int i,a=2;
for(i=0;i<5;i++)
{
printf("%d ",sum(a)); //输出结果为 8 10 12 14 16
}
return 0;
}
20.const的作用
const变量:
关键的一点是:
const关键字的目的是说明变量不能被修改或更新,所以定义时必须初始化
1、全局const变量
局的只读变量(const)被放在代码段,也可以说是只读数据段
2、函数中的const变量
同样,定义时就要初始化,只不过作用域是当前函数。
3、类的const成员
不能在类内初始化,只能通过构造函数初始化列表初始化。
const指针
指向常量指针(point to const) 是说明指针的内容是常量,不能通过指针修改其所指对象的值。
const和static的区别
const的局部变量在超出其作用域后其空间会被释放,static放在静态存储区 。
21.R13、R14、R15寄存器的理解
1、堆栈指针r13(SP):每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),都有各自独立的堆栈,用不同的堆栈指针来索引。这样当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。
2、连接寄存器r14(LR):每种模式下r14都有自身版组,它有两个特殊功能。
(1)保存子程序返回地址。使用BL或BLX时,跳转指令自动把返回地址放入r14中;子程序通过把r14复制到PC来实现返回,通常用下列指令之一:
MOV PC, LR
BX LR
通常子程序这样写,保证了子程序中还可以调用子程序。
stmfd sp!, {lr}
……
ldmfd sp!, {pc}
(2)当异常发生时,异常模式的r14用来保存异常返回地址,将r14如栈可以处理嵌套中断。
3、程序计数器r15(PC):PC是有读写限制的。当没有超过读取限制的时候,读取的值是指令的地址加上8个字节,由于ARM指令总是以字对齐的,故bit[1:0]总是00。当用str或stm存储PC的时候,偏移量有可能是8或12等其它值。在V3及以下版本中,写入bit[1:0]的值将被忽略,而在V4及以上版本写入r15的bit[1:0]必须为00,否则后果不可预测。