0%

检查协议栈buffer

检查buffer大小,修改之后重启网络服务:

sysctl -a |grep net.core
net.core.rmem_default = 212992
net.core.rmem_max = 212992

检查网卡收包情况:

2015 git:(master)✗ > netstat -su
IcmpMsg:
    InType3: 366
    OutType3: 325
Udp:
    88110 packets received
    325 packets to unknown port received.
    0 packet receive errors
    6219 packets sent
    IgnoredMulti: 2208
UdpLite:
IpExt:
    InMcastPkts: 1945
    OutMcastPkts: 35
    InBcastPkts: 42730
    OutBcastPkts: 261
    InOctets: 192296247659
    OutOctets: 192200794856
    InMcastOctets: 220327
    OutMcastOctets: 5370
    InBcastOctets: 4953400
    OutBcastOctets: 30722
    InNoECTPkts: 6131438
    InECT0Pkts: 127

检查socket收发缓冲区

TCP 的性能取决于几个方面的因素。两个最重要的因素是:

  1. 链接带宽(link bandwidth)(报文在网络上传输的速率)
  2. 往返时间(round-trip time) 或 RTT(发送报文与接收到另一端的响应之间的延时)

这两个值确定了称为 Bandwidth Delay Product(BDP)的内容
给定链接带宽和 RTT 之后,您就可以计算出 BDP 的值了,
BDP 给出了一种简单的方法来计算理论上最优的 TCP socket 缓冲区大小(其中保存了排队等待传输和等待应用程序接收的数据)。

如果缓冲区太小,那么 TCP 窗口就不能完全打开,这会对性能造成限制。
如果缓冲区太大,那么宝贵的内存资源就会造成浪费。
如果您设置的缓冲区大小正好合适,那么就可以完全利用可用的带宽。下面我们来看一个例子:

如果应用程序是通过一个 100Mbps 的局域网进行通信,其 RRT 为 50 ms,那么 BDP 就是:

BDP = link_bandwidth * RTT
100MBps * 0.050 sec / 8 = 0.625MB = 625KB

注意:此处除以 8 是将位转换成通信使用的字节。

因此,我们可以将 TCP 窗口设置为 BDP 或 1.25MB。
但是在 Linux 2.6 上默认的 TCP 窗口大小是 110KB,这会将连接的带宽限制为 2.2MBps,计算方法如下:

throughput = window_size / RTT
110KB / 0.050 = 2.2MBps

如果使用上面计算的窗口大小,我们得到的带宽就是 12.5MBps,计算方法如下:

625KB / 0.050 = 12.5MBps

差别的确很大,并且可以为 socket 提供更大的吞吐量。
Sockets API 提供了几个 socket 选项,其中两个可以用于修改 socket 的发送和接收缓冲区的大小。
使用 SO_SNDBUF 和 SO_RCVBUF 选项来调整发送和接收缓冲区的大小
注意:尽管 socket 缓冲区的大小确定了通告 TCP 窗口的大小,但是 TCP 还在通告窗口内维护了一个拥塞窗口。
因此,由于这个拥塞窗口的存在,给定的 socket 可能永远都不会利用最大的通告窗口。

修改默认socket收发缓冲区大小:

int ret, sock, sock_buf_size;
sock = socket( AF_INET, SOCK_STREAM, 0  );
sock_buf_size = 64*1024;
ret = setsockopt( sock, SOL_SOCKET, SO_SNDBUF,
        (char *)&sock_buf_size, sizeof(sock_buf_size) );
ret = setsockopt( sock, SOL_SOCKET, SO_RCVBUF,
        (char *)&sock_buf_size, sizeof(sock_buf_size) );

禁用 Nagle 算法

在通过 TCP socket 进行通信时,数据都拆分成了数据块,
这样它们就可以封装到给定连接的 TCP payload(指 TCP 数据包中的有效负荷)中了。
TCP payload 的大小取决于几个因素(例如最大报文长度和路径),
但是这些因素在连接发起时都是已知的。
为了达到最好的性能,我们的目标是使用尽可能多的可用数据来填充每个报文。
当没有足够的数据来填充 payload 时(也称为最大报文段长度(maximum segment size) 或 MSS),
TCP 就会采用 Nagle 算法自动将一些小的缓冲区连接到一个报文段中。

这样可以通过最小化所发送的报文的数量来提高应用程序的效率,并减轻整体的网络拥塞问题。

尽管 John Nagle 的算法可以通过将这些数据连接成更大的报文来最小化所发送的报文的数量,
但是有时您可能希望只发送一些较小的报文。
一个简单的例子是 telnet 程序,它让用户可以与远程系统进行交互,这通常都是通过一个 shell 来进行的。
如果用户被要求用发送报文之前输入的字符来填充某个报文段,那么这种方法就绝对不能满足我们的需要。
另外一个例子是 HTTP 协议。通常,客户机浏览器会产生一个小请求(一条 HTTP 请求消息),
然后 Web 服务器就会返回一个更大的响应(Web 页面)。

您应该考虑的第一件事情是 Nagle 算法满足一种需求。
由于这种算法对数据进行合并,试图构成一个完整的 TCP 报文段,因此它会引入一些延时。
但是这种算法可以最小化在线路上发送的报文的数量,因此可以最小化网络拥塞的问题。
但是在需要最小化传输延时的情况中,Sockets API 可以提供一种解决方案。
禁用 Nagle 算法,您可以设置 TCP_NODELAY socket 选项。

为TCP socket 禁用 Nagle 算法

int sock, flag, ret;
/* Create new stream socket */
sock = socket( AF_INET, SOCK_STREAM, 0  );
/* Disable the Nagle (TCP No Delay) algorithm */
flag = 1;
ret = setsockopt( sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag)  );
if (ret == -1) {
printf("Couldn't setsockopt(TCP_NODELAY)\n");
    exit(-1);
}

使用 Samba 的实验表明,在从 Microsoft® Windows® 服务器上的 Samba 驱动器上读取数据时,禁用 Nagle 算法几乎可以加倍提高读性能。

tcp

我们知道TCP链接是有很多开销的,一个是会占用文件描述符,另一个是会开缓存
一般来说一个系统可以支持的TCP链接数是有限的,我们需要清楚地认识到TCP链接对系统的开销是很大的
正是因为TCP是耗资源的,所以,很多攻击都是让你系统上出现大量的TCP链接,把你的系统资源耗尽。比如著名的SYNC Flood攻击。

所以,我们要注意配置KeepAlive参数,这个参数的意思是定义一个时间,
如果链接上没有数据传输,系统会在这个时间发一个包,如果没有收到回应,
那么TCP就认为链接断了,然后就会把链接关闭,这样可以回收系统资源开销。
(注:HTTP层上也有KeepAlive参数)对于像HTTP这样的短链接,设置一个1-2分钟的keepalive非常重要。
这可以在一定程度上防止DoS攻击。有下面几个参数(下面这些参数的值仅供参考):

net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 20
net.ipv4.tcp_fin_timeout = 30

对于TCP的TIME_WAIT这个状态,主动关闭的一方进入TIME_WAIT状态,
TIME_WAIT状态将持续2个MSL(Max Segment Lifetime),默认为4分钟,TIME_WAIT状态下的资源不能回收。
有大量的TIME_WAIT链接的情况一般是在HTTP服务器上。对此,有两个参数需要注意,

net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_tw_recycle=1

前者表示重用TIME_WAIT,后者表示回收TIME_WAIT的资源。

TCP还有一个重要的概念叫RWIN(TCP Receive Window Size),
这个东西的意思是,我一个TCP链接在没有向Sender发出ack时可以接收到的最大的数据包。
为什么这个很重要?因为如果Sender没有收到Receiver发过来ack,
Sender就会停止发送数据并会等一段时间,如果超时,那么就会重传。
这就是为什么TCP链接是可靠链接的原因。
重传还不是最严重的,如果有丢包发生的话,TCP的带宽使用率会马上受到影响(会盲目减半),
再丢包,再减半,然后如果不丢包了,就逐步恢复。
相关参数如下:

net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

一般来说,理论上的RWIN应该设置成:吞吐量 * 回路时间
Sender端的buffer应该和RWIN有一样的大小
因为Sender端发送完数据后要等Receiver端确认,如果网络延时很大,
buffer过小了,确认的次数就会多,于是性能就不高,对网络的利用率也就不高了。
也就是说,对于延迟大的网络,我们需要大的buffer,这样可以少一点ack,多一些数据,对于响应快一点的网络,可以少一些buffer
因为,如果有丢包(没有收到ack),buffer过大可能会有问题,因为这会让TCP重传所有的数据,反而影响网络性能。
(当然,网络差的情况下,就别玩什么高性能了) 所以,
高性能的网络重要的是要让网络丢包率非常非常地小(基本上是用在LAN里),
如果网络基本是可信的,这样用大一点的buffer会有更好的网络传输性能(来来回回太多太影响性能了)。

udp

说到UDP的调优,有一些事我想重点说一样,那就是MTU——最大传输单元(其实这对TCP也一样,因为这是链路层上的东西)
对于一个UDP的包,我们要尽量地让他大到MTU的最大尺寸再往网络上传,这样可以最大化带宽利用率。
对于这个MTU,以太网是1500字节,光纤是4352字节,802.11无线网是7981。
但是,当我们用TCP/UDP发包的时候,
我们的有效负载Payload要低于这个值,因为IP协议会加上20个字节,UDP会加上8个字节(TCP加的更多)
所以,一般来说,你的一个UDP包的最大应该是1500-8-20=1472,这是你的数据的大小。
当然,如果你用光纤的话, 这个值就可以更大一些。

再多说一下,使用Socket编程的时候,你可以使用setsockopt() 设置 SO_SNDBUF/SO_RCVBUF 的大小,TTL和KeepAlive这些关键的设置

多路复用

关于多路复用技术,也就是用一个线程来管理所有的TCP链接,有三个系统调用要重点注意:

  1. 一个是select,这个系统调用只支持上限1024个链接
  2. 第二个是poll,其可以突破1024的限制,但是select和poll本质上是使用的轮询机制,
    轮询机制在链接多的时候性能很差,因主是O(n)的算法
  3. 所以,epoll出现了,epoll是操作系统内核支持的,仅当在链接活跃时,操作系统才会callback,
    这是由操作系统通知触发的,但其只有Linux Kernel 2.6以后才支持(准确说是2.5.44中引入的),
    当然,如果所有的链接都是活跃的,过多的使用epoll_ctl可能会比轮询的方式还影响性能,不过影响的不大。

另外,关于一些和DNS Lookup的系统调用要小心,比如:gethostbyaddr/gethostbyname,
这个函数可能会相当的费时,因为其要到网络上去找域名,因为DNS的递归查询,会导致严重超时,
而又不能通过设置什么参数来设置time out

对此你可以通过配置hosts文件来加快速度,或是自己在内存中管理对应表,在程序启动时查好,
而不要在运行时每次都查。

另外,在多线程下面,gethostbyname会一个更严重的问题,
就是如果有一个线程的gethostbyname发生阻塞,
其它线程都会在gethostbyname处发生阻塞
,这个比较变态,要小心。
(你可以试试GNU的gethostbyname_r(),这个的性能要好一些) 这种到网上找信息的东西很多,
比如,如果你的Linux使用了NIS,或是NFS,某些用户或文件相关的系统调用就很慢,所以要小心。

检查防火墙

iptables -L

如果iptables工作,需要关闭:

service iptables stop

Linux内核参数优化

  1. /proc/sys/net/core/rmem_max — 最大的TCP数据接收缓冲。
  2. /proc/sys/net/core/wmem_max — 最大的TCP数据发送缓冲。
  3. /proc/sys/net/ipv4/tcp_timestamps — 时间戳在(请参考RFC 1323)TCP的包头增加12个字节。
  4. /proc/sys/net/ipv4/tcp_sack — 有选择的应答。
  5. /proc/sys/net/ipv4/tcp_window_scaling — 支持更大的TCP窗口. 如果TCP窗口最大超过65535(64KB), 必须设置该数值为1。
  6. rmem_default — 默认的接收窗口大小。
  7. rmem_max — 接收窗口的最大大小。
  8. wmem_default — 默认的发送窗口大小。
  9. wmem_max — 发送窗口的最大大小。

/proc目录下的所有内容都是临时性的, 所以重启动系统后任何修改都会丢失。
建议在系统启动时自动修改TCP/IP参数,两种方式:

  1. 修改/etc/rc.local
  2. 修改/etc/sysctl.conf

GUN/Linux网络工具

GNU/Linux 提供了几个工具 —— 有些是 GNU/Linux 自己提供的,
有些是开放源码软件 —— 用于调试网络应用程序,测量带宽/吞吐量,以及检查链接的使用情况。

任何 GNU/Linux 发行版中都可以找到的工具:

  1. ping这是用于检查主机的可用性的最常用的工具,但是也可以用于识别带宽延时产品计算的 RTT。
  2. traceroute打印某个连接到网络主机所经过的包括一系列路由器和网关的路径(路由),从而确定每个 hop 之间的延时。
  3. netstat确定有关网络子系统、协议和连接的各种统计信息。
  4. tcpdump显示一个或多个连接的协议级的报文跟踪信息;其中还包括时间信息,您可以使用这些信息来研究不同协议服务的报文时间。

GNU/Linux 发行版中没有提供的有用性能工具:

  1. netlog为应用程序提供一些有关网络性能方面的信息。
  2. nettimer为瓶颈链接带宽生成一个度量标准;可以用于协议的自动优化。
  3. Ethereal以一个易于使用的图形化界面提供了 tcpump(报文跟踪)的特性。
  4. iperf测量 TCP 和 UDP 的网络性能;测量最大带宽,并汇报延时和数据报的丢失情况。

Ref

  1. 提高 Linux 上 socket 性能
  2. 浅谈linux性能调优之十四:调节socket缓冲区
  3. 性能调优攻略

tips

net/ipv4/af_inet.c 查看:inet_stream_ops 和 inet_dgram_ops

TCP和UDP

TCP(传输控制协议)和UDP(用户数据报协议)是网络体系结构TCP/IP模型中传输层一层中的两个不同的通信协议。

  1. TCP:传输控制协议,一种面向连接的协议,给用户进程提供可靠的全双工的字节流,TCP套接口是字节流套接口(stream socket)的一种。
  2. UDP:用户数据报协议。UDP是一种无连接协议。UDP套接口是数据报套接口(datagram socket)的一种。

TCP Client-Server框架

TCP

服务器程序流程:

  1. 程序初始化
  2. 填写本机地址信息
  3. 绑定并监听一个固定的端口
  4. 收到Client的连接后建立一个socket连接
  5. 产生一个新的进程与Client进行通信和信息处理
  6. 子通信结束后中断与Client的连接

客户端程序流程:

  1. 程序初始化
  2. 填写服务器地址信息
  3. 连接服务器
  4. 与服务器通信和信息处理
  5. 通信结束后断开连接

UDP Client-Server框架

UDP

服务器程序流程:

  1. 程序初始化

  2. 填写本机地址信息

  3. 绑定一个固定的端口

  4. 收到Client的数据报后进行处理与通信

  5. 通信结束后断开连接

    客户端程序流程:

  6. 程序初始化

  7. 填写服务器地址信息

  8. 与服务器通信和信息处理

  9. 通信结束后断开连接

Socket编程

Socket接口是TCP/IP网络的API,
网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)数据报式Socket(SOCK_DGRAM)

  1. 流式是一种面向连接的Socket,针对于面向连接的TCP服务应用
  2. 数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用

创建套接字

int socket(int domain, int type, int protocol);

建立地址和套接字的联系

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

服务器端侦听客户端的请求

int listen(int sockfd, int backlog);

建立服务器/客户端的连接 (面向连接TCP)

//客户端请求连接 
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//服务器端从编号为Sockid的Socket上接收客户连接请求 
newsockid=accept(Sockid,Clientaddr, paddrlen)

发送/接收数据

//面向连接:
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);

释放套接字

int close(int fd);

区别

  1. socket()的参数不同
  2. UDP Server不需要调用listen和accept
  3. UDP收发数据用sendto/recvfrom函数
  4. TCP:地址信息在connect/accept时确定
  5. UDP:在sendto/recvfrom函数中每次均需指定地址信息(客户端调用connect之后,不需要每次指定)
  6. UDP:shutdown函数无效

socket插入内核hash表

根据服务器和客户端的行为不同,bind()和sendto()都会调用到get_port(),
也就是说,在bind()或sendto()调用时,socket才被插入到内核表中。

bind() 绑定地址

sys_bind -> sock->ops->bind -> inet_bind -> sk->sk_prot->get_port

sk->sk_prot是udp_prot,这里实际调用udp_v4_get_port()函数。

sendto() 发送到指定地址

sys_sendto -> sock_sendmsg -> __sock_sendmsg -> sock->ops->sendmsg

由于创建的是udp socket,因此sock->ops指向inet_dgram_ops,sendmsg()实际调用inet_sendmsg()函数。该函数中的有如下语句:

if (!inet_sk(sk)->inet_num && inet_autobind(sk))  
    return -EAGAIN;  

客户端在执行sendto()前仅仅执行了socket()操作,
此时inet_num=0,因此执行了inet_autobind(),
该函数会调用sk->sk_prot->get_port()。
从而回到了udp_v4_get_port()函数,它会将sk插入到内核表udp_table中。

UDP调用connect

标准的udp客户端开了套接口后,一般使用sendto和recvfrom函数来发数据,
udp发送数据有两种方法供大家选用的:

  1. socket–>sendto()或recvfrom()
  2. socket–>connect()–>send()或recv()

sendto和recvfrom在收发时指定地址,而send和recv则没有,地址是在connect指定的.

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

在udp编程中,如果你只往一个地址发送,那么你可以使用send和recv,
在使用它们之前用connect指定目的地址。connect函数在udp中就是这个作用,用它来检测udp端口的是否开放是没有用的。

udp中也有connect,只是它的connect不会进行三步握手,udp中调用connect时什么包也不发送。
调用connect是可选的,调用connect后就可以使用send、recv来进行UDP的收发包,而不必每次都要指定地址,
然后使用sendto、recvfrom进行操作,当然也可以调用sendto recvfrom。
没有调用connect那只能调用sendto、recvfrom,不可以调用send、recv。
调用sendto的时候第五个参数必须是NULL,第六个参数是0.
调用recvfrom,recv,read系统调用只能获取到先前connect的ip&port发送的报文.

UDP中使用connect可以提高效率.原因如下:

  1. 普通的UDP发送两个报文内核做了如下:#1:建立连结#2:发送报文#3:断开连结#4:建立连结#5:发送报文#6:断开连结
  2. 采用connect方式的UDP发送两个报文内核如下处理:#1:建立连结#2:发送报文#3:发送报文另外一点, 每次发送报文内核都由可能要做路由查询.

TCP中调用connect会引起三次握手,client与server建立连结.UDP中调用connect内核仅仅把对端ip&port记录下来.
UDP中可以多次调用connect,TCP只能调用一次connect. UDP多次调用connect有两种用途:

  1. 指定一个新的ip&port连结. 指定新连结,直接设置connect第二个参数即可.
  2. 断开和之前的ip&port的连结. 断开连结,需要将connect第二个参数中的sin_family设置成AF_UNSPEC即可.

UDP中使用connect的好处:

  1. 会提升效率
  2. 高并发服务中会增加系统稳定性

内核:发送时有两种调用方式:sys_send()和sys_sendto(),
两者的区别在于sys_sendto()需要给入目的地址的参数;
而sys_send()调用前需要调用sys_connect()来绑定目的地址信息;
两者的后续调用是相同的。如果调用sys_sendto()发送,
地址信息在sys_sendto()中从用户空间拷贝到内核空间,
而报文内容在udp_sendmsg()中从用户空间拷贝到内核空间。

TCP使用sendto

tcp_v4_connect函数代码片段:

/* 记录目的端口和目的IP */
inet->dport = usin->sin_port;
inet->daddr = daddr;

将目标地址及端口记录在inet中

同样的,TCP也可以调用sendto、recvfrom来完成数据的读写。Linux内核中send系统调用

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
    unsigned int, flags)
{
    return sys_sendto(fd, buff, len, flags, NULL, 0);
}

可以看到,send直接调用sendto,后面两个参数直接填NULL,0,
因此对于TCP调用来将,使用sendto,并将后两个参数置0,一样可以工作。

TCP/UDP Send流程代码分析

TCP调用sendto时没有给定地址,如何来完成TCP传输?sendto函数原型:

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
    unsigned, flags, struct sockaddr __user *, addr,
    int, addr_len)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err;
    struct msghdr msg;
    struct iovec iov;
    int fput_needed;

    if (len > INT_MAX)
        len = INT_MAX;
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;

    iov.iov_base = buff;
    iov.iov_len = len;
    msg.msg_name = NULL;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = NULL;
    msg.msg_controllen = 0;
    msg.msg_namelen = 0;

    if (addr) {
        err = move_addr_to_kernel(addr, addr_len, (struct sockaddr *)&address);
        if (err < 0)
            goto out_put;
        msg.msg_name = (struct sockaddr *)&address;
        msg.msg_namelen = addr_len;
    }

    if (sock->file->f_flags & O_NONBLOCK)
        flags |= MSG_DONTWAIT;
    msg.msg_flags = flags;
    err = sock_sendmsg(sock, &msg, len);

    out_put:
        fput_light(sock->file, fput_needed);
    out:
        return err;
}                          

tips: 使用sock->file->f_flags & O_NONBLOCK 来检查是否是nonblock的传送。

sendto中首先调用函数move_addr_to_kernel,将地址copy进内核空间:

int move_addr_to_kernel(void __user *uaddr, int ulen, struct sockaddr *kaddr)                                                      
{
    if (ulen < 0 || ulen > sizeof(struct sockaddr_storage))
        return -EINVAL;
    if (ulen == 0)
        return 0;
    if (copy_from_user(kaddr, uaddr, ulen))
        return -EFAULT;
    return audit_sockaddr(ulen, kaddr);
}

对于TCP来说,传入参数分别为NULL,0,这部分不会执行到。

msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;

地址存入msg中,用于UDP连接

然后调用sock_sendmsg来完成数据发送,调用顺序:

sys_send -> sys_sendto -> sock_sendmsg -> __sock_sendmsg -> sock->ops->sendmsg -> inet_sendmsg -> sk->sk_prot->sendmsg

static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock,                                                          
        struct msghdr *msg, size_t size)
{
    struct sock_iocb *si = kiocb_to_siocb(iocb);
    int err;

    si->sock = sock;
    si->scm = NULL;
    si->msg = msg;
    si->size = size;

    err = security_socket_sendmsg(sock, msg, size);
    if (err)
        return err;

    return sock->ops->sendmsg(iocb, sock, msg, size);
}

根据socket不同具体对应于tcp_sendmsg、udp_sendmsg

udp sendmsg

udp_sendmsg代码分析:

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len);

struct inet_sock *inet = inet_sk(sk);
if (msg->msg_name) {
    struct sockaddr_in * usin = (struct sockaddr_in*)msg->msg_name;
    if (msg->msg_namelen < sizeof(*usin))
        return -EINVAL;
    if (usin->sin_family != AF_INET) {
        if (usin->sin_family != AF_UNSPEC)
            return -EAFNOSUPPORT;
    }

    daddr = usin->sin_addr.s_addr;
    dport = usin->sin_port;
    if (dport == 0)
        return -EINVAL;
} else {
    if (sk->sk_state != TCP_ESTABLISHED)
        return -EDESTADDRREQ;
    daddr = inet->daddr;
    dport = inet->dport;
    /* Open fast path for connected socket.
       Route will not be used, if at least one option is set.
     */
    connected = 1;
}
ipc.addr = inet->saddr;

这段代码获取要发送数据的目的地址和端口号
一种情况是调用sendto()发送数据,此时目的的信息以参数传入,存储在msg->msg_name中,因此从中取出daddr和dport;
另一种情况是调用connect(), send()发送数据,在connect()调用时绑定了目的的信息,存储在inet中
并且由于是调用了connect(),sk->sk_state会设置为TCP_ESTABLISHED。
以后调用send()发送数据时,无需要再给入目的信息参数,因此从inet中取出dadr和dport。而connected表示了该socket是否已绑定目的。

tcp sendmsg

tcp_sendmsg代码分析,调用流程:

tcp_sendmsg -> tcp_push -> __tcp_push_pending_frames -> tcp_write_xmit -> tcp_transmit_skb

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
        gfp_t gfp_mask)
{
    struct inet_sock *inet;
    .
    .
    inet = inet_sk(sk);
    /* Build TCP header and checksum it. */
    th = tcp_hdr(skb);
    th->source      = inet->sport;
    th->dest        = inet->dport;
    th->seq         = htonl(tcb->seq);
    th->ack_seq     = htonl(tp->rcv_nxt);
    *(((__be16 *)th) + 6)   = htons(((tcp_header_size >> 2) << 12) |
            tcb->flags);
    .
    .
}

tcp_sendmsg目标地址在函数tcp_transmit_skb中获取,获取的值是调用connect时存储在inet中!

  1. udp调用connect有什么作用
  2. UDP 的Connect函数的例子
  3. Linux内核分析 - 网络:UDP模块 - 收发
  4. Linux内核分析 - 网络:UDP模块 - socket
  5. linux-Tcp IP协议栈源码阅读笔记
  6. Linux TCP/IP源码分析 Connect
  7. Linux TCP/IP 协议栈源码分析

格式输出控制符

printf的格式控制的完整格式:% - 0 m.n lh:

  1. %:格式说明的起始符号,不可缺少。
  2. -: 有-表示左对齐输出,如省略表示右对齐输出。
  3. 0:有0表示指定空位填0,如省略表示指定空位不填。
  4. m.n:m指域宽,即对应的输出项在输出设备上所占的字符数。n指精度,用于说明输出的实型数的小数位数。未指定n时,隐含的精度为n=6位。
    针对字符输出,%m.ns:输出占m列,但只取字符串中左端n个字符。
  5. l:l对整型指long型,对实型指double型。
  6. h:用于将整型的格式字符修正为short型。

格式输出数据类型控制

  1. d格式:用来输出十进制整数。%ld:输出长整型数据。%md:m为指定的输出字段的宽度。
  2. o格式:以无符号八进制形式输出整数。对长整型可以用”%lo”格式输出。同样也可以指定字段宽度用“%mo”格式输出。
  3. x格式:以无符号十六进制形式输出整数。对长整型可以用”%lx”格式输出。同样也可以指定字段宽度用”%mx”格式输出。
  4. u格式:以无符号十进制形式输出整数。对长整型可以用”%lu”格式输出。同样也可以指定字段宽度用“%mu”格式输出。
  5. c格式:输出一个字符。
  6. s格式:用来输出一个字符串
  7. f格式:用来输出实数(包括单、双精度),以小数形式输出。
  8. e格式:以指数形式输出实数。
  9. g格式:自动选f格式或e格式中较短的一种输出,且不输出无意义的零。

m.n格式另一种表述

对于m.n的格式还可以用如下方法表示:

printf(“%*.*s\n”,m,n,ch);

前边的*定义的是总的宽度,后边的定义的是输出的个数。
分别对应外面的参数m和n 。
这种方法的好处是可以在语句之外对参数m和n赋值,从而控制输出格式。

例如:函数接口传给你一个没有“\0”结尾的字符串str和他的长度str_len,调试的时候你需要将其打印出来。

printf("%.*s\n", str_len, str);

printf输出字符串长度

输出格式 %n 可以将所输出字符串的长度值赋绐一个变量:

int slen;
printf(“hello world%n”, &slen);

执行后变量slen被赋值为11。

输出颜色

printf("\033[31m test print color \033[0m\n");

示例

printf("%3s, \n%7.2s, \n%.4s, \n%-5.3s\n", "PRINTF", "PRINTF", "PRINTF", "PRINTF");
gcc main.c
./a.out
PRINTF, 
     PR, 
PRIN, 
PRI  

解析如下:

  1. %s原样输出字符串: %s
  2. %ns输出指定长度n的字符串, 超长时不截断, 不足时右对齐: %3s
  3. %m.ns输出指定长度的字符串, 超长时截断, 不足时右对齐,m为最终的字符串输出长度,n为从参数字符串中取出的子串长度: %7.2s
  4. %.ns输出指定长度的字符串, 超长时截断,不足时右对齐: %.4s; 如果使用 %.8s 与%s %3s效果一样
  5. %-m.ns输出指定长度的字符串, 超长时截断, 不足时左对齐: %-5.3s

通过上述用例,所谓超长时截断用到的n并不是只在超长时才起作用,而是不管你有没有超长,都必须截取这么长。
上述m,n是可以动态指定的,方法是用*代替m或者n,然后在参数列表里加上一个数字参数


常用的Python打包为exe工具有:

  1. py2exe
  2. pyinstaller
  3. cxfreeze

选择使用方便的pyinstaller作为打包工具。

安装

有两种方式:

  1. 官网下载后解压缩到需要的文件夹就可以执行,不需要安装。
  2. pip 方式安装升级: pip install pyinstallerpip install –upgrade pyinstaller

使用第二种方式安装pyinstaller,安装过程中遇到一些环境问题,主要是pip,google解决之。

python2.7 -m pip install -U setuptools
python3.4 -m pip install -U setuptools
python2.7 -m pip install pyinstaller
python3.4 -m pip install pyinstaller

执行pyinstaller可能遇到如下问题:

FileNotFoundError: Path or glob "/usr/include/python3.4m/pyconfig.h" not found or matches no files.

文章
解决:

sudo apt-get install libpython3.4-dev

使用

测试代码:

#!/usr/bin/env python3.4
# -*- coding: utf-8 -*-

from tkinter import *

root = Tk()
root.title('测试界面')
root.config(height=480,pady=10)

title = Label(root,text='测试控件',font='微软雅黑 -23 bold',fg='#fa8723')
title.pack()

root.mainloop()                         

执行打包命令:

✘ /data/OpenSourceCode/python/tkinter/test  > pyinstaller -F tk-1030.py
28 INFO: PyInstaller: 3.0
28 INFO: Python: 3.4.3
28 INFO: Platform: Linux-3.19.0-32-generic-x86_64-with-Ubuntu-15.04-vivid
28 INFO: wrote /data/OpenSourceCode/python/tkinter/test/tk-1030.spec
29 INFO: UPX is not available.
30 INFO: Extending PYTHONPATH with /data/OpenSourceCode/python/tkinter/test
31 INFO: checking Analysis
31 INFO: Building Analysis because out00-Analysis.toc is non existent
31 INFO: Initializing module dependency graph...
96 INFO: Initializing module graph hooks...
110 INFO: Analyzing base_library.zip ...
1864 INFO: Processing pre-find module path hook   distutils
3560 INFO: running Analysis out00-Analysis.toc
3647 INFO: Analyzing /data/OpenSourceCode/python/tkinter/test/tk-1030.py
3834 INFO: Looking for import hooks ...
3839 INFO: Processing hook   hook-sysconfig.py
3859 INFO: Processing hook   hook-pydoc.py
3860 INFO: Processing hook   hook-_tkinter.py
3934 INFO: checking Tree
3934 INFO: Building Tree because out00-Tree.toc is non existent
3934 INFO: Building Tree out00-Tree.toc
3954 INFO: checking Tree
3954 INFO: Building Tree because out01-Tree.toc is non existent
3954 INFO: Building Tree out01-Tree.toc
3963 INFO: Processing hook   hook-encodings.py
3983 INFO: Processing hook   hook-distutils.py
3984 INFO: Processing hook   hook-xml.py
4272 INFO: Processing hook   hook-xml.sax.py
4281 INFO: Looking for ctypes DLLs
4628 INFO: Analyzing run-time hooks ...
4631 INFO: Including run-time hook 'pyi_rth__tkinter.py'
4643 INFO: Looking for dynamic libraries
5008 INFO: Looking for eggs
5008 INFO: Python library not in binary depedencies. Doing additional searching...
5197 INFO: Using Python library /usr/lib/x86_64-linux-gnu/libpython3.4m.so.1.0
5204 INFO: Warnings written to /data/OpenSourceCode/python/tkinter/test/build/tk-1030/warntk-1030.txt
5218 INFO: checking PYZ
5218 INFO: Building PYZ because out00-PYZ.toc is non existent
5218 INFO: Building PYZ (ZlibArchive) /data/OpenSourceCode/python/tkinter/test/build/tk-1030/out00-PYZ.pyz
5430 INFO: checking PKG
5430 INFO: Building PKG because out00-PKG.toc is non existent
5430 INFO: Building PKG (CArchive) out00-PKG.pkg
10161 INFO: Bootloader /usr/local/lib/python3.4/dist-packages/PyInstaller/bootloader/Linux-64bit/run
10161 INFO: checking EXE
10161 INFO: Building EXE because out00-EXE.toc is non existent
10161 INFO: Building EXE from out00-EXE.toc
10167 INFO: Appending archive to EXE /data/OpenSourceCode/python/tkinter/test/dist/tk-1030

exe文件生成在dist文件夹中,执行命令验证正常:

✔ /data/OpenSourceCode/python/tkinter/test  > ./dist/tk-1030
  1. 关于python打包成exe的一点经验之谈
  2. PYINSTALLER 安装和使用
  3. py2exe Tutorial
  4. PyInstaller Manual
  5. PYInstaller Supported Packages
  6. cx_Freeze’s documentation

python提供了多个图形开发界面的库,几个常用Python GUI库如下:

  • Tkinter: Tkinter模块(“Tk 接口”)是Python的标准Tk GUI工具包的接口.
    Tk和Tkinter可以在大多数的Unix平台下使用,同样可以应用在Windows和Macintosh系统里.
    Tk8.0的后续版本可以实现本地窗口风格,并良好地运行在绝大多数平台中。
  • wxPython:wxPython 是一款开源软件,是 Python 语言的一套优秀的 GUI 图形库,
    允许 Python 程序员很方便的创建完整的、功能键全的 GUI 用户界面。
  • Jython:Jython程序可以和Java无缝集成。除了一些标准模块,Jython使用Java的模块。
    Jython几乎拥有标准的Python中不依赖于C语言的全部模块。
    比如,Jython的用户界面将使用Swing,AWT或者SWT。Jython可以被动态或静态地编译成Java字节码。
Read more »

ImportError: cannot import name IncompleteRead

pip -h
ImportError: cannot import name IncompleteRead

pip出现问题,执行修复:

sudo apt-get remove python-pip
sudo apt-get install python-pip

没有解决问题,尝试另一种:

sudo easy_install pip

解决此问题

ImportError: No module named ‘pip’

Pip则是一种更为高级的安装工具,它依赖于Setuptools

pip list
Traceback (most recent call last):
File "/usr/local/bin/pip", line 9, in <module>
load_entry_point('pip==1.4.1', 'console_scripts', 'pip')()
File "/usr/local/lib/python3.4/dist-packages/setuptools-1.1.5-py3.4.egg /pkg_resources.py", line 357, in load_entry_point
def get_entry_info(dist, group, name):
File "/usr/local/lib/python3.4/dist-packages/setuptools-1.1.5-py3.4.egg/pkg_resources.py", line 2394, in load_entry_point
break
File "/usr/local/lib/python3.4/dist-packages/setuptools-1.1.5-py3.4.egg/pkg_resources.py", line 2108, in load
name = some.module:some.attr [extra1,extra2]
ImportError: No module named 'pip'

$ which pip
/usr/local/bin/pip

$ python2.7 -m pip //here can be just python, btw
Usage:   
/usr/bin/python2.7 -m pip <command> [options]
//and so on...

$ python3.4 -m pip
/usr/bin/python3.4: No module named pip

需要安装python3.4下的pip

$ curl https://bootstrap.pypa.io/get-pip.py | python3.4
$ python{2.7,3.4} -m pip install -U setuptools

然后执行

pip list
pip install pyinstaller

均正确,解决此问题

python中使用PIL ImageTK出现以下问题:

Traceback (most recent call last):
    File "./ExportXiamiList.py", line 4, in <module>
        from tkinter import *
ImportError: No module named tkinter

Ubuntu下环境:

ExportXiamiList-master > python
python            python2.7         python2-config    python3.3         python3.4         python3m                          
python2           python2.7-config  python3           python3.3m        python3.4m        python-config            

安装如下包,不起作用:

sudo apt-get install python-pil python-pil.imagetk --reinstall

继续安装如下包解决问题:

sudo apt-get install python3-pil python3-pil.imagetk

python中ImageTK用法如下:

from PIL import Image
from PIL import ImageTk


标准UNIX命一般都提供大量的选项和参数,例如:

ps --help
Usage:
ps [options]

Try 'ps --help <simple|list|output|threads|misc|all>'
or 'ps --help <s|l|o|t|m|a>'
for additional help text.

在这些工具的命令行处理中使用了C函数getopt
shell脚本可以通过使用getopts来保持与UNIX一致的风格。

常用shell参数变量

  • $0 :即命令本身,相当于C/C++中的argv[0]
  • $1 :第一个参数.
  • $2, $3, $4 … :第2、3、4个参数,依次类推。
  • $# 参数的个数,不包括命令本身
  • $@ :参数本身的列表,也不包括命令本身
  • $* :和$@相同,但”$*“ 和 “$@”(加引号)并不同,”$*“将所有的参数解释成一个字符串,而”$@”是一个参数数组。

getopts

getopts有两个参数,
第一个参数是一个字符串,包括字符和“:”,每一个字符都是一个有效的选项,
如果字符后面带有“:”,表示这个字符有自己的参数。
如果字符串最前面带有“:”,表示开启静默模式,屏蔽系统提示错误。
getopts从命令中获取这些参数,并且删去了“-”,
并将其赋值在第二个参数中,如果带有自己参数,这个参数赋值在“OPTARG”中。

在使用getopts命令的时候,shell会自动产生两个变量OPTIND和OPTARG。
OPTIND初始值为1,其含义是下一个待处理的参数的索引。
只要存在,getopts命令返回true,所以一般getopts命令使用while循环;
OPTARG是当getopts获取到其期望的参数后存入的位置。

脚本

[ $# -eq 0   ] && usage

# option_string以冒号开头表示屏蔽脚本的系统提示错误,自己处理错误提示。
# 后面接合法的单字母选项,选项后若有冒号,则表示该选项必须接具体的参数

while getopts :s:t:i:oh OPTION
do
    case $OPTION in
        s)
            STRING=$OPTARG
            ;;

        t)
            STRING=`cat $OPTARG`
            ;;

        i)
            IN=$OPTARG        #$OPTARG为特殊变量,表示选项的具体参数
            ;;

        o)
            OUT=$OPTARG        #$OPTARG为特殊变量,表示选项的具体参数
            ;;

        h)
            echo "\033[31mHelp Info:\033[0m"
            java -jar sfntly-builds/java-openjdk-8/sfnttool/sfnttool.jar -h
            exit 1
        ;;

        \?)                       #如果出现错误,则解析为?
            usage
            ;;
    esac
done

#$OPTIND表示第几个选项,初始值为1
#shift $(($OPTIND - 1))
#if [ $# -eq 0  ]; then
#    usage
#fi

shift

使用shift $(($OPTIND-1))的作用是:
通过shift $(($OPTIND - 1))的处理,$*中就只保留了除去选项内容的参数,可以在其后进行正常的shell编程处理,例如:

./xx -i in -o out para1 para2 ...

经过处理之后,$*为 para1 para2,方便后续脚本处理

while getopts s:h OPTION
do
    case $OPTION in
        s)
            STRING=$OPTARG
            ;;

        h)
            echo "\033[31mHelp Info:\033[0m"
            exit 1
        ;;

        \?)                       #如果出现错误,则解析为?
            usage
            ;;
    esac
done

echo "OPTIND:"$OPTIND
shift $(($OPTIND - 1))      #除了选项之外,该脚本必须接至少一个参数

echo $0
echo $*
echo $@

if [ $# -eq 0  ]; then
    usage
fi

for file in $@          #依次处理剩余的参数
do
    echo $file
done

执行结果如下:

➜  /data/OpenSourceCode/subset-ttf  > ./test.sh -s "123" test1 test2 test3
OPTIND:3
./test.sh               $0
test1 test2 test3       $*
test1 test2 test3       $@
test1                   file
test2
test3


在shell中用于提取文件名或后缀,需要用到以下几个操作符: %、%%、#、##

% 和 %% 操作符

用法:

${VAR%.*}
${VAR%%.*}

${VAR%.*}含义:从$VAR删除位于 % 右侧的通配符左右匹配的字符串,通配符从右向左进行匹配

  • % 属于非贪婪操作符,从右向左匹配最短结果
  • %% 属于贪婪操作符,从右向左匹配符合条件的最长字符串

示例如下:

file="abc.c"
name=${file%.*}
echo file name is: $name

输出结果:
file name is: abc

变量 name 赋值abc.c,那么通配符从右向左就会匹配到 .c,所有从 $VAR 中删除匹配结果。

file="text.gif.bak.2012"
name=${file%.*}
name2=${file%%.*}
echo file name is: $name
echo file name is: $name2

输出结果:
file name is: text.gif.bak    //使用 %
file name is: text            //使用 %%

操作符 %% 使用 .* 从右向左贪婪匹配到 .gif.bak.2012

# 和 ## 操作符

用法:

${VAR#*.}
${VAR##*.}

${VAR#*.} 含义:从 $VAR删除位于 # 右侧的通配符所匹配的字符串,通配符是左向右进行匹配

  • # 属于非贪婪操作符,从左向右匹配最短结果
  • ## 属于贪婪操作符,从左向右匹配符合条件的最长字符串

符示例:

file="text.gif"
suffix=${file#*.}
echo suffix is: $suffix

输出结果:
suffix is: gif


file="text.gif.bak.2012.txt"
suffix=${file#*.}
suffix2=${file##*.}
echo suffix is: $suffix
echo suffix is: $suffix2

输出结果:
suffix is: text.gif.bak.2012     //使用 #
suffix2 is: txt                  //使用 ##

操作符 ## 使用 *. 从左向右贪婪匹配到 text.gif.bak.2012

示例

url="www.baidu.com"

echo ${url%.*}      #移除 .* 所匹配的最右边的内容。
www.baidu

echo ${url%%.*}     #将从右边开始一直匹配到最左边的 *. 移除,贪婪操作符。
www

echo ${url#*.}      #移除 *. 所有匹配的最左边的内容。
baidu.com

echo ${url##*.}     #将从左边开始一直匹配到最右边的 *. 移除,贪婪操作符。
com

Makefile中需要在编译前进行一些处理,shell可以方便完成。以下为在Makefile中使用shell需要注意的一些事项:

makefile和shell区别

变量引用

shell中所有引用以$打头的变量其后要加{},而在Makefile中的变量是以$打头的后加()。如下:

Makefile
PATH="/data/"
SUBPATH=$(PATH)

Shell
PATH="/data/"
SUBPATH=${PATH}

引用shell变量

Makefile中所有以$打头的单词都会被解释成Makefile中的变量。
如果需要调用shell中的变量(或者正则表达式中锚定句位$),都需要加两个$符号($$)。如下:

PATH="/data/"
all:
    echo $(PATH)
    echo $$PATH

第一个$(PATH)引用的是Makefile中的变量,而不是shell中的PATH环境变量,后者引用的是Shell中的PATH环境变量。

通配符区别

shell 中通配符*表示所有的字符;Makefile 中通配符%表示所有的字符

打印输出

在Makefile中只能在target中调用Shell脚本,其他地方是不能输出的。比如如下代码就是没有任何输出:

VAR="Hello"
echo "$VAR"
all:

以上代码任何时候都不会输出,没有在target内,如果上述代码改为如下:

VAR="Hello"
all:
    echo "$VAR"

以上代码,在make all的时候将会执行echo命令。

代码片段

Makefile中的shell,每一行是一个进程,不同行之间变量值不能传递。所以,Makefile中的shell不管多长也要写在一行
在Makefile中执行shell命令,一行创建一个进程来执行。
不同行之间变量值不能传递。
这也是为什么很多Makefile中有很多行的末尾都是“;\”,
以此来保证代码是一行而不是多行,这样Makefile可以在一个进程中执行,例如:

SUBDIR=src example
all:
    @for subdir in $(SUBDIR); \     #shell代码开始
    do\
        echo "building "; \
    done

上述可以看出for循环中每行都是以”; \”结尾的。

shell代码范围

在Makefile文件的目标项冒号后的另起一行的代码才是shell代码。

xx = xx1       #这里时makefile代码
yy:xx = xx2   #这是是makefile代码,makefile允许变量赋值时,'='号两边留空格
yy:
    xx=xx3     #只有这里是shell代码 ,shell不允许‘=’号两边有空格哦。

有一个例外:
xx=$(shell 这里的代码也是shell代码)

makefile总的反引号`

反引号括起来的字符串被shell解释为命令行,
在执行时,shell首先执行该命令行,并以它的标准输出结果取代整个反引号(包括两个反引号)部分

PATH=`pwd`
TODAY=`date`

#等同于
PATH=$(shell pwd)
TODAY=$(shell date)

实例

获取当前目录

TOPDIR:=$(shell pwd)

获取日期

DATE:=$(shell date +%Y%m%d)

获取源文件列表

SRC += $(shell find . -iname "*.c"  | grep -v "./opensource/*")
SRC += $(shell find . -iname "*.cpp")

PRE_SRC=$(shell find -name "*.c" | xargs grep "HANDLER" | awk -F ':' '{print $$1}' | sort -u) #注意awk中使用的是 $$1

Makefile打印输出

print_version:
    @echo $(BR2_VERSION_FULL)

Makefile嵌入脚本

env:                                                                                                                                
    sh $(SRC_PATH)/scripts/connect.sh
    find $(SRC_PATH) -name "sysinfo.o" | xargs rm -f

signalpre:                                                                                                                          
    sed -i 's/HANDLER/HANDLER HANDLERX/g' $(LIB_PATH)/include/core.h
    @mkdir -p $(SRC_PATH)/output/signalpres
    @echo "\033[031msignale handler processor...\033[0m"
    @for f in $(PRE_SRC); do \
        OBJ=$(SRC_PATH)/output/signalpres/`basename $$f|sed -e 's/\.c/\.i/'`; \
        $(CC) $(CFLAGS) -E $$f -o $$OBJ;\
        echo -n ".";\
    done
    sed -i 's/HANDLER HANDLERX/HANDLER/g' $(LIB_PATH)/include/core.h
    echo "\n"

prepare_config:
    @chmod +x scripts/proj.sh
    @scripts/proj.sh
    @chmod +x scripts/theme.sh
    @scripts/theme.sh

Makefile中的exec执行脚本

format:
    @echo "Makeing format...";
    @find -name "*.c" -exec dos2unix -qU 2>d2utmp1 {} \;
    @find -name "*.h" -exec dos2unix -qU 2>d2utmp1 {} \; 
#   @find -name "*.c" -exec indent -npro -kr -i8 -sob -l120 -ss -ncs  {} \;
    @find -name "*~" -exec rm {} \;
    @find -name "d2utmp*" -exec rm {} \;
    @find -name "deps*" -exec rm {} \;

循环处理文件行

cat config_tmp | while read line
do
    echo "${line}"

    MACRO=$(echo ${line} | awk -F ' ' '{print $2}')
    VALUE=$(echo ${line} | awk -F ' ' '{print $3}')
    echo "Macro: $MACRO Value: $VALUE\n"

    #cat "$SIGNALE_HANDLER_FILE" | sed -e "s/$MACRO/$VALUE/g" > signal_tmp
    sed -i "s/$MACRO/$VALUE/g" ${SIGNALE_HANDLER_FILE}
done

grep反选

cat $CONFIG_FILE | sed -s '/^$/d' | grep -v -e "LangName" -e "__CONFIG_H__" -e "#endif" -e "//" > config_tmp

Makefile依赖规则

OBJS=$(addprefix $(SRC_PATH)/objects/, $(addsuffix .o, $(basename $(notdir $(SRC)))))

deps: $(SRC)
    @-rm -f deps;
    @for f in $(SRC); do \
        OBJ=$(SRC_PATH)/objects/`basename $$f|sed -e 's/\.c/\.o/'`; \
        echo $$OBJ: $$f>> deps; \
        echo '  @echo -e "compiling \033[032m[$(CC)]\033[0m": ' $$f >> deps; \
        echo '  $(CC) $$(CFLAGS) -c -o $$@ $$^'>> deps; \
    done

Makefile模式规则

%.d: %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ : ]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

这个规则的意思是,所有的[.d]文件依赖于[.c]文件,“rm -f $@”的意思是删除所有的目标,也就是[.d]文件,
第二行的意思是,为每个依赖文件“$<”,也就是[.c]文件生成依赖文件,“$@”表示模式“%.d”文件,
如果有一个C文件是name.c,那么“%”就是 “name”,“$$$$”意为一个随机编号,
第二行生成的文件有可能是“name.d.12345”,
第三行使用sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档。
第四行就是删除临时文件。

变量值的替换

foo := a.o b.o c.o
bar := $(foo:.o=.c)

foo := a.o b.o c.o
bar := $(foo:%.o=%.c)

变量.o替换为.c

makefile函数列表

函数调用,很像变量的使用,也是以“$”来标识的,其語法如下:

$(<function> <arguments>)

或是

${<function> <arguments>}

这里,<function>就是函数名,make支持的函数不多。
<arguments>为函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。
函数调用以“$”开头,以圆括号或花括号把函数名和参数括起。
感觉很像一个变量,是不是?函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,
如使用“$(subst a,b,$(x))”这样的形式,而不是“$(subst a,b, ${x})”的形式。
因为统一会更清楚,也会减少一些不必要的麻烦。

字符串处理函数

  • $(subst <from>,<to>,<text>), 字符串替换函数
  • $(patsubst <pattern>,<replacement>,<text>), 模式字符串替换函数
  • $(strip <string>), 去空格函数,字串中开头和结尾的空字符
  • $(findstring <find>,<in>), 查找字符串函数
  • $(filter <pattern…>,<text>), 以<pattern>模式过滤<text>字符串中的单词,保留符合模式<pattern>的单词。可以有多个模式
  • $(filter-out <pattern…>,<text>), 以<pattern>模式过滤<text>字符串中的单词,去除符合模式<pattern>的单词。可以有多个模式
  • $(sort <list>), 给字符串<list>中的单词排序(升序)。
  • $(word <n>,<text>), 取字符串<text>中第<n>个单词。(从一开始)
  • $(wordlist <ss>,<e>,<text>), 从字符串<text>中取从<ss>开始到<e>的单词串。<ss>和<e>是一个数字
  • $(words <text>), 统计<text>中字符串中的单词个数
  • $(firstword <text>), 取字符串<text>中的第一个单词

搭配使用:

override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))

如果我们的“$(VPATH)”值是“src:../headers”,
那么“$(patsubst %,-I%,$(subst :, ,$(VPATH)))”将返回“-Isrc -I../headers”,这正是cc或gcc搜索头文件路径的参数

文件名操作函数

  • $(dir <names…>), 从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”

  • $(notdir <names…>), 从文件名序列<names>中取出非目录部分。非目录部分是指最後一个反斜杠(“/”)之后的部分

  • $(suffix <names…>) , 从文件名序列<names>中取出各个文件名的后缀

  • $(basename <names…>), 从文件名序列<names>中取出各个文件名的前缀部分

  • $(addsuffix <suffix>,<names…>), 把后缀<suffix>加到<names>中的每个单词后面

  • $(addprefix <prefix>,<names…>), 把前缀<prefix>加到<names>中的每个单词后面

  • $(join <list1>,<list2>), 把<list2>中的单词对应地加到<list1>的单词后面。
    如果<list1>的单词个数要比<list2>的多,那么,<list1>中的多出来的单词将保持原样。
    如果<list2>的单词个数要比<list1>多,那么,<list2>多出来的单词将被复制到<list1>中

    $(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”

foreach 函数

foreach函数和别的函数非常的不一样。
因为这个函数是用来做循环用的,
Makefile中的foreach函数几乎是仿照于Unix标准 Shell(/bin/sh)中的for语句,
或是C-Shell(/bin/csh)中的foreach语句而构建的。它的语法是:

$(foreach   <var>,<list>,<text>)

这个函数的意思是,把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,
然后再执行< text>所包含的表达式。
每一次<text>会返回一个字符串,循环过程中,<text>的所返回的每个字符串会以空格分隔,
最后当整个循环结束时,
<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

所以,<var>最好是一个变量名,<list>可以是一个表达式,而<text>中一般会使用<var>这个参数来依次枚举<list>中的单词。举个例子:

names := a b c d
files := $(foreach n,$(names),$(n).o)

上面的例子中,$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出一个值,
这些值以空格分隔,最后作为foreach函数的返回,所以,$(files)的值是“a.o b.o c.o d.o”。

注意,foreach中的<var>参数是一个临时的局部变量,foreach函数执行完后,参数<var>的变量将不在作用,其作用域只在foreach函数当中。

if 函数

if函数很像GNU的make所支持的条件语句——ifeq,if函数的语法是:

$(if <condition>,<then-part>) 

或是

$(if <condition>,<then-part>,<else-part>)

call函数

call函数是唯一一个可以用来创建新的参数化的函数。
你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。其语法是:

$(call <expression>;,<parm1>;,<parm2>;,<parm3>;...)

当make执行这个函数时,<expression>;参数中的变量,
如$(1),$(2),$(3)等,会被参数< parm1>;,<parm2>;,<parm3>;依次取代。
而<expression>;的返回值就是 call函数的返回值。例如:

reverse =  $(1) $(2)
foo = $(call reverse,a,b)

那么,foo的值就是“a b”。当然,参数的次序是可以自定义的,不一定是顺序的,如:

reverse =  $(2) $(1)
foo = $(call reverse,a,b)

此时的foo的值就是“b a”。

origin函数

origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的?其语法是:

$(origin <variable>;)

注意,<variable>;是变量的名字,不应该是引用。
所以你最好不要在<variable>;中使用“$”字符。
Origin函数会以其返回值来告诉你这个变量的“出生情况”,下面,是origin函数的返回值:

  • “undefined” 如果<variable>;从来没有定义过,origin函数返回这个值“undefined”。
  • “default” 如果<variable>;是一个默认的定义,比如“CC”这个变量,这种变量我们将在后面讲述。
  • “environment” 如果<variable>;是一个环境变量,并且当Makefile被执行时,“-e”参数没有被打开。
  • “file” 如果<variable>;这个变量被定义在Makefile中。
  • “command line” 如果<variable>;这个变量是被命令行定义的。
  • “override” 如果<variable>;是被override指示符重新定义的。
  • “automatic” 如果<variable>;是一个命令运行中的自动化变量。关于自动化变量将在后面讲述。

这些信息对于我们编写Makefile是非常有用的,
例如,假设我们有一个Makefile其包了一个定义文件Make.def,
在 Make.def中定义了一个变量“bletch”,而我们的环境中也有一个环境变量“bletch”,
此时,我们想判断一下,如果变量来源于环境,那么我们就把之重定义了,
如果来源于Make.def或是命令行等非环境的,那么我们就不重新定义它。于是,在我们的Makefile中,我们可以这样写:

ifdef bletch
    ifeq "$(origin bletch)" "environment"
        bletch = barf, gag, etc.
    endif
endif

shell函数

它的参数应该就是操作系统Shell的命令。它和反引号`是相同的功能。
这就是说,shell函数把执行操作系统命令后的输出作为函数返回。
于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:

contents := $(shell cat foo)
files := $(shell echo *.c)

注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,
如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。
特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。

自动化变量

  • $@ 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,”$@”就是匹配于目标中模式定义的集合。

  • $% 仅当目标是函数库文件中,表示规则中的目标成员名。
    例如,如果一个目标是”foo.a(bar.o)”,那么,”$%”就是 “bar.o”,”$@”就是”foo.a”。
    如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空。

  • $< 依赖目标中的第一个目标名字。如果依赖目标是以模式(即”%”)定义的,那么”$<”将是符合模式的一系列的文件集。
    注意,其是一个一个取出来的。

  • $? 所有比目标新的依赖目标的集合。以空格分隔。

  • $^ 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。

  • $+ 这个变量很像”$^”,也是所有依赖目标的集合。只是它不去除重复的依赖目标。

  • $* 这个变量表示目标模式中”%”及其之前的部分。如果目标是”dir/a.foo.b”,并且目标的模式是”a.%.b”,
    那么,”$*“的值就是”dir/a.foo”。这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,
    那么”$*“也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么”$*“就是除了后缀的那一部分。
    例如:如果目标是”foo.c”,因为”.c”是make所能识别的后缀名,
    所以,” $*“的值就是”foo”。这个特性是GNU make的,很有可能不兼容于其它版本的make,
    所以,你应该尽量避免使用”$*“,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么”$*“就是空值。
    当你希望只对更新过的依赖文件进行操作时,”$?”在显式规则中很有用,
    例如,假设有一个函数库文件叫”lib”,其由其它几个object文件更新。那么把object文件打包的比较有效率的Makefile规则是:

    lib : foo.o bar.o lose.o win.o

      ar r lib $?

在上述所列出来的自动量变量中。四个变量($@、$<、$%、$*)在扩展时只会有一个文件,
而另三个的值是一个文件列表。

对于”$<”,为了避免产生不必要的麻烦,我们最好给$后面的那个特定字符都加上圆括号,比如,”$(<)”就要比”$<”要好一些。

  1. makefile:使用函数
  2. 跟我一起写Makefile
  3. makefile:隐含规则

implicit declaration of function

warning: implicit declaration of function 'app_create_dialog'

包含函数’app_create_dialog’定义的头文件解决警告。
如果没有声明头文件,可以将函数extern声明一下。

unused variable

warning: unused variable 'buf'

删除不使用的变量

initialization discards qualifiers from pointer target type

xx.c: In function 'cmm_cascam_create_dialog1':
xx.c:95:26: warning: initialization discards qualifiers from pointer target type

添加类型强制转换

pointer targets in return differ in signedness

xx.c: In function 'cmm_ca_get_notice_str':
xx.c:58:5: warning: pointer targets in return differ in signedness

数据类型不匹配,强制转换

assignment discards qualifiers from pointer target type

ca/common_menu/cmm_cascam_dialog3.c: In function '_create_dialog_check':
ca/common_menu/cmm_cascam_dialog3.c:150:17: warning: assignment discards qualifiers from pointer target type

强制转换解决

defined but not used

x.c:59:12: warning: '_mutex_delete' defined but not used

删除无用代码

control reaches end of non-void function

module/cascam/cas/thinew/adapter/tnt_demux.c: In function '_nit_set_equal_filter':
module/cascam/cas/thinew/adapter/tnt_demux.c:600:1: warning: control reaches end of non-void function

添加

return ret;

pointer targets in passing argument 1 of ‘cascam_strlen’ differ in signedness

module/cascam/cas/thinew/adapter/tnt_baseinfo.c: In function '_get_basic_information':
module/cascam/cas/thinew/adapter/tnt_baseinfo.c:45:9: warning: pointer targets in passing argument 1 of 'cascam_strlen' differ in signedness
module/cascam/cas/thinew/adapter/../../../include/cascam_os.h:61:14: note: expected 'char *' but argument is of type 'unsigned char *'

形参与实参类型不匹配,强制转换

warning: missing braces around initializer

module/cascam/adapter/cascam_osd_adapter.c: In function 'cascam_osd_pop_dialog5':
module/cascam/adapter/cascam_osd_adapter.c:295:5: warning: missing braces around initializer
module/cascam/adapter/cascam_osd_adapter.c:295:5: warning: (near initialization for 'para.title')

初始化大括号不明确,内部加上一个大括号,二维数组初始化。

may be used uninitialized in this function

初始化变量

问题背景

整合版本时,遇到一个必现死机问题,每次出现问题都是调用库中的一个函数。
经过排查,初步怀疑是库存在一些问题,或者与外部使用配合出现问题。

定位问题

(gdb) set $pc=$epc
(gdb) i r r10
r10            0x970038656
(gdb) info registers 
r0             0x90972c100x90972c10
r1             0x90486564-1874303644
r2             0x970038656
r3             0xff255
r4             0x904895de-1874291234
r5             0xae174
r6             0x00
r7             0x00
r8             0x904d03a0-1874000992
r9             0x11110009286326793
r10            0x970038656
r11            0x6096
r12            0x11
r13            0x904d03a0-1874000992
r14            0x904d02a0-1874001248
r15            0x90142750-1877727408
pc             0x9000060a0x9000060a <__default_14>
epc            0x902abd300x902abd30 <inflate_fast+708>
psr            0x8f0e0010-1894907888
epsr           0x800e0150-2146565808
(gdb) pc
#0  0x9014279cin xxx_xxm()at xx.c:574
#1  0x90142750in xxx_xxm()at xx.c:568
#2  0x9014144ein xxx_xx()at xx.c:2659
#3  0x90141446in xxx_xx()at xx.c:2658
#4  0x9014143ein xxx_xx()at xx.c:2657
#5  0x901413f2in xxx_xx()at xx.c:2657
#6  0x901413dein xxx_xx()at xx.c:2652
#7  0x901413d2in xxx_xx()at xx.c:2652
(gdb) disassemble $pc-10,$pc+10
Dump of assembler code from 0x90000600 to 0x90000614:
0x90000600 <__hardware_accelerator+2>:br0x90000600
0x90000602 <__trap0+0>:bkpt
0x90000604 <__trap0+2>:br0x90000604
0x90000606 <__default_13+0>:bkpt
0x90000608 <__default_13+2>:br0x90000608
=> 0x9000060a <__default_14+0>:bkpt
0x9000060c <__default_14+2>:br0x9000060c
0x9000060e <__default_15+0>:bkpt
0x90000610 <__default_15+2>:br0x90000610
0x90000612 <__default_17+0>:bkpt
End of assembler dump.
(gdb) bt
#0  0x9000060a in __default_14 ()
#1  0x90142750 in xxx_xxm () at xx.c:568
#2  0x9013cfb8 in func () at c1.c:167
#3  0x9013f394 in main_entry (args=<value optimized out>) at init.c:14
#4  0x9008724c in default_thread_function (arg=0x907f32b8) at os/ecos/osapi.c:371
#5  0x901232f2 in pthread_entry(unsigned int) ()
#6  0x90127030 in Cyg_HardwareThread::thread_entry(Cyg_Thread*) ()
#7  0x9012701c in Cyg_Thread::exit() ()
#8  0xb0b6ccd0 in ?? ()

可以判断出r10中值出现问题,并且问题出现在文件xx.c的函数xxx_xxm()中,重启跟踪代码:

(gdb) b xxx_xxm 
Breakpoint 3 at 0x901425f4: file xx.c, line 524.
(gdb) c
Continuing.

Breakpoint 3, xxx_xxm () at xx.c:524
524xx.c: 没有那个文件或目录.
in xx.c
(gdb) display /i $r10
1: x/i $r10
0x1111000a:ldm r3-r15, (r0)
(gdb) display /i $pc
2: x/i $pc
=> 0x901425f4 <xxx_xxm+20>:lrw r7, 0x903BA9CC
(gdb) n
526in xx.c
(gdb) 
574in xx.c
2: x/i $pc
=> 0x9014279c <xxx_xxm+444>:mov r7, r2
1: x/i $r10
0x1111000a:movi r4, 86
(gdb) 
575in xx.c
2: x/i $pc
=> 0x901427a6 <xxx_xxm+454>:mov r2, r10
1: x/i $r10
0x9700:movi r4, 86

问题很可能出现在源文件xx.c的574行,执行过这一行之后,r10的值修改为0x9700,最后请客户检查代码,确认为野指针。

map模式

有五种映射存在:

  1. Normal mode: 普通模式, 输入命令时
  2. Visual mode: 可视模式: 可视区域高亮并输入命令时
  3. Select mode: 选择模式: 与可视模式区别是会替换选择文本
  4. Operator-pending mode: 操作符等待模式: 操作符等待中 (“d”,”y”,”c” 等等之后)。 见下: |omap-info|。
  5. Insert mode: 插入模式: 也用于替换模式。
  6. Command-line mode: 命令行模式: 输入 “:” 或 “/“ 命令时。

快捷键映射

快捷键映射分两种: map 和 noremap

递归映射 (map)

如果键 b 映射为键 a,然后键 c 映射为键 b,那么当按键 c 时会产生按键 a 的效果。

:map b a
:map c b

相当于

:map c a

非递归映射 (noremap)

:noremap b a
:noremap c b

非递归映射则不会产生递归映射一样的效果。

不同模式下的快捷键映射

映射模式

在 map 与 noremap 前分别可以加 ‘n’, ‘v’, ‘x’, ‘s’, ‘o’, ‘i’, ‘l’, ‘c’ 以及 ‘map!’ 和 ‘noremap!’:

  • Normal, Visual, Select and Operator-pending
  • n Normal
  • v Visual and Select
  • s Select (在可视模式下Ctrl+G进入)
  • x Visual
  • o Operator-pending
  • ! Insert and Command-line
  • i Insert
  • l “:lmap” mappings for Insert, Command-line and Lang-Arg
  • c Command-line

常用参数:

  • {lhs} 表示左手边 *{lhs}*
  • {rhs} 表示右手边 *{rhs}*

普通模式的映射命令

可以随时:help map一下,查看相关命令具体信息。

map

:map {lhs} {rhs} 
:map {lhs} {rhs} |mapmode-nvo| *:map* 

作用模式: n、v、o (普通、可视和选择、操作符等待)。
在:map作用的模式中把键系列 {lhs} 映射为 {rhs},{rhs}可进行映射扫描,也就是可递归映射。

举例:

:map td :tabnew .<cr> 

在其作用模式(普通、可视、操作符)下,输入td等价于输入 :tabnew .
而普通模式下输入:tabnew . 就是打开当前目录,
如果再定义绑定:

:map ts td

就是指在其作用模式下输入ts等价于td,也就是打开当前目录。不过如果没有特殊需要,一般不建议递归映射。

noremap

:moremap和:map命令相对,作用模式和命令格式都相同,
只不过不允许再对{rhs}进行映射扫描,也就是{lhs}定义后的映射就是{rhs}的键序列,不会再对{rhs}键序列重新解释扫描。
它一般用于重定义一个命令,当然如果:map不需要递归映射的话,建议使用:noremap,比如:

:noremap ts td 

它的意思是在其作用模式下,输入ts就是输入td,但是和:map不同的是,此时td再不会做进一步扫描解释。
虽然之前已经定义了td,但是不会对td再做扫描

unmap

:unmap是对应取消:map绑定的{lhs},作用模式相同,命令格式

:unmap {lhs}

例如:

:unmap td 

就是取消在其作用模式中td的绑定,比如之前td被绑定为:tabnew .,此时此绑定消失。

mapclear

:mapclear时对应取消所有:map绑定的,慎用!

nmap

:nmap是:map的普通模式板,也就是说其绑定的键只作用于普通模式。例如:

:nmap td :tabnew .<cr> 

在普通模式下等效

:map td :tabnew .<cr> 

nnoremap

:nnorempa和:nmap的关系和:noremap和:map的关系一样,只是:nmap的非递归版

nunmap

:nunmap和:nmap的关系和:unmap和:map的关系一样,取消:nmap的绑定。

nmapclear

:nmapclear是对应取消所有:map绑定的,慎用!

可以发现一个规律,前4个是一组,后4个时一组,后一组比前一组多一个n就是指只作用于普通模式。
其中每组内*nore*是其对应的非递归版、*un*是取消绑定某个<lhs>绑定、clear后缀是取消所有绑定。
发现了这个规律,再翻到前面的模式代号表,
大体可以猜到vmap、xmap、smap、omap是什么意思,以及相对应的nore版本、un版本、clear版本。

另外,{rhs} 之前可能显示一个特殊字符:

  • * 表示它不可重映射
  • & 表示仅脚本的局部映射可以被重映射
  • @ 表示缓冲区的局部映射

键表|key-notation|

  • <k0> - <k9> 小键盘 0 到 9 *keypad-0* *keypad-9*
  • <S-…> Shift+键 *shift* *<S-*
  • <C-…> Control+键 *control* *ctrl* *<C-*
  • <M-…> Alt+键 或 meta+键 *meta* *alt* *<M-*
  • <A-…> 同 <m-…> *<A-*
  • <t_xx> termcap 里的 “xx” 入口键

特殊参数

  • <buffer>
  • <nowait>
  • <silent>
  • <special>
  • <script>
  • <expr>
  • <unique>

可以按任意顺序使用。它们必须紧跟在命令的后边,而在其它任何参数的前边。

<buffer>

如果这些映射命令的第一个参数是<buffer>,映射将只局限于当前缓冲区(也就是你此时正编辑的文件)内。比如:

:map <buffer> ,w /a<CR> 

它的意思时在当前缓冲区里定义键绑定,“,w”将在当前缓冲区里查找字符a。同样你可以在其他缓冲区里定义:

:map <buffer> ,w /b<CR> 

其作用域也只在各自的标签里。同样要清除这些缓冲区的键绑定也要加上<buffer>参数,比如:

:unmap <buffer> ,w 
:mapclear <buffer> 

<nowait>

定义局部于缓冲区的映射 “,” 时,可能有另一个全局映射也以 “,” 开始。
这时你需要键入另一个字符,Vim 才能知道是用 “,” 映射还是更长的那个。
要避免这个问题,加入 参数。
这样映射一旦匹配就会被使用,Vim 不会等待更多字符的输入。但如果那些字符已经输入了,还是会使用的。

<silent>

执行键绑定时不在命令行上回显,比如:

:map <silent> ,w /abcd<CR> 

输入,w查找abcd时,命令行上不会显示/abcd,如果没有<silent>参数就会显示出来

<special>

一般用于定义特殊键怕有副作用的场合。比如:

:map <special> <F12> /Header<CR> 

<unique>

一般用于定义新的键映射或者缩写命令的同时检查是否该键已经被映射,如果该映射或者缩写已经存在,则该命令会失败

:map <unique> ,w  /[#&!]<CR>

这个例子将失败:

:map ,w  /[#&!]<CR>
:map <buffer> <unique> ,w  /[.,;]<CR>

<expr>

如果定义新映射的第一个参数是<expr>,那么参数会作为表达式来进行计算,结果使用实际使用的<rhs>,例如:

:inoremap <expr\> . InsertDot() 

这可以用来检查光标之前的文本并在一定条件下启动全能 (omni) 补全。

这里是插入递增的列表编号的例子:

let counter = 0 
inoremap <expr> <C-L> ListItem() 
inoremap <expr> <C-R> ListReset() 

func ListItem() 
    let g:counter += 1 
    return g:counter . '. ' 
endfunc 

func ListReset() 
    let g:counter = 0 
    return '' 
endfunc 

CTRL-L 插入下一个数值,CTRL-R 复位计数且返回空字符串,这样就不会插入任何内容。

<Leader>mapleader

要定义一个使用 “mapleader” 变量的映射,可以使用特殊字串 ““。
它会被 “mapleader” 的字串值所替换。如果 “mapleader” 未设置或为空,则用反斜杠代替,例如:

:map <Leader>A  oanother line<Esc>

和下面一样:

:map \A  oanother line<Esc>

但是当:

:let mapleader = ","

又相当于:

:map ,A  oanother line<Esc>

注意 “mapleader” 的值仅当定义映射时被使用。后来改变的 “mapleader” 不会影响已定义的映射。

  1. VIM 参考手册-MAP

vim配置

Vim-go是当前使用最为广泛的用于搭建Golang开发环境的vim插件:

Bundle 'fatih/vim-go'
:BundleInstall

Tagbar配合gotags配置:

let g:tagbar_type_go = {
    \ 'ctagstype' : 'go',
    \ 'kinds'     : [
        \ 'p:package',
    \ 'i:imports:1',
    \ 'c:constants',
    \ 'v:variables',
    \ 't:types',
    \ 'n:interfaces',
    \ 'w:fields',
    \ 'e:embedded',
    \ 'm:methods',
    \ 'r:constructor',
    \ 'f:functions'
        \ 
    ],
    \ 'sro' : '.',
    \ 'kind2scope' : {
        \ 't' : 'ctype',
        \ 'n' : 'ntype'
            \ 
    },
    \ 'scope2kind' : {
        \ 'ctype' : 't',
        \ 'ntype' : 'n'
            \ 
    },
    \ 'ctagsbin'  : 'gotags',
    \ 'ctagsargs' : '-sort -silent'
    \ }

安装Binaries

还需要安装其他tools,vim-go安装说明中提到所有必要的binary需要先安装好,比如gocode、godef、goimports等。通过:

:GoInstallBinaries

查看文件.vim/bundle/vim-go/plugin/go.vim:

let s:packages = [
    \ "github.com/nsf/gocode",
    \ "golang.org/x/tools/cmd/goimports",
    \ "github.com/rogpeppe/godef",
    \ "golang.org/x/tools/cmd/oracle",
    \ "golang.org/x/tools/cmd/gorename",
    \ "github.com/golang/lint/golint",
    \ "github.com/kisielk/errcheck",
    \ "github.com/jstemmer/gotags",
    \ ]

这些vim-go依赖的二进制工具将会自动被下载,并被安装到$GOBIN下,但是自动安装很可能部分tools安装不成功,此时需要手动安装:

go get -u github.com/nsf/gocode

//或者
git clone https://go.googlesource.com/tools $GOPATH/src/golang.org/x/tools
go build golang.org/x/tools/cmd/goimports

安装golint:

./3rdpkg/src/github.com/golang
git clone git@github.com:golang/lint.git
cd ../../../../go/bin
pwd
./go/bin
go build -x github.com/golang/lint/golint
cp $WORK/github.com/golang/lint/golint/_obj/exe/a.out golint

常用工具

安装完毕$GOBIN目录如下:

.
├── errcheck
├── go
├── gocode
├── godef
├── godoc
├── gofmt
├── goimports
├── gorename
├── gotags
└── oracle
  • gotags 这个用来配合vim的tagbar,查看方便的生成和查看文件中的各种方法,并进行跳转
  • goimport 根据你所使用的方法,自动调整import中引用的内容,比较方便。
    可以随时用vim命令 :Fmt 来格式化你的代码,也可以每次保存代码的时候,让vim自动为你格式化代码
  • godef 实现在代码中的跳转,从函数调用的地方,直接跳转到函数的定义。默认的命令是:gd
  • gocode实现Golang的代码自动补全

用法

  • 新起一行输入fmt.,然后ctrl+x, ctrl+o,Vim 会弹出补齐提示下拉框,不过并非实时跟随的那种补齐,这个补齐是由gocode提供的。
  • 输入一行代码:time.Sleep(time.Second),执行:GoImports,Vim会自动导入time包。
  • 将光标移到Sleep函数上,执行:GoDef或命令模式下敲入gd,Vim会打开$GOROOT/src/time/sleep.go中 的Sleep函数的定义。执行:b 1返回到hellogolang.go。
  • 执行:GoLint,运行golint在当前Go源文件上。
  • 执行:GoDoc,打开当前光标对应符号的Go文档。
  • 执行:GoVet,在当前目录下运行go vet在当前Go源文件上。
  • 执行:GoRun,编译运行当前main package。
  • 执行:GoBuild,编译当前包,这取决于你的源文件,GoBuild不产生结果文件。
  • 执行:GoInstall,安装当前包。
  • 执行:GoTest,测试你当前路径下地_test.go文件。
  • 执行:GoCoverage,创建一个测试覆盖结果文件,并打开浏览器展示当前包的情况。
  • 执行:GoErrCheck,检查当前包种可能的未捕获的errors。
  • 执行:GoFiles,显示当前包对应的源文件列表。
  • 执行:GoDeps,显示当前包的依赖包列表。
  • 执行:GoImplements,显示当前类型实现的interface列表。
  • 执行:GoRename [to],将当前光标下的符号替换为[to]。

Reference

  1. Golang开发环境搭建
  2. gocode
  3. Go

GB2312-80

GB 2312或GB 2312-80 是中国国家标准简体中文字符集
全称《信息交换用汉字编码字符集·基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。
GB2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB 2312。

GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;
同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。

  • GB 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。
  • 对于人名、古汉语等方面出现的罕用字,GB 2312不能处理,这导致了后来GBK及GB 18030汉字字符集的出现。

GB2312对任意一个图形字符都采用两个字节表示,并对所收汉字进行了“分区”处理,每区含有94个汉字/符号,
分别对应第一字节和第二字节。这种表示方式也称为区位码。

  • 01-09区为特殊符号。
  • 16-55区为一级汉字,按拼音排序。
  • 56-87区为二级汉字,按部首/笔画排序。
  • 10-15区及88-94区则未有编码。

GB2312的编码范围为2121H-777EH,与ASCII有重叠,通行方法是将GB码两个字节的最高位置1以示区别。

GBK

GBK即汉字内码扩展规范,K为汉语拼音 Kuo Zhan(扩展)中“扩”字的声母。英文全称Chinese Internal Code Specification。

GBK共收入21886个汉字和图形符号,包括:

  • GB2312中的全部汉字、非汉字符号。
  • BIG5中的全部汉字。
  • 与ISO 10646相应的国家标准GB13000中的其它CJK汉字,以上合计20902个汉字。
  • 其它汉字、部首、符号,共计984个。

GBK向下与GB2312 完全兼容,向上支持ISO 10646国际标准,在前者向后者过渡过程中起到的承上启下的作用。

GBK 采用双字节表示,总体编码范围为8140-FEFE之间,首字节在81-FE之间,尾字节在40-FE之间,剔除XX7F一条线。GBK编码区分三部分:

  • 汉字区:
    • GBK/2:OXBOA1-F7FE, 收录GB2312汉字6763个,按原序排列;
    • GBK/3:OX8140-AOFE,收录CJK汉字6080个;
    • GBK/4:OXAA40-FEAO,收录CJK汉字和增补的汉字8160个。
  • 图形符号区:
    • GBK/1:OXA1A1-A9FE,除GB2312的符号外,还增补了其它符号
    • GBK/5:OXA840-A9AO,扩除非汉字区。
  • 用户自定义区
    • GBK区域中的空白区,用户可以自己定义字符。

GB18030

GB 18030,全称:国家标准GB 18030-2005《信息技术中文编码字符集》
是中华人民共和国现时最新的内码字集,是GB 18030-2000《信息技术信息交换用汉字编码字符集基本集的扩充》的修订版。
GB 18030与GB 2312-1980完全兼容,与GBK基本兼容,支持GB 13000及Unicode的全部统一汉字,共收录汉字70244个。

  • 与 UTF-8 相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。
  • 编码空间庞大,最多可定义161万个字符。
  • 支持中国国内少数民族的文字,不需要动用造字区。
  • 汉字收录范围包含繁体汉字以及日韩汉字

GB18030 编码是一二四字节变长编码。

  • 单字节,其值从0到0x7F,与 ASCII 编码兼容。
  • 双字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x40到0xFE(不包括0x7F),与 GBK标准基本兼容。
  • 四字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x30到0x39,第三个字节从0x81到0xFE,第四个字节从0x30到0x39。

CP936

代码页936(Codepage 936)是 Microsoft 的简体中文字元集标准
是东亚语文的四种双字节字元集(DBCS)之一。其最初版本和 GB 2312 一模一样,
但在推出 Windows 95 时扩展成包含绝大部分的 GBK 字元(代码页936 比 GBK 少95个字元,皆为当时尚未收入 Unicode 的字元);
虽然该等字元现在已全部收入 Unicode,但代码页936 至今都没有修订。

libiconv

几个函数入口及相关转换表:

static int ces_gbk_mbtowc (conv_t conv, ucs4_t *pwc, const unsigned char *s, int n);
static int gbk_mbtowc (conv_t conv, ucs4_t *pwc, const unsigned char *s, int n);

重要头文件:

lib/encodings.def
lib/ces_gbk.h
lib/gbk.h
lib/gb2312.h
lib/cp936ext.h
  1. GB 2312
  2. GB2312简体中文编码表
  3. 汉字内码扩展规范
  4. GB 18030
  5. 代码页936
  6. Unicode code converter
  7. 中文編碼網頁GB18030
  8. International Components for Unicode ː Repository Browser
  9. GB2312-80 to Unicode table
  10. GB18030 Conversion Documentation