Linux
1.修改软链接
ln –snf [新的源文件或目录] [目标文件或目录]
这将会修改原有的链接地址为新的地址
例如:
创建一个软链接
ln –s /var/www/test /var/test
修改指向的新路径
ln –snf /var/www/test1 /var/test
- vim跳转行
命令模式下ngg
:n
- vim分屏
:sp 文件名 水平
:vsp 文件名 垂直
- 解压到指定文件夹
tar -xzvf 源文件 -C(大写) 指定文件夹 (tar.gz)
tar -czvf 新文件 源文件(或目录/*)
- vim搜索字符串
命令行模式
/字符串 (回车,n下一跳,N上一跳)
- ls 显示颜色
白色表示普通文件;
亮绿色表示可执行文件;
亮红色表示压缩文件;
灰蓝色表示目录;
亮蓝色表示链接文件;
亮黄色表示设备文件;
- Vimdiff
光标移动
可以使用下列两种快捷键,在文件的各个差异点之间前后移动:
], c:跳转到下个差异点
[, c:跳转到上个差异点
至于光标在两个窗口之前的切换,可以使用如下按键:
Ctrl-w, l:光标切换到右侧的窗口
Ctrl-w, h:光标切换到左侧的窗口
Ctrl-w, w:光标在两个窗口间彼此切换
内容合并
可以使用 d, p (即 diff put)命令,将当前差异点中的内容覆盖到另一文件中的对应位置。
如当光标位于左侧文件(file1)中的第一行时,依次按下 d、p 键,则 file1 中的 Line one 被推送到右侧,并替换掉 file2 中对应位置上的 Line
即在 file1 的第一行执行 d o 命令后,file2 中的第一行内容 Line 1 被拉取到 file1 中并替换掉原来位置上的 Line one。
同时操作两个文件
vimdiff 实际上是 Vim 编辑器的 diff 模式,因此适用于 Vim 编辑器的命令和快捷键也同样可以在该模式下使用。常用的几个命令如下:
:qa:退出所有文件
:wa:保存所有文件
:wqa:保存并退出所有文件
qa!:强制退出(不保存)所有文件
z o:查看被折叠的内容
z c:重新折叠
8.shell运算符
布尔运算符
下表列出了常用的布尔运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
! | 非运算,表达式为 true 则返回 false,否则返回 true。 | [ ! false ] 返回 true。 |
-o | 或运算,有一个表达式为 true 则返回 true。 | [ $a -lt 20 -o $b -gt 100 ] 返回 true。 |
-a | 与运算,两个表达式都为 true 才返回 true。 | [ $a -lt 20 -a $b -gt 100 ] 返回 false。 |
关系运算符
关系运算符只支持数字,不支持字符串,除非字符串的值是数字。
下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20:
运算符 说明 举例
-eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。
-ne 检测两个数是否不相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
-gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。
-lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。
-ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。
-le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。
Shell 脚本中 ‘$’ 符号的多种用法
在前面的文章里,我们介绍了什么是 Shell 脚本,以及编写简单的 Shell 脚本,数值 / 字符串 / 文件状态测试的关系运算符以及 if-then-else / case 分支结构、for / while / until 循环结构的基础,详情请参考:Shell编程-条件测试 | 基础篇 和 Shell编程-控制结构 | 基础篇
通常情况下,在工作中用的最多的有如下几项:
$0:Shell 的命令本身
$1 到 $9:表示 Shell 的第几个参数
$? :显示最后命令的执行情况
$#:传递到脚本的参数个数
$$:脚本运行的当前进程 ID 号
$*:以一个单字符串显示所有向脚本传递的参数
$!:后台运行的最后一个进程的 ID 号
$-:显示 Shell 使用的当前选项
字符串运算符
下表列出了常用的字符串运算符,假定变量 a 为 "abc",变量 b 为 "efg":
运算符 | 说明 | 举例 |
= | 检测两个字符串是否相等,相等返回 true。 | [ $a = $b ] 返回 false。 |
!= | 检测两个字符串是否不相等,不相等返回 true。 | [ $a != $b ] 返回 true。 |
-z | 检测字符串长度是否为0,为0返回 true。 | [ -z $a ] 返回 false。 |
-n | 检测字符串长度是否不为 0,不为 0 返回 true。 | [ -n "$a" ] 返回 true。 |
$ | 检测字符串是否不为空,不为空返回 true。 | [ $a ] 返回 true。 |
文件测试运算符
文件测试运算符用于检测 Unix 文件的各种属性。
属性检测描述如下:
操作符 | 说明 | 举例 |
-b file | 检测文件是否是块设备文件,如果是,则返回 true。 | [ -b $file ] 返回 false。 |
-c file | 检测文件是否是字符设备文件,如果是,则返回 true。 | [ -c $file ] 返回 false。 |
-d file | 检测文件是否是目录,如果是,则返回 true。 | [ -d $file ] 返回 false。 |
-f file | 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 | [ -f $file ] 返回 true。 |
-g file | 检测文件是否设置了 SGID 位,如果是,则返回 true。 | [ -g $file ] 返回 false。 |
-k file | 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 | [ -k $file ] 返回 false。 |
-p file | 检测文件是否是有名管道,如果是,则返回 true。 | [ -p $file ] 返回 false。 |
-u file | 检测文件是否设置了 SUID 位,如果是,则返回 true。 | [ -u $file ] 返回 false。 |
-r file | 检测文件是否可读,如果是,则返回 true。 | [ -r $file ] 返回 true。 |
-w file | 检测文件是否可写,如果是,则返回 true。 | [ -w $file ] 返回 true。 |
-x file | 检测文件是否可执行,如果是,则返回 true。 | [ -x $file ] 返回 true。 |
-s file | 检测文件是否为空(文件大小是否大于0),不为空返回 true。 | [ -s $file ] 返回 true。 |
-e file | 检测文件(包括目录)是否存在,如果是,则返回 true。 | [ -e $file ] 返回 true。 |
其他检查符:
- -S: 判断某文件是否 socket。
- -L: 检测文件是否存在并且是一个符号链接。
例:
file="/var/www/runoob/test.sh"
($ 判断字符串是否为空 取变量值)
if [ -s $file ]
then
echo "文件不为空"
else
echo "文件为空"
fi
if [ -e $file ]
then
echo "文件存在"
else
echo "文件不存在"
fi
9.Shell脚本
1.echo -e
-e:支持反斜线控制的字符转换(具体参见表 1)
-n:取消输出后行末的换行符号(内容输出后不换行)
\e[1 是标准格式,代表颜色输出开始,\e[0m 代表颜色输出结束,31m 定义字体颜色是红色。
echo 命令能够识别的颜色如下:30m=黑色,31m=红色,32m=绿色,33m=黄色,34m=蓝色,35m=洋红,36m=青色,37m=白色。
2.awk命令详解
awk [选项] '脚本命令' 文件名
awk 的强大之处在于脚本命令,它由 2 部分组成,分别为匹配规则和执行命令,如下所示:
'匹配规则{执行命令}'
默认情况下,awk 会将如下变量分配给它在文本行中发现的数据字段:
$0 代表整个文本行;
$1 代表文本行中的第 1 个数据字段;
$2 代表文本行中的第 2 个数据字段;
$n 代表文本行中的第 n 个数据字段。
awk 允许将多条命令组合成一个正常的程序。要在命令行上的程序脚本中使用多条命令,只要在命令之间放个分号即可,例如:
[root@localhost ~]# echo "My name is Rich" | awk '{$4="Christine"; print $0}'
My name is Christine
第一条命令会给字段变量 $4 赋值。第二条命令会打印整个数据字段。可以看到,awk 程序在输出中已经将原文本中的第四个数据字段替换成了新值。
10.路由
1.协议和一些含义
指该路由条目是通过什么路由协议学些过来的。例如是直连的,或是静态的,或者是通过OSPF、IS-IS、EIGRP、BGP等动态路由学习到的。路由匹配时,子网掩码长度长的优先级高,即10.5.0.0/17的匹配程度高于10.5.0.0/16
每种协议类型对应不同的优先级,优先级值越小则路由越优。
常用路由协议和优先级的关系表如下图。
开销
开销:路由的度量值,经常也使用metric来描述。(就是经过多少个路由器才能达到目的地址的开销?)Metric的值越小,优先级越高
如果两块网卡的Metric的值相同,就会出现抢占优先级继而网卡冲突,将会有一块网卡无法连接。直连及静态路由的Cost为0。
Weight
调整静态路由的优先级,可以设置其权重值。权重值越大级别越低。
局域网两个不同网段互相访问 - yuxi_o - 博客园 (cnblogs.com)
iP route 错误 RTNETLINK answers: Network is unreachable:
对应网卡没有起来,IP link
Arp没学习到网关的Mac
使用的网关不属于本机所在网络
缺少直连路由
IP路由表中包含了目的网络/掩码,协议类型,优先级,开销,标志,下一跳,出接口这个七大要素。
(1)直连路由和静态路由
三个网段的网关地址都在同一个路由器的接口上,属于直连路由。
路由器会自动生成路由表,不需要手动配置就可以让这三个区域互相ping通。这就是直连路由的作用!
静态路由的公式:ip route + ‘目的地址’+‘目的地址子网掩码’+‘下一跳路由地址’。
静态路由的工作原理:
与直连路由一样,当路由器收到一段IP数据包时,会将IP数据包拆开,寻找目的IP地址,当找到目的IP地址后,会查自身路由表中的路由,从而寻找到由哪个端口发出数据包,将数据包重新打包后发出,完成路由动作。
策略路由
它只不过是一种复杂的静态路由,可以基于数据包 源或目的 地址向指定下一跳路由器转发数据包。
(2)scope和proto
scope 类型 指的是路由前缀覆盖的目标地址范围。 scope link表示在设备的网段内允许通过该路由进行通信。 通过其他往网段的话的话,应该使用路由。
protocol类型 是该路由的路由协议标识符。proto kernel的意思是: 在自动配置过程中由内核安装的路由。(boot 表示启动时安装的路由,)
取值:ip route help查看
link: 当一个地址有意义且只能在局域网内使用时,该地址就有链接作用域。例如子网的广播地址。
使用作用域的主要原因似乎是,具有多个接口和地址的主机必须决定何时使用哪个地址。为了与自身通信,可以使用环回地址(作用域主机)。如果在其他地方通信,则必须选择不同的地址。
(3)最长掩码匹配原则匹配的就是目的网络/掩码。
比如:路由器收到一个目的IP地址为10.1.1.1的数据包,此时查找路由表,有两个路由条目,一个路由条目的A的目的网络/掩码是10.1.1.0/24,另一条路由条目B的目的网络/掩码是10.1.1.0/28,匹配路由条目B,因为B的掩码长。
2.路由的权重值和度量值
(54条消息) 路由来源、优先级和度量值_路由度量值_布道天下的博客-CSDN博客
边缘路由器:
Metric:度量值都为零。Metric的值越小,优先级越高。如果两块网卡的Metric的值相同,就会出现抢占优先级继而网卡冲突,将会有一块网卡无法连接。
度量值就是设备到达目的网络的代价值。
直连路由的度量值为0(路由器认为这是自己直连的网络,也就是在“家门口”的网终,从自己家走到家门口自然不需要耗费任何代价)。另外静态路由的度量值缺省也为0,而不同的动态路由协议定义的度量值是不同的,比如RIP路由是以跳数(到达目的网络所需经过的路由器的个数)作为度量值,而OSPF则以开销(与链路带宽有关)作为度量值。
Weight:所以使用权重值weight来衡量静态路由的优先级。调整静态路由的优先级,可以设置其权重值??????权重值越大级别越低?????
静态路由的weight权重缺省时值为1,权重参数是在静态路由实现负载分担时使用的一个参数,决定IP包负载分担的比例。当有两条或两条以上路由到达同一目的地址,但是下一跳不同的时候,路由器按照各条路由的权重比例转发IP包,从而实现负载分担的目的。
255-优先级+1??(先不管,可能用在负载均衡)
操作系统上静态路由优先, 路由设备上直连路由优先. 当然这都是在相同网段的前提下, 在网段不同的时候, 都遵循深度优先原则, 即网段越小优先级越高.
在路由表输出中,"metric" 和 "pref" 是用于指示路由优先级和度量值的字段:
Metric(度量值):度量值用于指示路由的优先级。较小的度量值表示更高的优先级。当系统有多个路由可选时,它将选择具有较小度量值的路由进行数据包转发。在你的例子中,路由的度量值为 1024,这表示该路由的优先级相对较低。
Pref(优先级):优先级用于指示路由的优先级顺序,当度量值相同时,优先级将决定路由的选择。较高的优先级表示更高的优先级。在你的例子中,路由的优先级为 "medium",这表示该路由的优先级处于中等水平。
3.ECMP和WCMP
WCMP能够非常灵活地按照比例在链路上传递流量,ECMP是它的特例
关于流量的动态分配,即所谓的负载均衡问题:
1)负载分担方式有3种。
基于流负载分担:路由器根据IP报文的五元组信息(是指源IP地址,源端口,目的IP地址,目的端口,和传输层协议这五个量组成的一个集合。 例如:192.168.1.1 10000 TCP 121.14.88.76 80 就构成了一个五元组)将数据分成不同的流。具有相同五元组信息的IP报文属于同一个流。转发数据时,路由器把不同的数据流根据算法从多个路径上依次发送出去。
基于包负载分担:转发数据时,路由器把数据包从多个路径上依次发送出去。
基于带宽的非平衡负载分担:报文按接口物理带宽进行负载分担(即基于报文的负载分担)。当用户为接口配置了指定的负载带宽后,设备将按用户指定的接口带宽进行负载分担,即根据各接口物理带宽比例关系进行分配。
基于包转发能够做到更精确的负载分担。但是由于路由器要对每一个包进行路由查表与转发操作,所以无法使用快速转发缓存来转发数据,转发效率降低了。另外,Internet应用都是基于流的,如果路由器采用基于包的负载分担,一条流中的数据包会经过不同路径到达目的地,可能会造成接收方的乱序接收,从而影响应用程序的正常运行。
ECMP是指,到达一个目的地有多条相同度量值(metric)的路由项(路由路径)
ECMP面临的问题:
实际情况是,各路径的带宽、时延和可靠性等不一样,把Cost认可成一样,不能很好地利用带宽,尤其在路径间差异大时,效果会非常不理想。例如,路由器两个出口,两路径,一个带宽是100M,一个是2M,如果部署是ECMP,则网络总带宽只能达到4M的利用率。(在RFC2991中讨论了一般的多路径路由。每一封包多路径路由的负载平衡通常不适用因为大辐变化的延迟、数据包重新排序,以及可以破坏许多互联网协定运作的最大传输单元(MTU)在网络流量的差异,最特别是传输控制协议(TCP)和path MTU discovery。)另外一种情况下等价多路径路由也不能提供真正的最佳路径路由的优点,例如,如果多个最佳的next-hop的路径到目的地重新汇聚到一个单一的低带宽的路径(一种常见的情形)下游,它只会增加到该目的地流量路径的复杂性,而无法提高带宽的能力。
然而ECMP是一种较为简单的负载均衡策略,其在实际使用中面临的问题也不容忽视。
1.可能增加链路的拥塞
ECMP并没有拥塞感知的机制,只是将流分散到不同的路径上转发。对于已经产生拥塞的路径来说,很可能加剧路径的拥塞。而使用哈希的方法,产生哈希碰撞也会增加链路的拥塞可能。
2.非对称网络使用效果不好
例如图2中,A与h3之间的通信,ECMP只是均匀的将流通过B,D两条路径分别转发,但实际上,在B处可以承担更多的流量。因为B后面还有两条路径可以到达h3。
3.基于流的负载均衡效果不好
ECMP对于流大小相差不多的情况效果更好,而对于流大小差异较大,例如大象流和老鼠流并存的情况下,效果不好。如图2,主机h1到A的流量为15,h2到A的流量为5。那么无论为h1的流量选择哪条路径都会发生拥塞。但若将h1的流拆分成两部分传输,可以避免拥塞的情况。
以上,为使用ECMP算法进行负载均衡的分析,在数据中心这种突发性流量多,大象流与老鼠流并存的环境中,需要慎重考虑选择的负载均衡策略,ECMP简单易部署但也存在较多问题需要注意。
多个接口起来,直连路由的影响。
IPrule的使用
Mangle表使用
多个路由表是怎么匹配的?路由规则不是匹配防火墙的吗?
理解:在对应每个接口都有不同的路由表,所以查询出口具体查哪个表了?
Bug。
11.IPtable
四表五链
当4表处于同一条"链"时,执行的优先级如下。
raw --> mangle --> nat --> filter
1.Filter表 一般的过滤功能
2. NAT表 用于nat功能(端口映射,地址映射等)
3. Mangle表 提供修改数据包IP头部的功能,例如,修改数据包的TTL等。此外,mangle表中的规则还可以对数据包打一个仅在内核内有效的标记(mark),后续对于该数据包的处理可以用到这些标记。它能改变TCP头中的QoS位。
4. Raw表 有限级最高,设置raw时一般是为了不再让iptables做数据包的链接跟踪处理,提高性能 Raw表用于处理异常,
五链
PREROUTING链 – 处理刚到达本机并在路由转发前的数据包。它会转换数据包中的目标IP地址(destination ip address),通常用于DNAT(destination NAT)。
INPUT:通过路由表后目的地为本机
FORWARD链 – 将数据转发到本机的其他网卡设备上。
OUTPUT链 – 处理本机产生的数据包。
POSTROUTING链 – 处理即将离开本机的数据包。它会转换数据包中的源IP地址(source ip address),通常用于SNAT(source NAT)。
报文流向
iptables规则执行
iptables执行规则时,是从规则表中从上至下顺序执行的。
若没遇到匹配的规则,就一条一条往下匹配;
若完全没有匹配的规则,就执行该链上的默认规则;
若遇到匹配的规则,则执行规则,执行后根据本规则的动作,决定下一步执行的情况,后续执行一般有三种情况:
1。一种是继续执行当前规则队列内的下一条规则。比如执行过Filter队列内的LOG后,还会执行Filter队列内的下一条规则。
2。一种是中止当前规则队列的执行,转到下一条规则队列。比如从执行过accept后就中断Filter队列内其它规则,跳到nat队列规则去执行(有误,这是最后一个表)
3。一种是中止所有规则队列的执行。
表链理解
为chain配置规则,将规则写入表中。默认filter表,那其他表作用?(只是归纳相似的规则)
我们把具有相同功能的规则的集合叫做"表",定义的规则也都逃脱不了这4种功能的范围,在实际的使用过程中,往往是通过"表"作为操作入口,对规则进行定义的,其实我们还需要注意一点,因为数据包经过一个"链"的时候,会将当前链的所有规则都匹配一遍,但是匹配时总归要有顺序,我们应该一条一条的去匹配,而且我们说过,相同功能类型的规则会汇聚在一张"表"中,那么,哪些"表"中的规则会放在"链"的最前面执行呢,这时候就需要有一个优先级的问题
prerouting链中的规则存放于三张表中,而这三张表中的规则执行的优先级如下:
raw --> mangle --> nat
但是我们知道,iptables为我们定义了4张"表",当他们处于同一条"链"时,执行的优先级如下。
优先级次序(由高而低):
raw --> mangle --> nat --> filter
但是我们前面说过,某些链天生就不能使用某些表中的规则,所以,4张表中的规则处于同一条链的目前只有output链,它就是传说中海陆空都能防守的关卡。
自定义链作用??
一条链不可能包含所有规则,只能包含一定规则
表的划分依据:防火墙规则的作用相似
命令操作
(1)查看命令
·-L:list,列出指定链上的所有规则
·-n:numberic,以数字格式显示地址和端口号
·-v:verbose,显示详细信息
·-line-numbers:显示规则编号
pkts:对应规则匹配到的报文的个数。
bytes:对应匹配到的报文包的大小总和。
target:规则对应的target,往往表示规则对应的"动作",即规则匹配成功后需要采取的措施。
prot:表示规则对应的协议,是否只针对某些协议应用此规则。
opt:表示规则对应的选项。
in:表示数据包由哪个接口(网卡)流入,我们可以设置通过哪块网卡流入的报文需要匹配当前规则。
out:表示数据包由哪个接口(网卡)流出,我们可以设置通过哪块网卡流出的报文需要匹配当前规则。
source:表示规则对应的源头地址,可以是一个IP,也可以是一个网段。
destination:表示规则对应的目标地址。可以是一个IP,也可以是一个网段。
如果你想要查看精确的计数值,而不是经过可读性优化过的计数值,那么你可以使用-x选项,表示显示精确的计数值,
-S查看规则
iptables -t filter -S INPUT
(2)规则命令:
-A:append,将新规则追加于指定链的尾部
-I num:insert,将新规则插入至指定链的指定位置。-I 3:插入为第三条
-D num:delete,删除指定链上的指定规则,明确制定删除第几条规则
-R num:replace,替换指定链上的指定规则
- //追加规则
- 命令语法:iptables -t 表名 -A 链名 匹配条件 -j 动作
- 示例:iptables -t filter -A INPUT -s 192.168.1.146 -j DROP
- //插入规则
- 命令语法:iptables -t 表名 -I 链名 匹配条件 -j 动作
- 示例:iptables -t filter -I INPUT -s 192.168.1.146 -j ACCEPT
- 命令语法:iptables -t 表名 -I 链名 规则序号 匹配条件 -j 动作
- 示例:iptables -t filter -I INPUT 5 -s 192.168.1.146 -j REJECT
- 命令语法:iptables -t 表名 -P 链名 动作
- 示例:iptables -t filter -P FORWARD ACCEPT
- #删除规则
- 命令语法:iptables -t 表名 -D 链名 规则序号
- 示例:iptables -t filter -D INPUT 3
- 命令语法:iptables -t 表名 -F 链名
- 示例:iptables -t filter -F INPUT
- 命令语法:iptables -t 表名 -F
- 示例:iptables -t filter -F
- #修改规则
- 命令语法:iptables -t 表名 -R 链名 规则序号 规则原本的匹配条件 -j 动作
- 示例:iptables -t filter -R INPUT 3 -s 192.168.1.146 -j ACCEPT
- 命令语法:iptables -t 表名 -P 链名 动作
- 示例:iptables -t filter -P FORWARD ACCEPT
(3)匹配命令:
- iptables -t filter -I INPUT -s 192.168.1.111,192.168.1.118 -j DROP
- iptables -t filter -I INPUT -s 192.168.1.0/24 -j ACCEPT
- iptables -t filter -I INPUT ! -s 192.168.1.0/24 -j ACCEPT (!地址取反)
-o用于匹配报文将要从哪个网卡接口流出本机,于匹配条件只是用于匹配报文流出的网卡,所以在INPUT链与PREROUTING链中不能使用此选项。
- #示例如下
- iptables -t filter -I OUTPUT -p icmp -o eth4 -j DROP
- iptables -t filter -I OUTPUT -p icmp ! -o eth4 -j DROP
-p tcp -m multiport --sports 用于匹配报文的源端口,可以指定离散的多个端口号,端口之间用"逗号"隔开
-p udp -m multiport --dports 用于匹配报文的目标端口,可以指定离散的多个端口号,端口之间用"逗号"隔开
- iptables -t filter -I OUTPUT -d 192.168.1.146 -p udp -m multiport --sports 137,138 -j REJECT
- iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m multiport --dports 22,80 -j REJECT
- iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m multiport ! --dports 22,80 -j REJECT
- iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m multiport --dports 80:88 -j REJECT
- iptables -t filter -I INPUT -s 192.168.1.146 -p tcp -m multiport --dports 22,80:88 -j REJECT
使用iprange扩展模块可以指定"一段连续的IP地址范围",用于匹配报文的源地址或者目标地址。
iprange扩展模块中有两个扩展匹配条件可以使用
- iptables -t filter -A INPUT -m iprange --src-range 192.168.122.200-192.168.122.254 -j DROP
使用connlimit扩展模块,可以限制每个IP地址同时链接到server端的链接数量,注意:我们不用指定IP,其默认就是针对"每个客户端IP",即对单IP的并发连接数限制。
比如,我们想要限制,每个IP地址最多只能占用两个ssh链接远程到server端,我们则可以进行如下限制。
- [root@node-101 ~]#iptables -t filter -A INPUT -p tcp --dport 22 -m connlimit --connlimit-above 2 -j REJECT
- [root@node-103 ~]#ssh 192.168.122.101
- ssh: connect to host 192.168.122.101 port 22: Connection refused //第三个连接,被拒绝
对于state模块而言的"连接"并不能与tcp的"连接"画等号,在TCP/IP协议簇中,UDP和ICMP是没有所谓的连接的,但是对于state模块来说,tcp报文、udp报文、icmp报文都是有连接状态的,我们可以这样认为,对于state模块而言,只要两台机器在"你来我往"的通信,就算建立起了连接
对于state模块的连接而言,“连接"其中的报文可以分为5种状态,报文状态可以为NEW、ESTABLISHED、RELATED、INVALID、UNTRACKED
针对tcp连接,实现所有的外来访问都被拒绝,主动访问的除外。
(4)自定义链
为了举例我们添加两条规则给IN_WEB,接下来可以使用该自定义链IN_WEB
- [root@node-101 ~]#iptables -t filter -A IN_WEB -p tcp --dport 80 -j ACCEPT #定义IN_WEB第一条规则
- [root@node-101 ~]#iptables -t filter -A IN_WEB -p tcp --dport 443 -j ACCEPT #定义IN_WEB第二条规则
- [root@node-101 ~]#iptables -t filter -A INPUT -s 192.168.122.103 -j IN_WEB #使用自定义连IN_WEB
- [root@node-101 ~]#iptabels -t filter -I INPUT -s 172.18.43.4 -j ACCEPT #把管理机ip加入允许
- [root@node-101 ~]#iptables -t filter -A INPUT -j REJECT #决绝其他所有
- [root@node-103 ~]#curl 192.168.122.101 #101在允许规则,可以访问
- hello world
- [root@node-102 ~]#curl 192.168.122.101
- curl: (7) Failed connect to 192.168.122.101:80; Connection refused #101没有在允许规则,所以被拒绝
网络防火墙
网络防火墙往往处于网络的入口或者边缘,那么,如果想要使用iptables充当网络防火墙,iptables所在的主机则需要处于网络入口处。iptables的角色变为"网络防火墙"时,规则只能定义在FORWARD链中。
动作LOG(-j LOG)
使用LOG动作,可以将符合条件的报文的相关信息记录到日志中,但当前报文具体是被"接受",还是被"拒绝",都由后面的规则控制,换句话说,LOG动作只负责记录匹配到的报文的相关信息,不负责对报文的其他处理,如果想要对报文进行进一步的处理,可以在之后设置具体规则,进行进一步的处理
在使用LOG动作时,匹配条件应该尽量写的精确一些,匹配到的报文数量也会大幅度的减少,这样冗余的日志信息就会变少,同时日后分析日志时,日志中的信息可用程度更高
其他动作:ACCEPT、REJECT、DROP、REDIRECT、MASQUERADE、LOG、DNAT、SNAT、MIRROR、QUEUE、RETURN、MARK
SNAT和DNAT
动作target:SNAT、DNAT、MASQUERADE、REDIRECT
(1)SNAT
SNAT策略只能用在nat表的POSTROUTING链中,MASQUERADE会动态的将源地址转换为可用的IP地址,其实与SNAT实现的功能完全一致,都是修改源地址,只不过SNAT需要指明将报文的源地址改为哪个IP,而MASQUERADE则不用指定明确的IP,会动态的将报文的源地址修改为指定网卡上可用的IP地址
MASQUERADE:
-o wan1 -j MASQUERADE
(2)DNAT
就是指数据包从网卡发送出去的时候,修改数据包中的目的IP(应该是OUTPUT链),或者数据包进来修改目的IP(PREROUTING链)
REDIRECT
使用REDIRECT动作可以在本机上进行端口映射
比如,将本机的80端口映射到本机的8080端口上
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080
REDIRECT规则只能定义在PREROUTING链或者OUTPUT链中。
目标地址转换要做在到达网卡之前进行转换,所以DNAT只能用在nat表的PREROUTING链和OUTPUT链中
conntrack
Conntrack状态表和NAT
如上一节所述,列出的答复元组包含NAT信息。可以过滤输出以仅显示应用了源或目标nat的条目。这样可以查看在给定流中哪种类型的NAT转换处于活动状态。“sudo conntrack -L -p tcp –src-nat”可能显示以下内容:
tcp 6 114 TIME_WAIT src=10.0.0.10 dst=10.8.2.12 sport=5536 dport=80 src=10.8.2.12 dst=192.168.1.2 sport=80 dport=5536 [ASSURED]
此项显示从10.0.0.10:5536到10.8.2.12:80的连接。但是,与前面的示例不同,答复方向不仅是原始的反向方向:源地址已更改。目标主机(10.8.2.12)将答复数据包发送到192.168.1.2,而不是10.0.0.10。每当10.0.0.10发送另一个数据包时,具有此条目的路由器将源地址替换为192.168.1.2。当10.8.2.12发送答复时,它将目的地更改回10.0.0.10。此源NAT是由于nft假装规则所致:
inet nat postrouting meta oifname "veth0" masquerade
其他类型的NAT规则,例如“dnat to”或“redirect to”,将以类似的方式显示,其回复元组的目的地不同于原始的。
/proc/net/nf_conntrack或者conntract -L,可以看到类似如下的内容。
先说下每行entry的意思,第1列是网络层协议名称;第2列是协议代号;第3列代表传输层协议名称;第4列是传输层协议代号,其中tcp是6,udp是17;第5列的117指的是TTL;第6列是TCP的状态;第7列是表示源目地址以及对应端口;第8列是表示是否收到回应包;第9列表示期望收到的回包的源目地址及端口。
ipv4 2 tcp 6 117 SYN_SENT src=192.168.1.5 dst=192.168.1.7 sport=1031 dport=23 [UNREPLIED] src=192.168.1.7 dst=192.168.1.5 sport=23 dport=1031 use=1
1
第1次握手时(SYN),TCP的状态是SYN_SENT,且有 [UNREPLIED] 标记,意味着还没有收到回包。此时,连接是NEW的状态,这点很好理解。
UDP没有SYN_SENT这种TCP特有的状态标签,但是有 [UNREPLIED] 的标识,说明这个包是初次发出的包,还没有收到回应。此时conntrack中对应的状态是NEW,
ipv4 2 udp 17 170 src=192.168.1.2 dst=192.168.1.5 sport=137 dport=1025 src=192.168.1.5 dst=192.168.1.2 sport=1025 dport=137 [ASSURED] use=1
1
同样还是没有标识,但是 [UNREPLIED] 变成了 [ASSURED] ,表明已经收到了回包,且连接建立完成。另外TTL变成了170,这时由于在该连接状态下,默认的TTL是180,而第一次连接时默认值是30。此时conntrack中定义的状态是ESTABLISHED
问题
在 nat 表中,为了保证双向的流量都能正常完成地址替换,会跟踪并且记录链接状态。每一条连接都会有对应的记录生成。使用以下两个命令可以查看(SNAT相互转换)
# conntrack -L
# cat /proc/net/ip_conntrack
1.路由器上端设备访问下端设备添加静态路由就行了(需测试)。(其实上端设备找不到下端设备,因为走默认路由出去了,所以只需要在上端路由器添加一个静态路由到下端设备就行了(也就是说数据包来到设备后,经过preroute链后经过路由选择发到默认路由去了,并没有发到下端路由))
如下图:
具体访问流程:
下端到上端(S:10.8.0.21 D:192.168.5.97):pc经过路由器,会有SNAT,将原地址10.8.0.21转化为192.168.5.169,之后到达上端
上到下:添加静态路由,到达路由器,经过PREROUTING链后(没有DNAT),经过路由选择转发到下端设备(没DNAT那么怎么访问私有地址?TCP回应下端目的地址?其实DNAT和SNAT是没有联系的,SNAT后,数据包回复到PREROUTING后会自动转换(SNAT连接追踪)为内外IP和端口号)
2.路由和iptables的关系?先经过PREROUTING链,然后路由选择判断是转发还是INPUT。
3、(下端访问上端进行了SNAT)TCP回应下端目的地址是路由器,那怎么恢复到更下端的?
其实DNAT和SNAT是没有联系的,SNAT后,数据包回复到PREROUTING后会自动转换(SNAT连接追踪)为内外IP和端口号,然后通过路由转发到下端设备
在抓包看到源IP和目的IP正常,应该是应该层封装了这些地址信息
路由器作用snat,dnat没做是为了隔离这种情况?如果是,那么添加静态路由就不应该能访问?????
参考前面解析,
访问上层设备后,上层设备是怎么回应的?比如TCP连接
自动解析源IP对应的内网IP
12.iptables扩展模块
(1)ICMP
1.报文详解:
类型 type:占 1 个字节,表示较大范围类型分类的 ICMP 报文
代码 code:占 1 个字节,表示较小范围类型分类的 ICMP 报文(type的细分)
校验和 checksum:占 2 个字节,ICMP checksum 的计算方法类似于 IP checksum,但是不同的是 IP 只校验头部,ICMP 校验头部+数据部分
后面紧接的 ICMP 数据部分,根据前面的类型和代码字段的不同,具有不同的内容。
如果主机可达,对应主机会对我们的ping请求做出回应虽然ping请求报文与ping回应报文都属于ICMP类型的报文,但是如果在概念上细分的话,它们所属的类型还是不同的,我们发出的ping请求属于类型8的icmp报文,而对方主机的ping回应报文则属于类型0的icmp报文
例:
所有表示"目标不可达"的icmp报文的type码为3,而"目标不可达"又可以细分为多种情况,是网络不可达呢?还是主机不可达呢?再或者是端口不可达呢?所以,为了更加细化的区分它们,icmp对每种type又细分了对应的code,用不同的code对应具体的场景, 所以,我们可以使用type/code去匹配具体类型的ICMP报文,比如可以使用"3/1"表示主机不可达的icmp报文。
2.应用场景:
使用"-m icmp"表示使用icmp扩展,使用了"-p icmp",所以"-m icmp"可以省略
iptables -t filter -I INPUT -p icmp -m icmp --icmp-type 8/0 -j REJECT
使用"--icmp-type"选项表示根据具体的type与code去匹配对应的icmp报文,而上图中的"--icmp-type 8/0"表示icmp报文的type为8,code为0才会被匹配到,也就是只有ping请求类型的报文才能被匹配到,所以,别人对我们发起的ping请求将会被拒绝通过防火墙,而我们之所以能够ping通别人,是因为别人回应我们的报文的icmp type为0,code也为0,所以无法被上述规则匹配到,所以我们可以看到别人回应我们的信息。
(type为8的类型下只有一个code为0的类型,所以我们可以省略对应的code)
不知道type和code的情况下 我们可以用icmp报文的描述名称去匹配对应类型的报文
iptables -t filter -I INPUT -p icmp --icmp-type "echo-request" -j REJECT
(3)实例
iptables -t mangle -A ICMP-MARK-MATCH -p icmp --icmp-type echo-request -w -s %s -d %s -m string --string \"mark%x\" --algo bm -j MARK --set-mark 0x%x"
--wait-w[秒]放弃前获取xtables锁的最大等待时间--wait interval-w[usecs]尝试获取xtables-lock的等待时间默认为1秒
(2)string
string 模块,可以指定要匹配的字符串,如果报文中包含对应的字符串,则符合匹配条件。
--algo 【bm | kmp】∶字符匹配的查询算法
--string pattern∶字符匹配的字符串
(3)mark
给特定的数据包打上标记,配合TC做【QOS流量限制】 或 【策略路由实现】。
· -j MARK //-j代表动作这里代表要执行mark操作(CONNMARK和MARK,SECMARK)
· -m mark //-m代表匹配mark
· –mark xxx/yyy //xxx代表要匹配的mark的值,yyy代表掩码,如果要完全匹配可以省略掉掩码,不过一般不会直接整个匹配,因为可能一个系统中很多模块都需要打mark,所以一般用掩码来取得某几位
-j 标记
-j MARK 标记数据包
--set-mark #标记数据包
-j CNNMARK 标记链接
--set-mark #标记链接
--save-mark #保存数据包中的MARK到链接中
--restore-mark #将链接中的MARK设置到同一链接的其它数据包中
示例
# 数据包标记为50
iptables -t mangle -A PREROUTING -j MARK --set-mark 50
# 匹配标记为50的数据包,并保存数据包中的标记设置到链接中
iptables -t mangle -A PREROUTING -m mark --mark 50 -j CONNMARK --save-mark
# 链接标记为50
iptables -t mangle -A PREROUTING -j CONNMARK --set-mark 50
# 匹配链接标记为50数据包,并将链接中的标记设置到数据包中
iptables -t mangle -A PREROUTING -m connmark --mark 50 -j CONNMARK --restore-mark
13.不同局域网电脑如何互相访问
- 如果你有公网IP的话,上路由器做端口映射就可以。
- 但如果你没有公网IP的话,可以做内网穿透,来映射本地端口,一样可以实现不同局域网电脑之间的访问。
14.USB
(1)/sys/kernel/debug/usb/devices信息详解
(54条消息) USB-详解/sys/kernel/debug/usb/devices_华佗hans的博客-CSDN博客
- T: Bus=01 Lev=00 Prnt=00 Port=00 Cnt=00 Dev#= 1 Spd=480 MxCh= 1
- B: Alloc= 0/800 us ( 0%), #Int= 0, #Iso= 0
- D: Ver= 2.00 Cls=09(hub ) Sub=00 Prot=01 MxPS=64 #Cfgs= 1
- P: Vendor=1d6b ProdID=0002 Rev= 4.04
- S: Manufacturer=Linux 4.4.60 xhci-hcd
- S: Product=xHCI Host Controller
- S: SerialNumber=xhci-hcd.0.auto
- C:* #Ifs= 1 Cfg#= 1 Atr=e0 MxPwr= 0mA
- I:* If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub
- E: Ad=81(I) Atr=03(Int.) MxPS= 4 Ivl=256ms
T是拓扑结构,Lev就是该设备位于拓扑结构中的层次level,XHCI控制器对应的Lev=00,其下面挂接的USB网卡的Lev=01。Prnt是父设备的设备号(root hub没有父设备,这里固定用0)。Port是设备所挂接hub的端口。对于root hub,Port值固定用0,对于其余设备,Port值用物理端口号减1(物理端口号一般是从1开始计数,减1之后就是从0开始)。Cnt的一般说法是这个level上设备的总数,但这其实是不严谨的。对于root hub,Cnt的值固定为0,对于其余设备,Cnt值更像是一个同level内不同设备的序号(从1开始),因此只有同level内最后一个设备的Cnt值,才可以看做是该level上设备的总数。Dev是设备号,Spd是速度,MxCh是支持的最多子设备数(即hub设备包含的端口数(自己有多少个下行端口),非hub设备这个值为0)。
B是带宽信息,只有root hub有这一行。Alloc后的三个数值分别表示分配到的带宽,最大带宽及两者之比。这里的统计是以1毫秒为单位,一般高速或超速设备,最大带宽是预留80%,即800us,低速设备是预留90%。Int是中断请求次数,Iso是同步传输请求次数。
D是设备描述符,Ver:USB协议版本,比如Ver=3.00。Cls:由USB-IF(USB Implementers Forum)分配的设备类类码,Hub对应09;厂家自定义的为ff;如果该字段为0x00,表示由接口描述符bInterfaceClass来指定。其后的P,S信息也来自设备描述符。Prot是协议Protocol。MxPS是端点0一次可以处理的最大字节数。Cfgs是配置数。
C是配置描述符,带星号表示是当前生效的配置。Ifs是接口数目,Cfg是配置编号。
I是接口描述符,注意,这里的一行表示接口的一个设置(setting),带星号表示是该接口当前生效的设置(setting)。因为多个接口可以同时生效,因此就没有当前生效的接口这种说法。If是接口编号,Alt是设置(setting)编号,EPs是端点数。
E是端点描述符。
例:
T: Bus=00 Lev=00 Prnt=00 Port=00 Cnt=00 Dev#= 1 Spd=12 MxCh= 2
T: Bus=00 Lev=01 Prnt=01 Port=00 Cnt=01 Dev#= 2 Spd=12 MxCh= 4 接在父设备的0号端口上, 而且自己有4个下行端口
I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub
T: Bus=00 Lev=02 Prnt=02 Port=00 Cnt=01 Dev#= 3 Spd=1.5 MxCh= 0
I: If#= 0 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=02 Driver=mouse
T: Bus=00 Lev=02 Prnt=02 Port=02 Cnt=02 Dev#= 4 Spd=12 MxCh= 0 接在父设备的2号端口上,自己没有下行端口
I: If#= 0 Alt= 0 #EPs= 3 Cls=00(>ifc ) Sub=00 Prot=00 Driver=serial
得出拓扑:
+------------------+
| PC/root_hub (12)| Dev# = 1
+------------------+ (nn) is Mbps.
Level 0 | CN.0 | CN.1 | [CN = connector/port #]
+------------------+
/
/
+-----------------------+
Level 1 | Dev#2: 4-port hub (12)|
+-----------------------+
|CN.0 |CN.1 |CN.2 |CN.3 |
+-----------------------+
\ \____________________
\_____ \
\ \
+--------------------+ +--------------------+
Level 2 | Dev# 3: mouse (1.5)| | Dev# 4: serial (12)|
+--------------------+ +--------------------+
(2)/sys/bus/usb/devices/系统总线目录
Devptah会根据多层级联hub的情况而怎么变动
总线号-端口号:配置号.接口号:
其中 usbx/第x个总线,x-y:a.b/的目录格式,x表示总线号,y表示端口,a表示配置,b表示接口。
usb1/usb2/表示计算机上接了2条usb总线,即2个usb主机控制器,
2-0:1.0表示什么?2表示是2号总线,或者说2号Root Hub,0就是这里我们说的devpath,1表示配置为1号,0表示接口号为0。也即是说,2号总线的0号端口的设备,使用的是1号配置,接口号为0。那么devpath是否就是端口号呢?显然不是,这里我列出来的这个例子是只有Root Hub没有级联Hub的情况,如果在Root Hub上又接了别的Hub,然后一级一级连下去,子又生孙,孙又生子,子又有子,子又有孙。如何在sysfs里面来表征这整个大家族呢?这就是devpath的作用,顶级的设备其devpath就是其连在Root Hub上的端口号,而次级的设备就是其父Hub的devpath后面加上其端口号,即如果2-0:1.0如果是一个Hub,那么它下面的1号端口的设备就可以是2-0.1:1.0,2号端口的设备就可以是2-0.2:1.0,3号端口就可以是4-0.3:1.0。总的来说,就是端口号一级一级往下加。
另一个解释:
X-Y.Z:A.B
每个字段标识设备的连接点。前两个字段是必填字段:
- X 是连接 USB 系统的主板的 USB 总线。
- Y 是总线系统上使用的端口
所以用字符串标识的USB设备是连接在总线3的端口3上的设备。3-3
如果连接 USB 集线器,则会扩展单个 USB 端口的连接功能。Linux 内核通过附加 Z 字段来识别这种情况。
- Z 是在集线器上使用的端口
因此,用字符串标识的USB设备是连接在总线1的端口2上的集线器的端口5上的设备。1-2.5
USB 规范允许您级联连接多个 USB 集线器,因此 Linux 内核继续附加不同集线器上使用的端口。因此,用字符串标识的 USB 设备是连接在集线器的端口 1 上的设备,连接到连接到总线 1 的端口 2 的集线器的端口 1。1-2.1.1
所以:在设备中,USB控制器只有一个端口(有多个,只是固定为1端口),所以下面级联多少个hub都是连接在总线端口1上的,所以1-1是固定的。(不一定,如果下端连接hub,就可能不是1了)
(3)总结
总线接口:最底层的物理实体,以USB接口控制器为核心,USB发送和接收数据的接口
15.ebtables
C语言
1.指针和数组
(1)指针数组
:是一个数组,该数组用来存放指针
如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:
dataType *arrayName[length];
[ ]的优先级高于*,该定义形式应该理解为:
dataType *(arrayName[length]);
除了每个元素的数据类型不同,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子:
#include <stdio.h>
int main(){
int a = 16, b = 932, c = 100;
//定义一个指针数组
int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]
//定义一个指向指针数组的指针
int **parr = arr;
printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
return 0;
}
运行结果:
16, 932, 100
16, 932, 100
数组指针:指向数组的指针
数组指针相当于建立了一个名为p的数组(当然他叫数组形式的指针),这个数组每个值都是其他几个数组的开头地址的值,所以p本身是这个数学的首地址用*p提取其他几个数组的首地址。因此用* *p才能提取其他几个数组内部的相应值。
int temp[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &temp;
printf("%d\n", *(p + i));(直接地址加了整个数组的长度)
p是指向数组的,存的数组的地址,*p是指向数组首地址,printf("%d\n", *(*p + i));
//或者 printf("%d\n", (*p)[i]);
(2)函数指针
用typedef就行了
- int Func(int x); /*声明一个函数*/
- int (*p) (int x); /*定义一个函数指针*/
- p = Func; /*将Func函数的首地址赋给指针变量p*
2.链表
单向链表:只有一个指向下一个节点的指针。 优点:单向链表增加删除节点简单。遍历时候不会死循环;
缺点:只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进。 适用于节点的增加删除。
双向链表:有两个指针,一个指向前一个节点,一个指向后一个节点。 优点:可以找到前驱和后继,可进可退;
缺点:增加删除节点复杂,需要多分配一个指针存储空间。 适用于需要双向查找节点值的情况
1.单向链表
Fixme:主要要有一个表示尾节点的。(首节点,尾节点,插入节点)
1.单向链表的创建(初始化)
创建一个链表需要做如下工作: 1. 声明一个头指针(如果有必要,可以声明一个头节点); 2. 创建多个存储数据的节点,在创建的过程中,要随时与其前驱节点建立逻辑关系;
例如,创建一个存储{1,2,3,4 }且无头节点的链表,C 语言实现代码如下:
linklist * initlinklist(){
linklist * p=NULL;//创建头指针 linklist * temp = (linklist*)malloc(sizeof(linklist));//创建首元节点 //首元节点先初始化 temp->elem = 1;
temp->next = NULL;
p = temp;//头指针指向首元节点 //从第二个节点开始创建 for (int i=2; i<5; i++) {
//创建一个新节点并初始化 linklist *a=(linklist*)malloc(sizeof(linklist));
a->elem=i;
a->next=NULL;
//将temp节点与新建立的a节点建立逻辑关系 temp->next=a;
//指针temp每次都指向新链表的最后一个节点,其实就是 a节点,这里写temp=a也对 temp=temp->next;
}
//返回建立的节点,只返回头指针 p即可,通过头指针即可找到整个链表 return p;}
如果想创建一个存储{1,2,3,4}且含头节点的链表,则 C 语言实现代码为:
linklist * initlinklist(){
linklist * p=(linklist*)malloc(sizeof(linklist));//创建一个头结点 linklist * temp=p;//声明一个指针指向头结点, //生成链表 for (int i=1; i<5; i++) {
linklist *a=(linklist*)malloc(sizeof(linklist));
a->elem=i;
a->next=NULL;
temp->next=a;
temp=temp->next;
}
return p;}
2.双向链表
3.细节基础
1."#"和"##"的用法
使用#把宏参数变为一个字符串,用##把两个宏参数贴合在一起.
注意事项
当宏参数是另一个宏的时候,需要注意的是凡宏定义里有用’#’或’##’的地方宏参数是不会再展开.
即, 只有当前宏生效, 参数里的宏!不!会!生!效 !!!!
举例
#define A (2)
#define STR(s) #s
#define CONS(a,b) int(a##e##b)
printf("int max: %s\n", STR(INT_MAX)); // INT_MAX #include<climits>
printf("%s\n", CONS(A, A)); // compile error --- int(AeA)
两句print会被展开为:
printf("int max: %s\n","INT_MAX");
printf("%s\n", int(AeA));
分析:
由于A和INT_MAX均是宏,且作为宏CONS和STR的参数,并且宏CONS和STR中均含有#或者##符号,所以A和INT_MAX均不能被解引用。导致不符合预期的情况出现。
3.2 解决方案
(转换宏就是先把一层宏代换后,再使用##)
解决这个问题的方法很简单. 加多一层中间转换宏. 加这层宏的用意是把所有宏的参数在这层里全部展开,
那么在转换宏里的那一个宏(_STR)就能得到正确的宏参数.
#define A (2)
#define _STR(s) #s
#define STR(s) _STR(s) // 转换宏
#define _CONS(a,b) int(a##e##b)
#define CONS(a,b) _CONS(a,b) // 转换宏
结果:
printf("int max: %s\n",STR(INT_MAX));
//输出为: int max:0x7fffffff
//STR(INT_MAX) --> _STR(0x7fffffff) 然后再转换成字符串;
printf("%d\n", CONS(A, A));
//输出为:200
//CONS(A, A) --> _CONS((2), (2)) --> int((2)e(2))
2.可变参数的宏
对于__VA_ARGS__ 的缺点,使用 ##__VA_ARGS__ 直接完美解决!##__VA_ARGS__ 使用如下:
#define edebug(format, ...) fprintf (stderr, format, ##__VA_ARGS__)
- 如果可变参数被忽略或为空,## 操作将使预处理器(preprocessor)去除掉它前面的那个逗号.
- 如果你在宏调用时,确实提供了一些可变参数,GNU CPP 也会工作正常,它会把这些可变参数放到逗号的后面。
- 用法
#define PRINT_PAIR(...) printf("A. <x, y>=<%d,%d>\n", __VA_ARGS__)
#define PRINT_SELF(...) printf(__VA_ARGS__)
#define DEBUG_1(args...) printf(args)
#define DEBUG_2(fmt,args...) printf(fmt,args)
#define DEBUG_3(fmt,args...) printf(fmt,##args)
- 自己定义
注:使用:#define DEBUG_3(fmt,args...) printf(fmt,##args)
也可避免逗号问题
3.格式化输出
在scanf中*表示这个位置占位符对应的输入将被忽略。
在printf中*表示用后面的值替代*的位置
1.常规----格式化输出
%d //整型输出;;; 注:按格式输出几位数,
%ld //长整型输出
%o //以八进制数形式输出整数
%x //以十六进制数形式输出整数,或输出字符串的地址
%u //以十进制数输出unsigned型数据(无符号数)
注意: %d与%u的区别是,有无符号的数值范围不同,也就是极限的值不同,不然数值打印出来会有误
%c //用来输出一个字符
%s //用来输出一个字符串
%f //用来输出实数,以小数形式输出,默认情况下保留小数点6位
%.5f //用来输出实数,保留小数点5位
%e //以指数形式输出实数
%g //根据大小自动选f格式或e格式,且不输出无意义的零
特殊----格式化输出
一.整数的格式化取值
int main() {
int a = 123, b = 123456;
printf("%5d\n", a); // 默认右对齐,且最少取5位整数,多余5位全取,不足5位使用空格左面补全
printf("%05d\n", a); // 使用0代替空格,在左边补齐位数
printf("%-5d\n", a); // 左对齐,不足位数,使用空格补全
printf("%-05d\n", a); // 左对齐,不足位数,还是用空格补全
printf("%5d\n", b); // 超过5位全取
scanf("%3d%*3d%3d", &i, &j); //中间数字3位数值被跳过,称为假读或空读
}
二.小数的格式化取值
#include <stdio.h>
int main() {
double a = 123.326, b = 90.12;
printf("%.2f\n", a); // 取2位小数,且第三位四色五入
printf("%.3f\n", b); // 取3位小数,且不足的用0补全
printf("%.*f",n,a) //可以实现自定义保留小数位数
printf("%4.2f\n", b); // 取至少4位字符,2位从b的左边开始取2位,剩余2位<整数2位+小数点1位,所以3位全部输出
printf("%7.2f\n", b); // 取至少7位字符, 2位小数,剩余5位>整数2位 + 小数点1位, 多出来的2位用空格补全
三.字符数组的格式化取值
#include <stdio.h>
#include <time.h>
int main() {
char str[30];
char s1[5] = { 'a', 'b', 'c' };
printf("%s==\n", s1); // 打印完整的字符数组(字符串)
printf("%2s==\n", s1); // 打印至少2个字符
printf("%5s==\n", s1); // 打印至少5个字符,不足的用空格在左边补齐
printf("%-5s==\n", s1); // 打印至少5个字符,不足的用空格在右边补齐
printf("%4.2s==\n", s1); // 总共输出4个字符,但是有2个需要在s1里面从左取,剩余的字符用空格默认在左边补全
printf("%.2s==\n", s1); // 总共输出2个字符,这2个字符从s1里面的左边开始取
printf("%.*s", max, string); // 此处 * 表示宽度或精度,由后面参数 max 确定(且该参数必须为 int 类型)
4.静态局部变量
因为静态局部变量是在编译时赋初值的,且只赋初值一次,在程序运行时它已有初值。以后在每次调用函数时就不再重新赋初值,而是保留上次函数调用结束时的值。
通过上面的示例,我们可以得出静态局部变量一般的使用场景,如下所示:
- 需要保留函数上一次调用结束时的值。
- 如果初始化后,变量只会被引用而不会改变其值,则这时用静态局部变量比较方便,以免每次调用时重新赋值。
默认初始化为 0
在静态数据区,内存中所有的字节默认值都是 0x00。静态变量与全局变量也一样,它们都存储在静态数据区中,因此其变量的值默认也为 0。
5.进制与bit与字节
一位16进制表示4个bit
一个字节8个bit,2的8 次方255
所以一个字符由2位十六进制表示
编码一个汉字占2-4个字节,比如utf-8:3个字节,所以使用6位十六进制表示
4.具体函数使用
(1)fcntl函数(已打开的文件描述符进行操作)
函数原型:
- #include<unistd.h>
- #include<fcntl.h>
- int fcntl(int fd, int cmd);
- int fcntl(int fd, int cmd, long arg);
- int fcntl(int fd, int cmd ,struct flock* lock);
fcntl的返回值: 与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
fcntl函数功能依据cmd的值的不同而不同。参数对应功能如下:
fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
fcntl(trace_fd, F_SETFD, FD_CLOEXEC);将文件描述符close-on-exec标志设置
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
注意: 在修改文件描述符标志或文件状态标志时必须谨慎,先要取得现在的标志值,然后按照希望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。
文件的状态属性,它们由open的flags参数指明。分类:
访问方式标志:指明允许文件标志符用于读、写或两者兼之,包括O_RDONLY、O_WRONLY和O_RDWR。
打开时标志:指明打开文件时影响open行为的一些选项。
这些选项除了O_NONBLOCK其他的一旦文件打开就不再保留,因为O_NONBLOCK同时也是一个I/O操作方式,故此标志被保留。
O_CREAT:若设置,当该文件不存在时创建并打开此文件。
O_EXCL:若O_CREAT和O_EXCL同时设置,当指定的文件已经存在时open失败。保证不破坏已存在的文件。
O_TRUNC:截断文件为零长度,这一选项只对普通文件有用,对诸如目录或FIFO之类的特殊文件无用。
O_NONBLOCK:防止为打开文件而阻塞很长时间。这通常仅对设备、网络、管道文件才有意义。此标志同时也作为I/O操作方式标志,这意味着在open中指明O_NONBLOCK就同时设置了非阻塞I/O方式。因此要非阻塞地打开一个文件且不影响正常的阻塞I/O,必须先设置O_NONBLOCK调用open,然后调用fcntl关闭此位。
I/O操作方式标志:使用fd读/写的工作方式。这些标志由open()设置,之后可以用fcntl()获取和改变。
O_APPEND:用于追加写。若此位设置,所有write()操作写数据至文件尾而不管文件位置在何处。这是附加数据至文件尾唯一可靠的方法。用附加方式可以保证无论是否有其他进程正在写同一个文件,write()操作总是将数据写在当前文件尾。相反,在未设置此位的情况下,如果通过简单地移动文件位置到文件尾,然后再写数据,则在设置文件位置之后开始写之前,可能有其他进程扩展此文件(对应于两个不同的进程打开同一个文件的情形,它们共享同一个vnode,但各自有自己的系统打开文件表,因而有自己的文件位置),从而导致所写的数据出现在实际文件尾之前的某个地方。
O_NONBLOCK:用于非阻塞I/O。
O_ASYNC:用于信号驱动的I/O(异步I/O)。若此位设置,当文件标志符中有输入数据时会生成SIGIO信号。
O_SYNC:用于同步I/O。若此位设置,文件按同步I/O方式打开,并将导致任何写该文件的操作都阻塞调用进程直至内核I/O缓冲区的数据以及与此次写有关的文件属性已全部写至物理存储介质。
O_DSYNC:用于同步数据I/O。若此位设置,文件按同步I/O方式打开,并将导致任何写该文件的操作都阻塞调用进程直至内核I/O缓冲区的数据已全部写至物理存储介质。但如果所写的数据不影响读刚写入的数据,则不等待文件属性更新。
O_RSYNC:若此位设置,文件按同步I/O方式打开,并将导致任何读该文件的操作都将等待所有写入同一区域的写操作按O_DSYNC和O_SYNC完成后再进行。如果同时设置了O_SYNC 和 O_RSYNC标志,则读操作将阻塞直到文件的访问时间属性已写至物理存储介质。如果同时设置了O_DSYNC 和 O_RSYNC标志,则读操作将阻塞直到所有与保持文件完整性有关的数据都已写至物理存储介质。
简单地说,O_SYNC、O_DSYNC和O_RSYNC这几个标志的主要作用是使数据直接写到磁盘或直接从磁盘读入。
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
文件锁也被称为记录所,文件锁如果深讲的话,内容不少(比如文件锁最起码分为了建议锁和强制性锁,暂时挖坑,后面填)。
(2)fcntl文件锁作用
顾名思义,就是用来保护文件数据的。当多个进程共享读写同一个文件时,为了不让进程们各自读写数据时相互干扰,我们可以使用进程信号量来互斥实现,除了可以使用进程信号量以外,还可以使用我们本小节要讲的“文件锁”来实现,而且功能更丰富,使用起来相对还更容易些
高级IO——文件锁 - 克拉默与矩阵 - 博客园 (cnblogs.com)
(3)popen()函数执行命令输出
- #include<stdio.h>
- int main()
- {
- FILE *fp = NULL;
- char data[100] = {'0'};
- fp = popen("ls", "r");
- if (fp == NULL)
- {
- printf("popen error!\n");
- return 1;
- }
- while (fgets(data, sizeof(data), fp) != NULL)
- {
- printf("%s", data);
- }
- pclose(fp);
- return 0;
- }
(4)strstr() 函数的声明。
描述
C 库函数 char *strstr(const char *haystack, const char *needle) 在字符串 haystack 中查找第一次出现字符串 needle 的位置,不包含终止符 '\0'。
char *strstr(const char *haystack, const char *needle)
参数
haystack -- 要被检索的 C 字符串。
needle -- 在 haystack 字符串内要搜索的小字符串。
返回值
该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。
(5)fgets() 函数的声明。
描述
C 库函数 char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
char *fgets(char *str, int n, FILE *stream)
参数
str -- 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
n -- 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
返回值
如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。
如果发生错误,返回一个空指针。
(6)strtok() 函数的声明。
描述
C 库函数 char *strtok(char *str, const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。
char *strtok(char *str, const char *delim)
参数
str -- 要被分解成一组小字符串的字符串。
delim -- 包含分隔符的 C 字符串。
返回值
该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针
(7)strrchr()
C 标准库 - <string.h> C 标准库 - <string.h>
描述
C 库函数 char *strrchr(const char *str, int c) 在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。
声明
下面是 strrchr() 函数的声明。
char *strrchr(const char *str, int c)
参数
str -- C 字符串。
c -- 要搜索的字符。以 int 形式传递,但是最终会转换回 char 形式。
返回值
该函数返回 str 中最后一次出现字符 c 的位置。如果未找到该值,则函数返回一个空指针。
// raw string: "11:25:52 up 18:31, load average: 0.02, 0.11, 0.10"
ptr = strrchr(buff, ':') + 1; // jump ':'
LOG_DB("cpu usage: %s", ptr);
Ptr指针直接指向字符串指定位置,向后输出。
(8)sscanf函数用法详解
sscanf()会将参数str的字符串根据参数format字符串来转换并格式化数据。格式转换形式请参考scanf()。转换后的结果存于对应的参数内。
返回值 成功则返回参数数目,失败则返回-1,错误原因存于errno中。 返回0表示失败 否则,表示正确格式化数据的个数 例如:sscanf(str,"%d%d%s", &i,&i2, &s); 如果三个变成都读入成功会返回3。 如果只读入了第一个整数到i则会返回1。证明无法从str读入第二个整数。
sscanf和scanf确实很类似,两者都是用于输入。只是后者以屏幕stidin为输入源,而前者是以字符串为输入源,仅此而已。
(9)recv、recvfrom、recvmsg函数
Recv收到所以来自fd的消息,通常用于连接的
Recvfrom接受具体的,通常用于未连接的或者连接的
Recvmsg则可以控制接受类型
(10)fgets和fputs、fread和fwrite、fscanf和fprintf对fp操作写入或读出
fgets函数用来从流中读取字符串。文件的指针会偏移至当前读取完的这个字符之后的位置。
fgets函数的调用形式如下:fgets(str,n,fp);此处,fp是文件指针;str是存放在字符串的起始地址;n是一个int类型变量。函数的功能是从fp所指文件中读入n-1个字符放入str为起始地址的空间内;如果在未读满n-1个字符之时,已读到一个换行符或一个EOF(文件结束标志),则结束本次读操作,读入的字符串中最后包含读到的换行符。因此,确切地说,调用fgets函数时,最多只能读入n-1个字符。读入结束后,系统将自动在最后加'\0',并以str作为函数值返回。
fputs把字符串写入到指定的流( stream) 中,但不包括空字符。
返回值:该函数返回一个非负值,如果发生错误则返回 EOF(-1)。
fscanf在一个流中进行格式化读取数据,fscanf遇到空格和换行时结束,注意空格时也结束。这与fgets有区别,fgets遇到空格不结束。
fprintf在一个流中进行格式化写入数据。
fread 函数用于从指定的文件中读取指定尺寸的数据
fwrite 函数用于将指定尺寸的数据写入到指定的文件中
(11)system函数返回值
-1:创建子进程失败
对于其它值,先用返回值除以256,商对应的含义如下:
0:命令运行成功
1:通用未知错误
2:误用shell命令
126:命令不可执行
127:没有找到命令
128:无效退出参数
130:命令通过Ctrl+C终止
255:退出状态码越界
看到WEXITSTATUS(status),就是command的返回值。当然前提条件是shell命令顺利执行完毕。即:WIFEXITED(status) ! =0
CPP
(54条消息) 《C++面向对象程序设计》✍千处细节、万字总结(建议收藏)_白鳯的博客-CSDN博客
一、面向对象程序设计对C的扩展
面向对象程序的基本元素是对象,面向对象程序的主要结构特点是:第一,程序一般由类的定义和类的使用两部分组成;第二,程序中的一切操作都是通过向对象发送消息来实现的,对象接收到消息后,启动有关方法完成相应的操作。
对象:描述其属性的数据以及对这些数据施加的一组操作封装在一起构成的统一体。对象可认为是数据+操作。
类:类是具有相同的数据和相同的操作的一组对象的集合。
消息传递:对象之间的交互。
**方法:**对象实现的行为称为方法。
面向对象程序设计的基本特征:抽象、封装、继承、多态。
1.const修饰符
在C语言中,习惯使用#define来定义常量,例如#define PI 3.14,C++提供了一种更灵活、更安全的方式来定义常量,即使用const修饰符来定义常量。例如const float PI = 3.14;
const可以与指针一起使用,它们的组合情况复杂,可归纳为3种:指向常量的指针、常指针和指向常量的常指针。
指向常量的指针:一个指向常量的指针变量。const char* pc = "abcd";
常指针:将指针变量所指的地址声明为常量。char* const pc = "abcd";
指向常量的常指针:都不能更改。 const char* const pc = "abcd";
说明:
如果用const定义整型常量,关键字可以省略。即 const in bufsize = 100 与 const bufsize = 100等价;
常量一旦被建立,在程序的任何地方都不能再更改。
与#define不同,const定义的常量可以有自己的数据类型。
函数参数也可以用const说明,用于保证实参在该函数内不被改动。
2.内联函数
Tip: 只有当函数只有 10 行甚至更少时才将其定义为内联函数.
为防止函数被频繁调用而出现入栈出栈带来的消耗
在函数名前冠以关键字inline,该函数就被声明为内联函数。每当程序中出现对该函数的调用时,C++编译器使用函数体中的代码插入到调用该函数的语句之处,同时使用实参代替形参,以便在程序运行时不再进行函数调用。引入内联函数主要是为了消除调用函数时的系统开销,以提高运行速度。
说明:
内联函数在第一次被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码
在内联函数体内一般不能含有复杂的控制语句,如for语句和switch语句等
使用内联函数是一种空间换时间的措施,若内联函数较长,较复杂且调用较为频繁时不建议使用
3.作用域标识符"::"
通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。
如果希望在局部变量的作用域内使用同名的全局变量,可以在该变量前加上“::”,此时::value代表全局变量value,“::”称为作用域标识符。
- #include <iostream>
- using namespace std;
- int value; //定义全局变量value
- int main()
- {
- int value; //定义局部变量value
- value = 100;
- ::value = 1000;
- cout << "local value : " << value << endl;
- cout << "global value : " << ::value << endl;
- return 0;
- }
4.new和delete运算符
程序运行时,计算机的内存被分为4个区:程序代码区、全局数据区、堆和栈。其中,堆可由用户分配和释放。C语言中使用函数malloc()和free()来进行动态内存管理。C++则提供了运算符new和delete来做同样的工作,而且后者比前者性能更优越,使用更灵活方便。
new/delete:这两个是C++中的关键字,若要使用,需要编译器支持;
malloc/free:这两个是库函数,若要使用则需要引入相应的头文件才可以正常使用。
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
5.引用
引用(reference)是C++对C的一个重要扩充。变量的引用就是变量的别名,因此引用又称别名。
引用与其所代表的变量共享同一内存单元,系统并不为引用另外分配存储空间。实际上,编译系统使引用和其代表的变量具有相同的地址。
int &j = i;
6.
二、类和对象
1.类的构成
类声明中的内容包括数据和函数,分别称为数据成员和成员函数。按访问权限划分,数据成员和成员函数又可分为共有、保护和私有3种。
一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。
- 若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。
- 不能在类声明中给数据成员赋初值。
类的声明:在类声明中只给出成员函数的原型,而将成员函数的定义放在类的外部。
2.对象定义和使用
(1)定义
和结构体一样
(2)类的成员访问
私有成员只能被类中的成员函数访问,不能在类的外部,通过类的对象进行访问。
一般来说,公有成员是类的对外接口,而私有成员是类的内部数据和内部实现,不希望外界访问。将类的成员划分为不同的访问级别有两个好处:一是信息隐蔽,即实现封装,将类的内部数据与内部实现和外部接口分开,这样使该类的外部程序不需要了解类的详细实现;二是数据保护,即将类的重要信息保护起来,以免其他程序进行不恰当的修改。
3.构造函数与析构函数
(1)构造函数
是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。构造函数的名字必须与类名相同,而不能由用户任意命名。它可以有任意类型的参数,但不能具有返回值。它不需要用户来调用,而是在建立对象时自动执行。
在声明类时,对数据成员的初始化工作一般在构造函数中用赋值语句进行。此外还可以用成员初始化列表实现对数据成员的初始化。
- 类名::构造函数名([参数表])[:(成员初始化列表)]
- {
- //构造函数体
- }
带默认参数的构造函数
- class Score{
- public:
- Score(int m = 0, int f = 0); //带默认参数的构造函数
- void setScore(int m, int f);
- void showScore();
- private:
- int mid_exam;
- int fin_exam;
- };
- Score::Score(int m, int f) : mid_exam(m), fin_exam(f)
- {
- cout << "构造函数使用中..." << endl;
- }
- void Score::setScore(int m, int f)
- {
- mid_exam = m;
- fin_exam = f;
- }
- void Score::showScore()
- {
- cout << "期中成绩: " << mid_exam << endl;
- cout << "期末成绩:" << fin_exam << endl;
- }
- int main()
- {
- Score op1(99, 100);
- Score op2(88);
- Score op3;
- op1.showScore();
- op2.showScore();
- op3.showScore();
- return 0;
- }
(2)析构函数
- 析构函数与构造函数名字相同,但它前面必须加一个波浪号(~)。
- 析构函数没有参数和返回值,也不能被重载,因此只有一个。
- 当撤销对象时,编译系统会自动调用析构函数。
(3)拷贝构造函数
拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用是在建立一个新对象时,使用一个已存在的对象去初始化这个新对象。
拷贝构造函数具有以下特点:
因为拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值。
拷贝构造函数只有一个参数,并且是同类对象的引用。
每个类都必须有一个拷贝构造函数。可以自己定义拷贝构造函数,用于按照需要初始化新对象;如果没有定义类的拷贝构造函数,系统就会自动生成一个默认拷贝构造函数,用于复制出与数据成员值完全相同的新对象。
- 类名::类名(const 类名 &对象名)
- {
- 拷贝构造函数的函数体;
- }
- Score(const Score &p); //拷贝构造函数
- 调用拷贝构造函数的一般形式为:
- 类名 对象2(对象1);
- 类名 对象2 = 对象1;
- Score sc1(98, 87);
- Score sc2(sc1); //调用拷贝构造函数
- Score sc3 = sc2; //调用拷贝构造函数
浅拷贝,就是由默认的拷贝构造函数所实现的数据成员逐一赋值。通常默认的拷贝构造函数是能够胜任此工作的,但若类中含有指针类型的数据,则这种按数据成员逐一赋值的方法会产生错误。
在析构函数释放stu1所指的内存后,再释放stu2所指的内存会发生错误,因为此内存空间已被释放。解决方法就是重定义拷贝构造函数,为其变量重新生成内存空间。
4.this指针
this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。
5.string类
string类的构造函数:【重要】
string(const char *s); //用c字符串s初始化
string(int n,char c); //用n个字符c初始化
此外,string类还支持默认构造函数和复制构造函数,如string s1;string s2="hello";都是正确的写法。当构造的string太长而无法表达时会异常
常用的string类运算符如下:
=、+、+=、==、!=、<、<=、>、>=、[](访问下标对应字符)、>>(输入)、<<(输出)
有很多函数
string类重载运算符operator>>用于输入,同样重载运算符operator<<用于输出操作。 函数getline(istream &in,string &s);用于从输入流in中读取字符串到s中,以换行符'\n'分开。
6.静态成员
说明:
静态数据成员的定义与普通数据成员相似,但前面要加上static关键字。
静态数据成员的初始化与普通数据成员不同。静态数据成员初始化应在类外单独进行,而且应在定义对象之前进行。一般在main()函数之前、类声明之后的特殊地带为它提供定义和初始化。
静态数据成员属于类(准确地说,是属于类中对象的集合),而不像普通数据成员那样属于某一对象,因此,可以使用“类名::”访问静态的数据成员。格式如下:类名::静态数据成员名。
静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。
- #include <iostream>
- using namespace std;
- class Score{
- private:
- int mid_exam;
- int fin_exam;
- static int count; //静态数据成员,用于统计学生人数
- static float sum; //静态数据成员,用于统计期末累加成绩
- static float ave; //静态数据成员,用于统计期末平均成绩
- public:
- Score(int m, int f);
- ~Score();
- static void show_count_sum_ave(); //静态成员函数
- };
- Score::Score(int m, int f)
- {
- mid_exam = m;
- fin_exam = f;
- ++count;
- sum += fin_exam;
- ave = sum / count;
- }
- Score::~Score()
- {
- }
- /*** 静态成员初始化 ***/
- int Score::count = 0;
- float Score::sum = 0.0;
- float Score::ave = 0.0;
- void Score::show_count_sum_ave()
- {
- cout << "学生人数: " << count << endl;
- cout << "期末累加成绩: " << sum << endl;
- cout << "期末平均成绩: " << ave << endl;
- }
- int main()
- {
- Score sco[3] = {Score(90, 89), Score(78, 99), Score(89, 88)};
- sco[2].show_count_sum_ave();
- Score::show_count_sum_ave();
- return 0;
- }
7.友元
类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。
说明:
友元函数虽然可以访问类对象的私有成员,但他毕竟不是成员函数。因此,在类的外部定义友元函数时,不必像成员函数那样,在函数名前加上“类名::”。
因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。
- #include <iostream>
- #include <string>
- using namespace std;
- class Score; //对Score类的提前引用说明
- class Student{
- private:
- string name;
- int number;
- public:
- Student(string na, int nu) {
- name = na;
- number = nu;
- }
- friend void show(Score &sc, Student &st);
- };
- class Score{
- private:
- int mid_exam;
- int fin_exam;
- public:
- Score(int m, int f) {
- mid_exam = m;
- fin_exam = f;
- }
- friend void show(Score &sc, Student &st);
- };
- void show(Score &sc, Student &st) {
- cout << "姓名:" << st.name << " 学号:" << st.number << endl;
- cout << "期中成绩:" << sc.mid_exam << " 期末成绩:" << sc.fin_exam << endl;
- }
- int main() {
- Score sc(89, 99);
- Student st("白", 12467);
- show(sc, st);
- return 0;
- }
三、继承和派生
1.概念(派生与初始化)
继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类或父类,在它基础上建立的新类称为派生类或子类。
从已有类派生出新类时,可以在派生类内完成以下几种功能:
- 可以增加新的数据成员和成员函数
- 可以对基类的成员进行重定义
- 可以改变基类成员在派生类中的访问属性
派生类的构造函数和析构函数
构造函数的主要作用是对数据进行初始化。在派生类中,如果对派生类新增的成员进行初始化,就需要加入派生类的构造函数。与此同时,对所有从基类继承下来的成员的初始化工作,还是由基类的构造函数完成,但是基类的构造函数和析构函数不能被继承,因此必须在派生类的构造函数中对基类的构造函数所需要的参数进行设置。同样,对撤销派生类对象的扫尾、清理工作也需要加入新的析构函数来完成。
可见:构造函数的调用严格地按照先调用基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。
2.多继承
- class 派生类名:继承方式1 基类名1,...,继承方式n 基类名n {
- 派生类新增的数据成员和成员函数
- };
- 默认的继承方式是private
3.虚基类
虚基类的作用:如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。
- #include <iostream>
- #include <string>
- using namespace std;
- class Base{
- protected:
- int a;
- public:
- Base(){
- a = 5;
- cout << "Base a = " << a << endl;
- }
- };
- class Base1: public Base{
- public:
- Base1() {
- a = a + 10;
- cout << "Base1 a = " << a << endl;
- }
- };
- class Base2: public Base{
- public:
- Base2() {
- a = a + 20;
- cout << "Base2 a = " << a << endl;
- }
- };
- class Derived: public Base1, public Base2{
- public:
- Derived() {
- cout << "Base1::a = " << Base1::a << endl;
- cout << "Base2::a = " << Base2::a << endl;
- }
- };
- int main() {
- Derived obj;
- return 0;
- }
虚基类的声明:
不难理解,如果在上列中类base只存在一个拷贝(即只有一个数据成员a),那么对a的访问就不会产生二义性。在C++中,可以通过将这个公共的基类声明为虚基类来解决这个问题。这就要求从类base派生新类时,使用关键字virtual将base声明为虚基类。
声明虚基类的语法形式如下:
class 派生类:virtual 继承方式 类名{
·····
};
上述代码修改如下:
class Base1:virtual public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};
class Base2:virtual public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};
虚基类的初始化:
虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。在使用虚基类机制时应该注意以下几点:
如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化列表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都被自动忽略。
若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。
对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。
~
4.派生类和基类对象赋值
派生类对象可以赋值给基类对象,即用派生类对象中从基类继承来的数据成员,逐个赋值给基类对象的数据成员。
派生类对象可以初始化基类对象的引用。
派生类对象的地址可以赋值给指向基类对象的指针。
5.派生类对基类成员的访问形式主要有:
- 内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。
- 对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。
通过基类指针只能访问派生类对象的成员变量,但是不能访问派生类的成员函数
四、多态性与虚函数
多态性的应用可以使编程显得更简洁便利,它为程序的模块化设计又提供了一种手段。
C++提供多态的目的是:可以通过 基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,基类只能访问派生类的成员变量
1.多态性
多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,这样就可以用同一个函数名调用不同内容的函数,也就是说,可以用同样的接口访问功能不同的函数,从而实现"一个接口,多种方法"的目的
最简单的例子:
就是运算符了,例如我们使用运算符+,就可以实现整型数、浮点数、双精度类型之间的加法运算,这三种类型的加法操作其实是互不相同的,是由不同内容的函数实现的。这个例子就是使用了多态的特征
理解:
基类指针指向不同的对象(基类对象和派生类对象)时,调用同名函数它执行的操作是不一样的,因此,当同一条语句可以执行不同的操作,看起来有不同表现方式时,这就是多态
两种多态的关系:
由静态联编支持的多态性称为编译时多态性(静态多态性),在C++中,编译时多态性是通过函数重载和模板实现,利用函数重载机制,在调用同名函数的同时,编译系统会根据实参的具体情况确定其所要调用的函数是哪个
由动态联编支持的多态性称为运行时多态(动态多态),在C++中,运行时的多态性是通过虚函数来实现的
多态的定义和实现:
举一个通俗易懂的例子:比如买票这个行为,普通人买是全价,学生买是半价票
实现多态的目的是在不同继承关系的类对象里,调用同一函数,能产生不同的行为,比如有一个Student派生类继承了Person基类,Person基类的买票函数就是全价,而Student派生类买票函数就是半价(即买票这个行为会根据买票对象的不同而不同)
如果需要使上方的要求成立,还需要两个条件,也就是在想x在继承中构成多态还需要两个条件:
1.调用函数的对象必须是指针或是引用(即必须以指针或引用的形式调用虚函数)
2.被调用的函数必须是虚函数(且必须已完成虚函数的重写)
2.虚函数
虚函数:在类(通常是最高基类)的成员函数前加上了virtual关键字的函数被称之为虚函数
当重写的函数未声明为虚函数时,基类类型的指针(*cat)指向派生类型的对象(new Cat并不是对象实例,但是也具有自己的内存地址,也是Cat类的对象)时, 通过基类指针只能访问派生类对象的成员变量,但是不能访问派生类的成员函数
为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了 虚 函 数(Virtual Function)
3.接口继承与实现继承
普通函数的继承方式是一种 实现继承,即派生类继承了基类的函数,可以使用实现继承的是函数的实现
虚函数的继承方式是一种 接口继承,派生类只基础 基类虚函数的接口,也就是只继承函数的声明,目的是实现重写,达成多态性。因为继承的是接口。所以如果不需要实现多态性,不需要把函数定义成虚函数(即使用类对象调用函数)
有了虚函数, 基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量), 基类指针指向派生类对象时就使用派生类的成员.换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)
派生类继承了基类的虚函数表并将在派生类中进行重写过的虚函数指针与继承来的虚函数表中的相对的虚函数指针进行了替换覆盖更新,而未进行重写的虚函数指针在虚函数表内的指针不变
4.总结
使用虚函数后的变化:
1.对象将增加一个存储地址(虚函数指针地址)的空间(32位系统为4字节,64位为8字节)
2.每个类编译器都创建一个虚函数地址表
3.对每个函数的调用都需要增加在表中查找地址的操作
1.构造函数不能为虚函数
2.基类的析构函数应该为虚函数
3.友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。如果派生类没有重定义函数,则会使用基类版本
3.基类方法中声明了方法为虚后,该方法在基类派生类中是虚的
4.如果不使用多态,那么就需要定义多个指针变量,很容易造成混乱,而有了多态,只需要一个基类的指针变量xxx就可以调用所有派生类的虚函数
6.多态性示例:
- #include <iostream>
- using namespace std;
- /*** 定义一个公共基类 ***/
- class Figure{
- protected:
- double x, y;
- public:
- Figure(double a, double b): x(a), y(b) { }
- virtual void getArea() //虚函数
- {
- cout << "No area computation defind for this class.\n";
- }
- };
- class Triangle: public Figure{
- public:
- Triangle(double a, double b): Figure(a, b){ }
- //虚函数重定义,用于求三角形的面积
- void getArea(){
- cout << "Triangle with height " << x << " and base " << y;
- cout << " has an area of " << x * y * 0.5 << endl;
- }
- };
- class Square: public Figure{
- public:
- Square(double a, double b): Figure(a, b){ }
- //虚函数重定义,用于求矩形的面积
- void getArea(){
- cout << "Square with dimension " << x << " and " << y;
- cout << " has an area of " << x * y << endl;
- }
- };
- class Circle: public Figure{
- public:
- Circle(double a): Figure(a, a){ }
- //虚函数重定义,用于求圆的面积
- void getArea(){
- cout << "Circle with radius " << x ;
- cout << " has an area of " << x * x * 3.14 << endl;
- }
- };
- int main(){
- Figure *p;
- Triangle t(10.0, 6.0);
- Square s(10.0, 6.0);
- Circle c(10.0);
- p = &t;
- p->getArea();
- p = &s;
- p->getArea();
- p = &c;
- p->getArea();
- return 0;
- }
五、运算符重载
对运算符使用其他的数据类型操作重新定义;
<返回类型说明符> operator <运算符符号>(<参数表>)
{
<函数体>
}
六、函数模板与类模板
#define Max(x, y)((x >= y) ? x : y)
宏定义带来的另一个问题是,可能在不该替换的地方进行了替换,而造成错误。事实上,由于宏定义会造成不少麻烦,所以在C++中不主张使用宏定义。解决以上问题的另一个方法就是使用模板。
1.函数模板
在模板定义语法中关键字 class 与 typename 的作用完全一样
C++typename的由来和用法 - 知乎 (zhihu.com)
嵌套从属类型
事实上类型T::const_iterator依赖于模板参数T, 模板中依赖于模板参数的名称称为从属名称(dependent name), 当一个从属名称嵌套在一个类里面时,称为嵌套从属名称(nested dependent name)。 其实T::const_iterator还是一个嵌套从属类型名称(nested dependent type name)。
嵌套从属名称是需要用typename声明的,其他的名称是不可以用typename声明的。比如下面是一个合法的声明:
template<typename T>
void fun(const T& proto ,typename T::const_iterator it);
- #include <iostream>
- using namespace std;
- template <typename T>
- T Max(T *array, int size = 0) {
- T max = array[0];
- for (int i = 1 ; i < size; i++) {
- if (array[i] > max) max = array[i];
- }
- return max;
- }
- int main() {
- int array_int[] = {783, 78, 234, 34, 90, 1};
- double array_double[] = {99.02, 21.9, 23.90, 12.89, 1.09, 34.9};
- int imax = Max(array_int, 6);
- double dmax = Max(array_double, 6);
- cout << "整型数组的最大值是:" << imax << endl;
- cout << "双精度型数组的最大值是:" << dmax << endl;
- return 0;
- }
所以对于
static std::promise<bool> connectionCompletedPromise;
这是一个类模板。
2.类模板
七、输入和输出
八、异常处理和命名空间
1.异常处理
C++处理异常的办法:如果在执行一个函数的过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用函数)来解决,如果上一级函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上传,如果到最高一级还无法处理,运行系统一般会自动调用系统函数terminate(),由它调用abort终止程序。
2.命名空间
九、STL标准模板库
STL六大部件
- 容器(Containers)
- 分配器(Allocators)
- 算法(Algorithm)
- 迭代器(Iterators)
- 适配器(Adapters)
- 仿函数(Functors)
要真正提高C++编程效率,需要将STL六大部件结合使用,才能大放异彩。所谓部件,也即是零件,需要将这六大零件组装在一起,配合使用,整整齐齐。
1.vector
vector(矢量),是一种「变长数组」,即“自动改变数组长度的数组”。
1>.定义
像定义变量一样定义vector变量:
vector<类型名> 变量名;
类型名可以是int、double、char、struct,也可以是STL容器:vector、set、queue。
2>.vector一般有两种访问方式:
通过下标访问
vector<int> vi;
vi.push_back(1);
cout<<vi[0]<<endl;
通过迭代器访问
vector<int>::iterator it=v.begin();
for (int i = 0; i < v.size(); i++)
{
cout<<it[i]<<" ";
}
//it[i] = *(it+i) //这两个写法等价
//vector的迭代器不支持it<v.end()的写法,因此循环条件只能it!=v.end()
for (vector<int>::iterator it=v.begin(); it!=v.end();it++)
3>.常见成员函数
push_back()
pop_back()
size()
clear() 清除全部元素
insert()
erase() 删除指定或区间元素
2.set容器
原本无序的元素,被插入set集合后,set内部的元素自动递增排序,并且自动去除了重复元素。(结构体插入需重载)
除了vector和string之外的STL容器都不支持*(it+i)的访问方式,因此set容器只能按照如下方式枚举:
for (set<int>::iterator it = st.begin(); it != st.end(); it++)
{
cout << *it << endl;
}
s.lower_bound() 返回第一个大于或等于给定关键值的元素
s.upper_bound() 返回第一个大于给定关键值的元素
s.equal_range() 返回一对定位器,分别表示 第一个大于或等于给定关键值的元素 和 第一个大于给定关键值
的元素,这个返回值是一个pair类型,如果这一对定位器中哪个返回失败,就会等于
s.end()
- cout << "第一个大于或等于3的元素: " << *s.lower_bound(3) << endl;
- cout << "第一个大于或等于2的元素: " <<*s.lower_bound(2) << endl;
- cout << "第一个大于2的元素: " <<*s.upper_bound(2) << endl;
- cout << "equal_range test:" << endl;
- cout << "第一个大于或等于2的元素: " << *s.equal_range(2).first << endl;
- cout << "第一个大于2的元素: " << *s.equal_range(2).second << endl;
3.deque
Vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。deque对象在队列的两端放置元素和删除元素是高效的,而向量vector只是在插入序列的末尾时操作才是高效的。deque和vector的最大差异,一在于deque允许于常数时间内对头端进行元素的插入或移除操作,二在于deque没有所谓的capacity观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。换句话说,像vector那样“因旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque中是不会发生的。也因此,deque没有必要提供所谓的空间预留(reserved)功能。
虽然deque也提供Random Access Iterator,但它的迭代器并不是普通指针,其复杂度和vector不可同日而语,这当然涉及到各个运算层面。因此,除非必要,我们应尽可能选择使用vector而非deque。对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector身上,将vector排序后(利用STL的sort算法),再复制回deque。
deque的常用成员函数:
deque<int> deq;
deq[ ]:用来访问双向队列中单个的元素。
deq.front():返回第一个元素的引用。
deq.back():返回最后一个元素的引用。
deq.push_front(x):把元素x插入到双向队列的头部。
deq.pop_front():弹出双向队列的第一个元素。
deq.push_back(x):把元素x插入到双向队列的尾部。
deq.pop_back():弹出双向队列的最后一个元素
deque的一些特点:
支持随机访问,即支持[ ]以及at(),但是性能没有vector好。
可以在内部进行插入和删除操作,但性能不及list。
deque两端都能够快速插入和删除元素,而vector只能在尾端进行。
deque的元素存取和迭代器操作会稍微慢一些,因为deque的内部结构会多一个间接过程。
deque迭代器是特殊的智能指针,而不是一般指针,它需要在不同的区块之间跳转。
deque可以包含更多的元素,其max_size可能更大,因为不止使用一块内存。
deque不支持对容量和内存分配时机的控制。
在除了首尾两端的其他地方插入和删除元素,都将会导致指向deque元素的任何pointers、references、iterators失效。不过,deque的内存重分配优于vector,因为其内部结构显示不需要复制所有元素。
deque的内存区块不再被使用时,会被释放,deque的内存大小是可缩减的。不过,是不是这么做以及怎么做由实际操作版本定义。
deque不提供容量操作:capacity()和reverse(),但是vector可以。
4.list
List是stl实现的双向链表,与向量(vectors)相比, 它允许快速的插入和删除,但是随机访问却比较慢
list常用操作函数:
Lst1.assign() 给list赋值
Lst1.back() 返回最后一个元素
Lst1.begin() 返回指向第一个元素的迭代器
Lst1.clear() 删除所有元素
Lst1.empty() 如果list是空的则返回true
Lst1.end() 返回末尾的迭代器
Lst1.erase() 删除一个元素
Lst1.front() 返回第一个元素
Lst1.get_allocator() 返回list的配置器
Lst1.insert() 插入一个元素到list中
Lst1.max_size() 返回list能容纳的最大元素数量
Lst1.merge() 合并两个list
Lst1.pop_back() 删除最后一个元素
Lst1.pop_front() 删除第一个元素
Lst1.push_back() 在list的末尾添加一个元素
Lst1.push_front() 在list的头部添加一个元素
Lst1.rbegin() 返回指向第一个元素的逆向迭代器
Lst1.remove() 从list删除元素
Lst1.remove_if() 按指定条件删除元素
Lst1.rend() 指向list末尾的逆向迭代器
Lst1.resize() 改变list的大小
Lst1.reverse() 把list的元素倒转
Lst1.size() 返回list中的元素个数
Lst1.sort() 给list排序
Lst1.splice() 合并两个list
Lst1.swap() 交换两个list
Lst1.unique() 删除list中相邻重复的元素
5.map/multimap
C++中map提供的是一种键值对容器,里面的数据都是成对出现的,如下图:每一对中的第一个值称之为关键字(key),每个关键字只能在map中出现一次;第二个称之为该关键字的对应值。
迭代器
共有八个获取迭代器的函数:* begin, end, rbegin,rend* 以及对应的 * cbegin, cend, crbegin,crend*。
二者的区别在于,后者一定返回 const_iterator,而前者则根据map的类型返回iterator 或者 const_iterator。const情况下,不允许对值进行修改。
插入操作:
4种
十、库函数
1、std
(1)【精选】std::bind()与std::ref()_std::bind std::ref-CSDN博客
(2)
std::promise 对象可以保存某一类型 T 的值,该值可被 future 对象读取(可能在另外一个线程中),
(3)Std:function
(4)Lambda表达式
c++ lambda 看这篇就够了!(有点详细)_c++ 运行时 构建 lamda-CSDN博客
Learn
1.OpenSSL
即open secure sockets layer,是一个开源的安全套接字层的密码库。包括常用的密码加解密算法、常用的密钥算法、证书管理和SSL协议。
1、库的主要内容
OpenSSL 库主要包含三大部分:
openssl: 多用途的命令行工具,可以执行交互或批量命令。
libcrypto: 加解密算法库。
libssl:加密模块应用库,实现了ssl及tls。
- Curl
curl 是常用的命令行工具,用来请求 Web 服务器。它的名字就是客户端(client)的 URL 工具的意思。
它的功能非常强大,命令行参数多达几十种。如果熟练的话,完全可以取代 Postman 这一类的图形界面工具。
- 编译原理
Inhand/configs/system。。
编译总是针对单个文件的,这也是很多初学者的困惑之一:那多个文件怎么办?那是连接的事,我们先把编译搞完
链接就是把编译产生的目标文件和库一起组合成最终的程序。
- Linux文件结构
/: 根目录,一般根目录下只存放目录,不要存放文件,/etc、/bin、/dev、/lib、/sbin应该和根目录放置在一个分区中
/bin:/usr/bin: 可执行二进制文件的目录,如常用的命令ls、tar、mv、cat等。
/boot: 放置linux系统启动时用到的一些文件。/boot/vmlinuz为linux的内核文件,以及/boot/gurb。建议单独分区,分区大小100M即可
/dev: 存放linux系统下的设备文件,访问该目录下某个文件,相当于访问某个设备,常用的是挂载光驱mount /dev/cdrom /mnt。
/etc: 系统配置文件存放的目录,不建议在此目录下存放可执行文件,重要的配置文件有/etc/inittab、/etc/fstab、/etc/init.d、/etc/X11、/etc/sysconfig、/etc/xinetd.d修改配置文件之前记得备份。注:/etc/X11存放与x windows有关的设置。
/home: 系统默认的用户家目录,新增用户账号时,用户的家目录都存放在此目录下,~表示当前用户的家目录,~test表示用户test的家目录。建议单独分区,并设置较大的磁盘空间,方便用户存放数据
/lib:/usr/lib:/usr/local/lib: 系统使用的函数库的目录,程序在执行过程中,需要调用一些额外的参数时需要函数库的协助,比较重要的目录为/lib/modules。
/lost+fount: 系统异常产生错误时,会将一些遗失的片段放置于此目录下,通常这个目录会自动出现在装置目录下。如加载硬盘于/disk 中,此目录下就会自动产生目录/disk/lost+found
/mnt:/media: 光盘默认挂载点,通常光盘挂载于/mnt/cdrom下,也不一定,可以选择任意位置进行挂载。
/opt: 给主机额外安装软件所摆放的目录。如:FC4使用的Fedora 社群开发软件,如果想要自行安装新的KDE 桌面软件,可以将该软件安装在该目录下。以前的 Linux 系统中,习惯放置在 /usr/local 目录下
/proc: 此目录的数据都在内存中,如系统核心,外部设备,网络状态,由于数据都存放于内存中,所以不占用磁盘空间,比较重要的目录有/proc/cpuinfo、/proc/interrupts、/proc/dma、/proc/ioports、/proc/net/*等
/root: 系统管理员root的家目录,系统第一个启动的分区为/,所以最好将/root和/放置在一个分区下。
/sbin:/usr/sbin:/usr/local/sbin: 放置系统管理员使用的可执行命令,如fdisk、shutdown、mount等。与/bin不同的是,这几个目录是给系统管理员root使用的命令,一般用户只能"查看"而不能设置和使用。
/tmp: 一般用户或正在执行的程序临时存放文件的目录,任何人都可以访问,重要数据不可放置在此目录下
/srv: 服务启动之后需要访问的数据目录,如www服务需要访问的网页数据存放在/srv/www内
/usr: 应用程序存放目录,/usr/bin 存放应用程序, /usr/share 存放共享数据,/usr/lib 存放不能直接运行的,却是许多程序运行所必需的一些函数库文件。/usr/local:存放软件升级包。/usr/share/doc: 系统说明文件存放目录。/usr/share/man: 程序说明文件存放目录,使用 man ls时会查询/usr/share/man/man1/ls.1.gz的内容建议单独分区,设置较大的磁盘空间
/var: 放置系统执行过程中经常变化的文件,如随时更改的日志文件 /var/log,/var/log/message: 所有的登录文件存放目录,/var/spool/mail: 邮件存放的目录, /var/run: 程序或服务启动
- 文件系统总结:
linux系统中每个分区都是一个文件系统,都有自己的目录层次结构。
(1)挂载点必须是一个目录。
一个分区挂载在一个已存在的目录上,这个目录可以不为空,但挂载后这个目录下以前的内容将不可用。对于其他操作系统建立的文件系统的挂载也是这样,卸载后,目录以前的文件都还在,不会有任何丢失。
目录只占磁盘里的一个inode,存放文件属性等信息。
任何一个分区都必须挂载到某个目录上。
目录是逻辑上的区分。分区是物理上的区分。
磁盘Linux分区都必须挂载到目录树中的某个具体的目录上才能进行读写操作。
根目录是所有Linux的文件和目录所在的地方,需要挂载上一个磁盘分区。
一个分区可以挂在多个目录,但反过来一个目录只能是一个分区的挂载点。
(2)系统分区查看命令
df -hT(mount -r)
分区的文件系统挂载点
Fdisk -l
查看系统分区
ER8后台看分区name
ER6看分区name
cat /proc/mtd
(3)系统文件类型TMPFS详解
我们通过df可以看到tmpfs是挂载到/dev/下的shm目录,tmpfs是什么呢? 其实是一个临时文件系统,驻留在内存中,所以/dev/shm/这个目录不在硬盘上,而是在内存里。因为是在内存里,所以读写非常快,可以提供较高的访问速度。linux下,tmpfs默认最大为内存的一半大小,使用df -h命令刚才已经看到了,但是这个df查看到的挂载内存大小的数值,如果没有使用,是没有去真正占用的,只有真正在tmpfs存储数据了,才会去占用。比如,tmpfs大小是499M,用了10M大小,内存里就会使用真正使用10M,剩余的489M是可以继续被服务器其他程序来使用的。但是因为数据是在内存里,所以断电后文件会丢失,内存数据不会和硬盘中数据一样可以永久保存。了解了tmpfs这个特性可以用来提高服务器性能,把一些对读写性能要求较高,但是数据又可以丢失的这样的数据保存在/dev/shm中,来提高访问速度。
(4)查看内存
Free. top .(fdisk -l)是查看分区.
Free = fdisk + tmpfs(总量496M)
挂载时使用mount命令:
格式:mount [-参数] [设备名称] [挂载点]
其中常用的参数有
-t 指定设备的文件系统类型,常见的有:
minix linux最早使用的文件系统
Ext4 linux目前常用的文件系统
命令:cat /proc/进程号/status
VmHWM: 1432 kB
VmRSS: 1420 kB
解释:
VmHWM是程序得到分配到物理内存的峰值.
VmRSS是程序现在使用的物理内存.
- 查看目录使用的分区
先看分区
Fdisk -l
在看挂载点
Df -hT
5.IP SLA特性
测量能力:可以测量UDP响应时间、单向延时、抖动、掉包情况和连通性;ICMP响应时间与连通性、每一跳的ICMP 响应时间与抖动;DNS查询、TCP 连接、HTTP 处理时间等的性能度量;丢包统计;DHCP响应时间测试;从网络设备到服务器的响应时间;模拟Voip的codec’s测试出语音质量的MOS/ICPIF得分;DLSw+通道性能;
6.HTTP请求报文格式
请求行:
①是请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。
②为请求对应的URL地址,它和报文头的Host属性组成完整的请求URL。
③是协议名称及版本号。
请求头:
④是HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。
与缓存相关的规则信息,均包含在header中
请求体:
⑤是报文体,它将一个页面表单中的组件值通过param1=value1¶m2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1¶m2=value2”的方传递请求参数。
以下是几个常见的状态码:
200 OK
你最希望看到的,即处理成功!
303 See Other
我把你redirect到其它的页面,目标的URL通过响应报文头的Location告诉你。
304 Not Modified
告诉客户端,你请求的这个资源至你上次取得后,并没有更改,你直接用你本地的缓存吧,我很忙哦,你能不能少来烦我啊!
404 Not Found
你最不希望看到的,即找不到页面。如你在google上找到一个页面,点击这个链接返回404,表示这个页面已经被网站删除了,google那边的记录只是美好的回忆。
500 Internal Server Error
看到这个错误,你就应该查查服务端的日志了,肯定抛出了一堆异常,别睡了,起来改BUG去吧!
◆200 (OK): 找到了该资源,并且一切正常。
◆302/307:临时重定向,指出请求的文档已被临时移动到别处, 此文档的新的url在location响应头中给出
◆304 (NOT MODIFIED): 该资源在上次请求之后没有任何修改。这通常用于浏览器的缓存机制。
◆401 (UNAUTHORIZED): 客户端无权访问该资源。这通常会使得浏览器要求用户输入用户名和密码,以登录到服务器。
◆403 (FORBIDDEN): 客户端未能获得授权。这通常是在401之后输入了不正确的用户名或密码。
◆404 (NOT FOUND): 在指定的位置不存在所申请的资源。
7.Linux中gmtime和localtime的区别
time()函数,返回一个从1970年1月1日 00:00:00到现在的秒数
(1)time_t time(time_t * t); 当参数为NULL时直接返回秒数,当然也会将该值写入t指针指向的地址
gmtime();将time函数得到的秒数转换成一个UTC时间的结构体struct tm,该结构体包含什么请自行man
通过此函数gmtime()是0时区,把UTC时间转换成北京时间的话,需要在年数上加1900,月份上加1,小时数加上8
当然同类型的函数还有localtime();得到本地时间,该函数同gmtime函数唯一区别是,在转换小时数不需要加上8了。
localtime是将时区考虑在内了,转出的当前时区的时间。但是注意,有些嵌入式设备上被裁减过的系统,时区没有被设置好,导致二者转出来的时间都是0时区的。
8.指针变量
从左往右计算:
关于 * 和 & 的谜题
假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a和&*pa分别是什么意思呢?
*&a可以理解为*(&a),&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。
&*pa可以理解为&(*pa),*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。
实际上*p++符号整体对外表现的值是*p的值,运算完后p再加1。
定义指针变量时的*和使用指针变量时的*意义完全不同。
- int a = 100;
- int *p_a = &a;
在定义指针变量 p_a 的同时对它进行初始化,并将变量 a 的地址赋予它,此时 p_a 就指向了 a。值得注意的是,p_a 需要的一个地址,a 前面必须要加取地址符&,否则是不对的。*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*。
9.关于数组指针的谜题
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?
*p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素,上面已经进行了详细讲解(注:是指针加1)。
*++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
(*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1,这是为什么呢?因为指针变量是地址。
理解这一段代码字符串指针加:
10.库函数
系统调用,我们可以理解是操作系统为用户提供的一系列操作的接口(API),这些接口提供了对系统硬件设备功能的操作。这么说可能会比较抽象,举个例子,我们最熟悉的 hello world 程序会在屏幕上打印出信息。程序中调用了 printf() 函数,而库函数 printf 本质上是调用了系统调用 write() 函数,实现了终端信息的打印功能。
11.系统调用意义
- 避免了用户直接对底层硬件进行编程。比如最简单的hello world程序是将信息打印到终端,终端对系统来说是硬件资源,如果没有系统调用,用户程序需要自己编写终端设备的驱动,以及控制终端如何显示的代码。
- 隐藏背后的技术细节。比如读写文件,如果使用了系统调用,用户程序无须关心数据在磁盘的哪个磁道和扇区,以及数据要加载到内存什么位置。
- 保证系统的安全性和稳定性。要知道用户程序是不能直接操作内核地址空间的,比如一个刚出道的程序猿,让他直接去访问内核底层的数据,那么内核系统的安全性就无法保证。而系统调用的功能是由内核来实现,用户只需要调用接口,无需关心细节,也避免了系统的安全隐患。
- 方便程序的移植性。如果针对一个系统资源的功能操作比如 write(),大家都按照自己思路去实现这个功能,那么我们写出来的程序的移植性就会非常差。
总而言之,我们只需要把系统调用当作一个接口,而这个接口能实现我们的一个功能,既方便又安全。
据书中记载,库函数调用大概花费时间为半微妙,而系统调用所需要的时间大约是库函数调用的 70 倍(35微秒),因为系统调用会有内核上下文切换的开销。纯粹从性能上考虑,你应该尽可能地减少系统调用的数量,但是,你必须记住许多 C 函数库中的程序通过系统调用来实现功能。
正确理解库函数高效于系统调用
首先解释,上述说明的库函数性能远高于系统调用的前提是,库函数种没有使用系统调用。再来解释下某些包含系统调用的库函数,然而其性能确实也要高于系统调用。比如上篇文章中关于文件 IO 函数 fread、fwrite、fputc、fgetc 等,这些函数通常情况下性能确实比系统调用高,原因在于这些库函数使用了缓冲区,减少了系统调用的次数,因而显得性能比较高。
- fork函数和多进程(exec)
fork系统调用用于创建一个新进程,称为子进程,它与父进程 同时运行(并发),且运行顺序不定(异步)。
fork()函数如果成功调用一次返回两个值,一共可能有三种不同的返回值:
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0;
(3)如果出现错误,fork返回一个负值。
system函数的特点:
建立独立进程,拥有独立的代码空间,内存空间
等待新的进程执行完毕,system才返回。(阻塞)
替换进程映像(exec函数)
exec函数可以用来替换进程映像。执行exec系列函数后,原来的进程将不再执行,新的进程的PID、PPID和nice值与原先的完全一样。其实执行exec系列函数所发生的一切就是,运行中的程序开始执行exec调用中指定的新的可执行文件中的代码。
exec函数的特点:
当进程调用一种exec函数时,源进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。特别地,在原进程中已经打开的文件描述符,在新进程中仍将保持打开,除非它们的“执行时关闭标志”(close on exec flag)被置位。任何在原进程中已打开的目录流都将在新进程中被关闭。
注意:
我们看到system()函数实际上就是先执行了fork函数,然后新产生的子进程立刻执行了exec函数,我们前面说个fork函数换汤不换药,exec函数换药不换汤,那么system函数就是既换汤也换了药,也就是system函数会产生新进程,这就意味着新进程的PID、PPID等与原进程不同。system也会产生新的进程空间,而且新的进程空间是为新的程序准备的,所以和原进程的进程空间没有任何关系(不像fork新进程空间是对原进程空间的一个复制)。还要注意的是,system函数代码中else部分执行了wait函数,这就意味着,原进程会等待子进程执行完毕(阻塞)
execvp()之后的部分根本不执行,因为“ ls -l”控制了我们的过程!
13.字符常量和变量
因为字符是常量,比如'a',&'a'取地址怎么理解?我们取地址一般是对于变量来讲的
"abc"是一个长度为4的字符数组,其中最后一个元素是结尾字符/ 0 。注意,字符串常量和字符常量是不同的概念。例如,'a' 和 "a" 并不相同。后者拥有两个元素,一个是'a' ,另一个是'/ 0 '
字符串常量和数组名一样,也是被编译器当成指针来对待的。它的值就是字符串的基地址。
考虑下面的代码:
char *p="abc";
printf("%s %s/n",p,p+1);
变量p被赋值为字符数组"abc"的基地址。当一个char类型的指针按照字符串格式打印时,被指向的字符以及每个后续字符都被打印出来,而指向字 符串"abc"中字母b的表达式p+1将导致bc被打印出来。由于象"abc"这样的字符串常量是被当作指针看待的,因此下面的两个表达式都是可行的:
"abc"[1] 和 *("abc"+2)
如果输出的话结果应该是bc 和c。
双引号做了3件事:
1.申请了空间(在常量区),存放了字符串
2. 在字符串尾加上了'/0'
3.返回地址
你这里就是 返回的地址 赋值给了 p
首先是数组的声明,数组在声明的时候可以连续进行赋值,即一次进行多个数组的元素的赋值,但进行声明后就不可以进行多元素的赋值(不包括memcpy),只能对每个元素进行赋值
14.libevent框架(转)
基本应用场景也是使用libevnet的基本流程,下面来考虑一个最简单的场景,使用livevent设置定时器,应用程序只需要执行下面几个简单的步骤即可。
1)首先初始化libevent库,并保存返回的指针
1 struct event_base * base = event_init();
g_my_event_base = event_base_new();
实际上这一步相当于初始化一个Reactor实例;在初始化libevent后,就可以注册事件了。
2)初始化事件event,设置回调函数和关注的事件
1 evtimer_set(&ev, timer_cb, NULL);
事实上这等价于调用
1 event_set(&ev, -1, 0, timer_cb, NULL);
event_set的函数原型是:
1 void event_set(struct event *ev, int fd, short event, void (*cb)(int, short, void *), void *arg)
ev:执行要初始化的event对象;
fd:该event绑定的“句柄”,对于信号事件,它就是关注的信号;
event:在该fd上关注的事件类型,它可以是EV_READ, EV_WRITE, EV_SIGNAL;
cb:这是一个函数指针,当fd上的事件event发生时,调用该函数执行处理,它有三个参数,调用时由event_base负责传入,按顺序,实际上就是event_set时的fd, event和arg;
arg:传递给cb函数指针的参数;
由于定时事件不需要fd,并且定时事件是根据添加时(event_add)的超时值设定的,因此这里event也不需要设置。
这一步相当于初始化一个event handler,在libevent中事件类型保存在event结构体中。
注意:libevent并不会管理event事件集合,这需要应用程序自行管理;
3)设置event从属的event_base
1 event_base_set(base, &ev);
这一步相当于指明event要注册到哪个event_base实例上;
4)是正式的添加事件的时候了
1 event_add(&ev, timeout);
基本信息都已设置完成,只要简单的调用event_add()函数即可完成,其中timeout是定时值;
这一步相当于调用Reactor::register_handler()函数注册事件。
5)程序进入无限循环,等待就绪事件并执行事件处理
1 event_base_dispatch(base);
15.理解
1.mac地址
MAC地址起源比IP地址早
起初通过交换机和MAC地址通信,由于MAC地址表越来越多,就有IP地址通信。
其实有了IP地址,MAC地址就不用存在,但交换机转发和DHCP静态分配地址需要MAC地址计算IP地址,以防冲突。
Arp工作在数据链路。在数据包在传输的过程需要Mac地址,Mac地址是随时变化的,IP地址在nat情况下才会变化。
IP整个通信过程先dns解析,然后arp请求网关Mac,然后发到路由器经过nat和路由转发,到达主机
2.nat地址转换
操作系统
1.进程和程序
区别:
1、进程是动态的,而程序是静态的;
2、进程有一定的生命期,而程序是指令的集合,本身无“运动”的含义。没有建立进程的程序不能作为一个独立单位得到操作系统的认可。
3、一个程序可以对应多个进程,但一个进程只能对应一个程序。
4、进程和程序的组成不同。从静态角度看,进程由程序、数据和进程控制块(PCB)三部分组成,而程序是一组有序的指令集合。
联系:通俗的说:进程就是程序的执行过程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
进程和应用程序
我打开一个程序,比如chrome,有十多个进程呢,这是咋回事。那就是十多个程序,操作系统给他们分配了彼此独立的内存,相互执行不受彼此约束,分配同样时间的CPU。对于用户而言,他们是一个整体,我们通常称之为应用程序(application)。对于计算机而言,一个进程就是一个程序,多个进程(比如一个浏览器的多个进程)对计算机而言就是多个不同的程序,它不会把它们理解为一个完整的“程序”。
2.线程
线程切换的上下文?
当两个线程属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
上下切换的耗时大概在几十纳秒到几微秒之间,如果锁住的代码执行时间比较短,可能上下文切换的时间比锁住的代码执行时间还要长。
若是能确定被锁住的代码执行时间很短,就不应该使用互斥锁,而应该选择自旋锁。
(1)深入理解线程取消(pop)
Pthread 并发编程(三)——深入理解线程取消机制 - 掘金 (juejin.cn)
pthread_cleanup_push()/pthread_cleanup_pop()的详解 - PKICA - 博客园 (cnblogs.com)
注意:使用push后,中途有return,之后再pop,那么在释放内存的时候就会存在问题。
因为return不会执行pop,会导致线程退出重复执行pop,导致程序崩溃。
在线程宿主函数中主动调用return,如果return语句包含在pthread_cleanup_push()/pthread_cleanup_pop()对中,则不会引起清理函数的执行,反而会导致segment fault。
线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;非正常终止是线程在其他线程的干预下,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。
不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑解决的问题。
push进去的函数可能在以下三个时机执行:
1,显示的调用pthread_exit();
2,在cancel点线程被cancel。
3,pthread_cleanup_pop()的参数不为0时。
- pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。在下面的例子里,当线程在"do some work"中终止时,将主动调用pthread_mutex_unlock(mut),以完成解锁动作。
- work"中终止时,将主动调用pthread_mutex_unlock(mut),以完成解锁动作。
- pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut);
- pthread_mutex_lock(&mut);
- /* do some work */
- pthread_mutex_unlock(&mut);
- pthread_cleanup_pop(0);
- 必须要注意的是,如果线程处于PTHREAD_CANCEL_ASYNCHRONOUS状态,上述代码段就有可能出错,因为CANCEL事件有可能在
- pthread_cleanup_push()和pthread_mutex_lock()之间发生,或者在pthread_mutex_unlock()和pthread_cleanup_pop()之间发生,从而导致清理函数unlock一个并没有加锁的
- mutex变量,造成错误。因此,在使用清理函数的时候,都应该暂时设置成PTHREAD_CANCEL_DEFERRED模式。为此,POSIX的
- Linux实现中还提供了一对不保证可移植的pthread_cleanup_push_defer_np()/pthread_cleanup_pop_defer_np()扩展函数,功能与以下
- 代码段相当:
- { int oldtype;
- pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);
- pthread_cleanup_push(routine, arg);
- ...
- pthread_cleanup_pop(execute);
- pthread_setcanceltype(oldtype, NULL);
- }
(2)条件变量
(理解:pthread_cond_wait进入解锁,返回加锁)
至于为什么在被唤醒之后还要再次进行条件判断(即为什么要使用while循环来判断条件),是因为可能有“惊群效应”。有人觉得此处既然是被唤醒的,肯定 是满足条件了,其实不然。如果是多个线程都在等待这个条件,而同时只能有一个线程进行处理,此时就必须要再次条件判断,以使只有一个线程进入临界区处理
从上文可以看出:
1,pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续 wait,while循环的意义就体现在这里了,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上 的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程.
2,某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐此处使用while循环.
其实说白了很简单,就是pthread_cond_signal()也可能唤醒多个线程,而如果你同时只允许一个线程访问的话,就必须要使用while来进行条件判断,以保证临界区内只有一个线程在处理。
pthread_cond_wait() 用于阻塞当前线程,等待别的线程使用 pthread_cond_signal() 或pthread_cond_broadcast来唤醒它 。 pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait() 函数一进入wait状态就会自动release mutex。当其他线程通过 pthread_cond_signal() 或pthread_cond_broadcast ,把该线程唤醒,使 pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex 。
pthread_cond_signal 函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待态,pthread_cond_signal也会成功返回。
使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。
但是 pthread_cond_signal 在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续 wait,
用法:
pthread_cond_wait必须放在pthread_mutex_lock和pthread_mutex_unlock之间,因为他要根据共享变量的状态来决定是否要等待,而为了不永远等待下去所以必须要在lock/unlock队中
共享变量的状态改变必须遵守lock/unlock的规则
pthread_cond_signal即可以放在pthread_mutex_lock和pthread_mutex_unlock之间,也可以放在pthread_mutex_lock和pthread_mutex_unlock之后,但是各有各缺点。
之间:
pthread_mutex_lock
xxxxxxx
pthread_cond_signal
pthread_mutex_unlock
缺点:在某下线程的实现中,会造成等待线程从内核中唤醒(由于cond_signal)然后又回到内核空间(因为cond_wait返回后会有原子加锁的行为),所以一来一回会有性能的问题。
在code review中,我会发现很多人喜欢在pthread_mutex_lock()和pthread_mutex_unlock(()之间调用 pthread_cond_signal或者pthread_cond_broadcast函数,从逻辑上来说,这种使用方法是完全正确的。但是在多线程环境中,这种使用方法可能是低效的。posix1标准说,pthread_cond_signal与pthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是说,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。这里举个例子:
我们假设系统中有线程1和线程2,他们都想获取mutex后处理共享数据,再释放mutex。请看这种序列:
1)线程1获取mutex,在进行数据处理的时候,线程2也想获取mutex,但是此时被线程1所占用,线程2进入休眠,等待mutex被释放。
2)线程1做完数据处理后,调用pthread_cond_signal()唤醒等待队列中某个线程,在本例中也就是线程2。线程1在调用pthread_mutex_unlock()前,因为系统调度的原因,线程2获取使用CPU的权利,那么它就想要开始处理数据,但是在开始处理之前,mutex必须被获取,很遗憾,线程1正在使用mutex,所以线程2被迫再次进入休眠。
3)然后就是线程1执行pthread_mutex_unlock()后,线程2方能被再次唤醒。
从这里看,使用的效率是比较低的,如果再多线程环境中,这种情况频繁发生的话,是一件比较痛苦的事情。
之后:
pthread_mutex_lock
xxxxxxx
pthread_mutex_unlock
pthread_cond_signal
优点:不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了
缺点:如果unlock和signal之前,有个低优先级的线程正在mutex上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(cond_wait的线程),而这在上面的放中间的模式下是不会出现的。
所以,在Linux下最好pthread_cond_signal放中间,但从编程规则上说,其他两种都可以
(3)进程栈和线程栈
栈空间是存储程序运行的数据。
线程栈大小为8192(8MB),修改栈大小,使用pthread_attr_setstack(),线程栈的空间从堆中进行分配。(也可以直接定义变量,从父线程栈中分配)
线程栈总结:
两个线程时,两个线程栈的总和不是固定值,也不是线程栈的2倍(进程创建单个线程的线程栈是8M)。
线程从进程栈分配空间,大小并不是固定的,如果分配空间大于进程栈空间,那么直接运行时出现段错误。从堆中分配就正常。
进程栈总结:
进程的栈大小不是固定的,而是比线程栈大一些
(4)创建线程的开销
创建多个线程,一个线程所需的虚拟内存大约需要8M(线程栈),但实际所占物理内存几百KB。
对于线程动态申请内存,如果没有真实的初始化和赋值,实际上这部分内存还是在虚拟内存上,只有真实的写入值,才会写入vmRSS(物理内存)。
3.文件权限
而此处的权限将用8进制的数字来表示User、Group、及Other的读、写、执行权限
范例:
设置所有人可以读写及执行
chmod 777 file (等价于 chmod u=rwx,g=rwx,o=rwx file 或 chmod a=rwx file)
设置拥有者可读写,其他人不可读写执行
chmod 600 file (等价于 chmod u=rw,g=---,o=--- file 或 chmod u=rw,go-rwx file )
十位权限表示
常见的权限表示形式有:
-rw------- (600) 只有拥有者有读写权限。
-rw-r--r-- (644) 只有拥有者有读写权限;而属组用户和其他用户只有读权限。
-rwx------ (700) 只有拥有者有读、写、执行权限。
-rwxr-xr-x (755) 拥有者有读、写、执行权限;而属组用户和其他用户只有读、执行权限。
-rwx--x--x (711) 拥有者有读、写、执行权限;而属组用户和其他用户只有执行权限。
-rw-rw-rw- (666) 所有用户都有文件读、写权限。
-rwxrwxrwx (777) 所有用户都有读、写、执行权限。
4.用户态和内核态
进行系统调用会进入内核态,如果运行普通函数是运行在用户态
5.获取时间函数
(1)时间数据结构类型
(2)时间的函数
函数 说明
time 获取当前经过的秒数 time_t time(time_t *t);
获取当前时间和日期 struct tm *gmtime(const time_t *timep);
获取当前时间和日期并转换为本地时间 struct tm *localtime(const time_t * timep);
timegm 从tm时间转换到time_t时间,不考虑时区 time_t timegm(struct tm *tm);
timelocal 从tm时间转换到time_t时间,考虑时区 time_t timelocal(struct tm *tm);
mktime 将时间转换成经过的秒数 time_t mktime(strcut tm * timeptr);
asctime 将时间日期以字符串格式表示 char *asctime(const struct tm * timeptr);
ctime 将时间日期以字符串格式表示 char *ctime(const time_t *timep);
gettimeofday 获取当前时int gettimeofday (struct timeval * tv, struct timezone * tz);
settimeofday 设置当天时间戳
1、time
头文件:time.h
原型:time_t time(time_t *t);
说明:此函数会返回从公元 1970 年1 月1 日的UTC 时间从0 时0 分0 秒算起到现在所经过的秒数。如果t 并非空指针的话,此函数也会将返回值存到t 指针所指的内存。
返回值:成功则返回秒数,失败则返回((time_t)-1)值,错误原因存于errno 中。
2、gmtime
头文件:time.h
原型:struct tm *gmtime(const time_t *timep);
说明:gmtime()将参数timep 所指的time_t 中的信息转换成真实世界所使用的时间日期表示方法,然后将结果由结构tm 返回。此函数返回的时间日期未经时区转换,是UTC 时间。
返回值:返回结构tm 代表目前UTC 时间。
ime.h还提供了两种不同的函数将日历时间(一个用time_t表示的整数)转换为我们平时看到的把年月日时分秒分开显示的时间格式tm:
struct tm * gmtime(const time_t *timer);
struct tm * localtime(const time_t * timer);
localtime()将参数timep 所指的time_t 结构中的信息转换成真实世界所使用的时间日期表示方法,然后将结果由结构tm 返回。此函数返回的时间日期已经转换成当地时区。
Git
- 查看跟踪上游分支和远程分支
Git branch -vv -r
直接与远程分支关联:git checkout -b 分支 远程分支
git remote show origin:展示远程分支
git remote prune origin:删除远程在本地的缓存
- 回退版本
版本回退之后,需要再次回到会退前,可以用git reflog查看命令历史,可以查看到每次命令的记录,里面会有我们需要的版本ID ,恢复到指定版本git reset --hard commit_id。
git reset HEAD 如果后面什么都不跟的话 就是上一次add 里面的全部撤销了
git reset HEAD XXX/XXX/XXX.java 就是对某个文件进行撤销了
git reset HEAD XXX/XXX/XXX/. 就是对某个文件夹进行撤销了
git reset --soft HEAD^(直接运行此命令,没push)这样就成功撤销了commit,
如果想要连着add也撤销的话,--soft改为--hard(删除工作空间的改动代码
git reset --hard
<=>
git reset .
git checkout --
已暂存状态。也就是说这些文件被add过,但是没有commit。如果此时提交,那么该文件此时此刻的版本将被留存在历史记录中。
但是这些文件是被.ignore忽略的,所以我们应该取消add
git reset HEAD <路径/文件名> 就是对某个文件进行撤销了
- 设置跟踪上游分支
git branch -u origin/分支
4.git stash命令
保存当前分支修改的代码,提交到堆栈中临时保存起来。
zzg@LAPTOP-8R0KHL88 MINGW64 /e/idea_workspace/smart-medical (master)
$ git stash
Saved working directory and index state WIP on master: ac4b488 初始化sql
2、暂存时,可以添加一些备注信息。
git stash save '暂存信息'
3、git stash list
查看暂存列表
4、git stash pop [–index] [stash_id]
git默认会把暂存区的代码都恢复到工作区。
git stash pop
git恢复最新暂存区的代码到工作区。
git stash pop --index
git 恢复指定的暂存区的代码到工作区。stash_id是通过git stash list命令得到的
git stash pop stash@{1}
温馨提示:通过git stash pop命令恢复进度后,会删除当前进度
5、git stash apply [–index] [stash_id]
与git stash pop 指令功能一样,不同于git stash pop,该命令不会将内容从堆栈中删除
6、git stash drop [stash_id]
删除一个存储的进度。如果不指定stash_id,则默认删除最新的存储进度。
7、git stash clear
删除所有存储的进度。
8、git stash show -p(展示代码)
查看堆栈中最新保存的stash和当前目录的差异。
5.git add撤销
(1)想要查看暂存区的修改,可以执行以下命令:
git diff --staged
(2)如果这时我们想要一次性撤销暂存区的全部修改,可以执行以下命令(当然也可以撤销暂存区指定文件的修改):
git reset .
(3)git reset --hard
<=>
git reset .
git checkout --
6.git pull 冲突
使用git stash push -m “备注” <file>暂存冲突文件
7.分支开发
git cherry-pick id id(id..id)
Sqlite3
1.sqlite3的特殊指令(都是以.开头的)
1.1打开数据库
sqlite3 xx.db
使用sqlite3管理系统打开文件名叫做xx的数据库文件
.db表示数据库文件后缀
1.3查询信息
.database(events.db)
1.4查询显示数据库中所有的表
.tables 或者 .table(events(EVENT_TB_B))
1.5查询显示创建表的结构
.schema xxx xx表示表名称,这个时候就是
查看显示xxx表的结构,如果不写xxx这个时候查的是所有表
表的结构就是你创建表的那句话
sqlite> .schema
CREATE TABLE events(ID INT PRIMARY KEY NOT NULL,TIME INT NOT NULL,CODE INT NOT NULL,ARGC INT NOT NULL,ARGS TEXT);
2.sql语句(sql中的语言)
增删改查:
新建修改删除表
插入修改删除行
查询
2.1新建一张数据表
语法:
create table 表的名称 (列名称 列数据类型, 列名称 数据类型);
例如:
create table persons (id int, name text);
创建一张表 表名为persons 表中两列 分别为id(int类型的)、name(文本类型的)
3.修改表的结构
语法:alter table 表名 rename to 新表名; (修改表的名称)
alter table 表名 add 列名 数据类型; (修改表的结构,添加新的一列)
在sqlite3中没有直接删除一列的sql语句,我们可以先新建一个表,将原表中除了你想删除的那列以外的所有列复制粘贴到新表中,删除旧表,将新表名设置为老表名。
5.插入新的一行数据
语法:
insert into 表名 values (列值1,列值2.....); 全部赋值
insert into 表名 (列名称,列名称) values ( 对应的列值);部分赋值
例如:(切记插入的过程中列值为字符串需要用" ")
6.修改行中数据
语法:
update 表名 set 列名称 = ‘列值’ 匹配条件;(匹配条件where id=10)
例如: 修改haha表中name那一列设置为文静,当你的id为10的那一行
7.删除表中一行数据或者匹配到的多行数据
语法:
delete from 表名 where 匹配条件;
9.数据库的匹配条件的提高版
SQL语句中,字符串是用两个单引号包起来标示的,所以要在字符串里保留单引号,必须要转义,而转义很简单,就是两个连续的单引号就表示一个单引号字符。SQL 使用单引号来环绕文本值(大部分数据库系统也接受双引号)。如果是数值,请不要使用引号。
例如:我们要进行使用用户名、密码进行登录,如果表中有你这个用户名且对应的密码一样,就可以让你登录
select *from haha where id =lanzhou and passwd = 123456;
如果你有显示,也就是查到东西了,这个时候你就可以登录了
常见的匹配条件:
in、and、or、between and、like模糊查找、not(取其补集)
where的高级用法:
in:
允许在where子句中规定多个值
语法:
where 列名称 in( 列值1,列值2,.....);
select *from haha where id in (1,2,3,4);
and:
多个条件结合起来,且
select *from haha where id =lanzhou and passwd = 123456;
or: 或
select *from haha where id =lanzhou or passwd = 123456;
between and:
between A and B,会选取A到B之间的数据
select *from haha where id between 1 and 3;
like:
模糊查找、像,一般用于地址、姓名
select *from haha where id like 3; select *from haha where addr like "%zhou%";
%zhou%表示你的地址中带有zhou的
%表示通配
例如:姓张的人 "张%"
名字带张就行 "%张%"
not:取补集
select *from haha where id not in (1,2,3,4);
排序:
根据指针的列结果集表进行排序
默认按照升序的进行排序的,可以使用关键字desc(降序)
升序:
select*from haha order by 列名;
降序:
select *from haha order by 列名 desc;
例如:
select *from haha order by id;
- 查询不连续的ID
select id from (select id from events order by id asc) t where (select 1 from events where id=t.id-1) is null;
1 6966查询出来的ID的前一个是空值。
create table ev as select * from events order by id asc;
数据库控制
我们的数据库一般都存放在我们的系统的内存上,以文件的形式
一般情况下对文件进行操作,主要有打开、读、写、关闭
我们对数据库的操作也是打开、读、写、关闭,只不过我们使用的是数据库库函数
PRAGMA integrity_check;
1.sqlite写优化
- 关闭写同步,PRAGMA synchronous = OFF(或编译指定宏定义),在 sqlite3 中 synchronous 有三种模式,分别是 FULL,NORMAL 和 OFF,在系统意外终止的时候,安全性逐级减弱,FULL模式下,保证数据不会损坏,安全性最高,写入速度也最慢。OFF 模式会比 FULL 模式快50倍以上。
PRAGMA synchronous = FULL; (2)
PRAGMA synchronous = NORMAL; (1)
PRAGMA synchronous = OFF; (0)
参数含义
当synchronous设置为FULL (2), SQLite数据库引擎在紧急时刻会暂停以确定数据已经写入磁盘。这使系统崩溃或电源出问题时能确保数据库在重起后不会损坏。FULL synchronous很安全但很慢。
当synchronous设置为NORMAL, SQLite数据库引擎在大部分紧急时刻会暂停,但不像FULL模式下那么频繁。 NORMAL模式下有很小的几率(但不是不存在)发生电源故障导致数据库损坏的情况。但实际上,在这种情况 下很可能你的硬盘已经不能使用,或者发生了其他的不可恢复的硬件错误。
synchronous 设置为OFF (0)时,SQLite在传递数据给系统以后直接继续而不暂停。若运行SQLite的应用程序崩溃, 数据不会损伤,但在系统崩溃或写入数据时意外断电的情况下数据库可能会损坏。另一方面,在synchronous OFF时 一些操作可能会快50倍甚至更多。在SQLite 2中,缺省值为NORMAL.而在3中修改为FULL。
sync拓展
数据库为了执行备份,备份文件依赖于写磁盘,然后才会写到数据库文件磁盘中。关键在于这里的写磁盘,都是调用系统的write接口,绝大部分都是直接写缓冲区的,只有调用sync才会将缓冲区中的数据flush到磁盘。所以在write,sync,再wirte再sync的过程中,掉电后是否能恢复数据,依赖于sync是否有真正执行。从这个角度看,FULL和NORMAL的区别,似乎就只有sync调用的频率,FULL按照多人的意见是一个transaction一个sync,而NORMAL是多个transactions调一个sync。
- 使用事务,如果有许多数据需要插入数据库,逐条插入,导致频繁的提交以及磁盘IO,使用事务机制,可以批量插入数据,可以极大的提升写入速度。实际测试中的情况是,开启事务之后,写入速度也可以提升近50倍。
- 执行准备,执行准备相当于将sql语句提前编译,省去每次执行sql语句时候的语法检查等操作,可以极大的优化sql语句的执行效率,其原理有点像 LuaJit 将 Lua 语言成静态机器码,提高运行速度。实测情况中,使用执行准备可以提升40倍的写入速度。
- 内存模式,sqlite3 支持内存模式,将数据库直接创建到内存中,打开地址传入”:memory:”即可,内存模式相比正常模式,可以省区IO的时间,使用内存模式的加速思路是,先将数据库创建到内存中,数据写入完整之后,再调用 “VACUUM INTO ‘out.db3’;” 语句将其写入到磁盘,在开启了执行准备的情况下,这种方式会稍微快上一点点。
- sqlite3* db = nullptr;
- CHECKZERO(sqlite3_open(":memory:", &db)); //内存模式
- CHECKZERO(sqlite3_exec(db, "PRAGMA synchronous = OFF", 0, 0, 0)); //写同步
- CHECKZERO(sqlite3_exec(db, "CREATE TABLE Test(ID INTEGER,var0 INTEGER,var1 REAL,var2 TEXT);", 0, 0, 0));
- // 执行准备
- sqlite3_stmt *pPrepare = nullptr;
- auto sql = "INSERT INTO Test (ID,var0,var1,var2) VALUES (?,?,?,?);";
- CHECKZERO(sqlite3_prepare_v2(db, sql, strlen(sql), &pPrepare, 0));
- CHECKZERO(sqlite3_exec(db, "BEGIN", 0, 0, 0)); //开启事务
- const int maxcount = 10000000;
- for (int i = 0; i < maxcount; i++) {
- CHECKZERO(sqlite3_reset(pPrepare));
- CHECKZERO(sqlite3_bind_int(pPrepare, 1, 0));
- CHECKZERO(sqlite3_bind_int(pPrepare, 2, 1));
- CHECKZERO(sqlite3_bind_double(pPrepare, 3, 2.0));
- const char* str = "hello sqlite3.";
- CHECKZERO(sqlite3_bind_text(pPrepare, 4, str, strlen(str), 0));
- int err = sqlite3_step(pPrepare);
- assert(SQLITE_DONE == err);
- if (i % 10000 == 9999) {
- CHECKZERO(sqlite3_exec(db, "COMMIT", 0, 0, 0));
- CHECKZERO(sqlite3_exec(db, "BEGIN", 0, 0, 0));
- }
- }
- CHECKZERO(sqlite3_exec(db, "COMMIT", 0, 0, 0));
- CHECKZERO(sqlite3_finalize(pPrepare)); // 释放
- // 导出
- CHECKZERO(sqlite3_exec(db, "VACUUM INTO 'out.db3';", 0, 0, 0));
- CHECKZERO(sqlite3_close(db));
2.数据库事务的隔离级别
SQLITE_API int sqlite3_exec(
Read uncommitted
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
3.sqlite3_exec的回调函数 callback
我们还是来先看下回调函数的参数:
typedef int(*sqlite_callback)(void* para, int columenCount, char** columnValue, char** columnName);
参数:
para : 由sqlite3_exec传入的参数指针,或者说是指针参数
columnCount: 查询到的这一条记录由多少个字段(多少列)
columnValue : 该参数是双指针,查询出来的数据都保存在这里,它是一个1维数组,每一个元素都是一
个char*,是一个字段内容,所以这个参数就可以不是单字节,而是可以为字符串等不定
长度的数值,用字符串表示,以'\0'结尾。
columnName : 该参数是双指针,语columnValue是对应的,表示这个字段的字段名称,
返回 : 执行成果则返回SQLITE_OK,否则返回其他值
这里面有几个地方容易理解错,回调函数的参数一定是 sql功能命令执行结果的进一步处理,其中para好理解,就是sqlite3_exec传递的参数,
columnCount:表示sql功能结果的“字段”,也就是“列”的个数,没错,就是“列”的个数。
另外需要特别注意的是:回调函数多数时候不是执行1次,而是会循环执行n次,当我们使用select进行sql功能时,往往输出的结果会是 多行,那么 有n行,就会执行n次的 回调函数。
- sqlite同步机制
(1)SQLite3 数据库的锁状态
锁只是加载操作系统的磁盘缓冲区,而不是磁盘本身。因此,一旦操作系统崩溃或者停电,锁会立即消失。当然创建该锁的进程消失,该锁也会一起消失。
UNLOCKED
加锁四种:
SHARED (共享锁),可以读,不能写
RESERVED (保留锁), 表示数据库被写,但写在缓存中,还不能写入数据库,因为有其他链接持有共享锁在读数据。数据库只能有一个保留锁, 保留锁可以和共享锁共存与PENDING锁的不同之处在于还能获得新的共享锁,PENDING锁被激活时, 不能再获得共享锁。
PENDING(未决)此时,其他链接不再能获取共享锁,即,等待读的链接退出,不接受新的链接来读
EXCLUSIVE(排他),读的链接都退出了,可以写入数据库了。一旦获取了独占锁,我们就知道再也没有其他进程在读取此数据库文件了,此时修改此文件是安全的了。通常,这些变更只会发生在操作系统磁盘缓存中,并不会写入到磁盘中去。之后写入磁盘。删除日志。释放锁。
sqlite 锁状态很容易理解: 多个链接可以同时从一个paper中读数据,但只允许一个链接往paper写数据,而且在写数据的时候不允许有其他链接读数据,防止造成数据不一致。另外,链接持有共享锁时,可以从paper中读数据,持有保留锁时可以往缓存中写数据和修改,如果想要把数据写入paper中,就要限制新的链接获取共享锁、等待有共享锁的链接执行完毕,此时称为未决。
最初的状态是未加锁状态,在此状态下,连接还没有存取数据库。 当连接到了一个数据库,
甚至已经用 BEGIN 开始了一个事务时,连接都还处于未加锁状态。
未加锁状态的下一个状态是共享状态。为了能够从数据库中读(不写)数据,连接必须首先进
入共享状态,也就是首先要获得一个共享锁。多个连接可以同时获得并保持共享锁,也就是
说多个连接可以同时从同一个数据库中读数据。但即使仅有一个共享锁没有释放,也不允许
任何连接写数据库。
如果一个连接想要写数据库,它必须首先获得一个保留锁。一个数据库上同时只能有一个保
留锁。保留锁可以与共享锁共存,保留锁是写数据库的第 1 阶段。保留锁即不阻止其它拥有
共享锁的连接继续读数据库,也不阻止其它连接获得新的共享锁。
一旦一个连接获得了保留锁,它就可以开始处理数据库修改操作了,尽管这些修改只能在
缓冲区中进行,而不是实际地写到磁盘。对读出内容所做的修改保存在内存缓冲区中。
当连接想要提交修改(或事务)时,需要将保留锁提升为排它锁。为了得到排它锁,还必须首
先将保留锁提升为未决锁。 获得未决锁之后,其它连接就不能再获得新的共享锁了,但已经拥有共享锁的连接仍然可以继续正常读数据库。此时,拥有未决锁的连接等待其它拥有共享锁的连接完成工作并释放其共享锁。
一旦所有其它共享锁都被释放,拥有未决锁的连接就可以将其锁提升至排它锁,此时就可
以自由地对数据库进行修改了。所有以前对缓冲区所做的修改都会被写到数据库文件。
(2)日志文件
在获得保留锁后,SQLite会生成一个单独的回滚日志文件,并将要更改的数据库页的原始内容写入回滚日志。回滚日志文件意味它将包含了所有可以将数据库文件恢复到原始状态的数据。。数据库文件中被修改的页码及他们的内容都被写进了回滚日志文件中。
在官网的介绍中指出,SQLite支持通过PRAGMA journal_mode=?的形式设置日志模式,而当前支持的日志模式有如下几种:
SQLite数据库连接默认为DELETE模式
DELETE、TRUNCATE、PERSIST、MEMORY、WAL、OFF
除去MEMORY与OFF两种不常用的模式,其余四种实际上可以分为两个大类:WAL(预写日志)与Rollback(回滚日志)模式。
日志模式按照其行为的不同,可以分为WAL日志与Rollback日志模式
DELETE模式:
SQLite的原子提交及WAL日志模式 - 简书 (jianshu.com)
(54条消息) [SQLite]浅析其二——SQLite数据库的日志_sqlite 日志_Ryan ZHENG的博客-CSDN博客
WAL模式:
WAL日志模式,可以实现读写并发,但还是写独占(与delete锁机制完全不同)
相比默认的日志模式,WAL日志模式有利也有弊。优点包括:
- 在大多数情况下,WAL速度更快。
- WAL进一步提升了数据库的并发性,因为读不会阻塞写,而写也不会阻塞读。读和写可以并发执行。
- 使用WAL,磁盘I / O操作更有秩序。
- WAL减少了fsync()操作次数,因此在fsync()系统调用被破坏的系统上不易受到问题的影响。
缺点有:
- WAL通常要求 VFS 支持共享内存原语。
- 使用数据库的所有进程必须位于同一台主机上; WAL无法在网络文件系统上运行。
- 在读取操作远多于写入操作的应用程序中,WAL可能比传统的日志模式稍慢(可能慢1%或2%)。
- 每个数据库文件都关联了额外的 .wal 文件和 .shm 共享内存文件。
启用:
WAL日志模式是持久的。如果进程设置WAL模式,然后关闭并重新打开数据库,数据库返回的日志模式仍然是WAL。相反,如果进程设置PRAGMA journal_mode = TRUNCATE然后关闭并重新打开,则数据库将以默认的日志模式(DELETE模式)。
(54条消息) sqlite的wal模式_sqlite journal mode=wal_冷兮公子的博客-CSDN博客
Sqlite学习笔记(四)&&SQLite-WAL原理 - 天士梦 - 博客园 (cnblogs.com)
close db是能保证落盘的,所以如果工程中,所有数据库操作都有close()作为节点,那么下一步只复制db文件也没错(如果完整close,应该没有wal文件了)
(3)并发性
要想保证线程安全的话,可以有这4种方式:
- SQLite使用单线程模式,用一个专门的线程访问数据库。
- SQLite使用单线程模式,用一个线程队列来访问数据库,队列一次只允许一个线程执行,队列里的线程共用一个数据库连接。
- SQLite使用多线程模式,每个线程创建自己的数据库连接。
- SQLite使用串行模式,所有线程共用全局的数据库连接。
线程模式:
如果没有SQLITE_THREADSAFE则编译时参数为 存在,则使用序列化模式。 这可以通过 -DSQLITE_THREADSAFE=1 来明确说明。 当 -DSQLITE_THREADSAFE=0 时,线程模式为 单线程。当 -DSQLITE_THREADSAFE=2 时,线程模式为 多线程。
线程模式可以在编译时(通过源码编译sqlite库时)、启动时(使用sqlite的应用程序初始化时)或者运行时(创建数据库连接时)来指定。一般而言,运行时指定的模式将覆盖启动时的指定模式,启动时指定的模式将覆盖编译时指定的模式。但是,单线程模式一旦被指定,将无法被覆盖。默认的线程模式是串行模式。
单线程-DSQLITE_THREADSAFE=0:
·SQLite 采用单线程模型,用专门的线程/队列(同时只能有一个任务执行访问) 进行访问(自己确认是否为单线程)
我们知道没有其他的嵌入式 SQL数据库引擎比SQLite支持更多的并发性。 SQLite允许多进程 同时打开和读取数据库。任何一个进程需要写入时,整个数据库将在这一过程中被锁定。但这一般仅耗时 几毫秒。其他进程只需等待然后继续其他事务。其他嵌入式SQL数据库引擎往往只允许单进程访问数据库。
如果你的应用需要很高的并发度,你应该考虑使用client/server数据库。事实上,经验告诉 我们大多数应用所需要的并发度比他们的设计者们想象的要少得多。
当 SQLite 尝试操作一个被另一个进程锁定的文件时,缺省的行为是返回 SQLITE_BUSY。你可以用 C代码更改这一行为。 使用 sqlite3_busy_handler() 或sqlite3_busy_timeout() API函数。
SQLite在多线程环境下的应用 - 袁军峰 - 博客园 (cnblogs.com)
一个sqlite3结构只能在调用 sqlite3_open创建它的那个进程中使用。你不能在一个线程中打开一个数据库然后把指针传递给另一个线程使用。
在UNIX下,你不能通过一个 fork() 系统调用把一个打开的 SQLite 数据库放入子过程中,否则会出错。
5.返回SQLITE_BUSY和错误码:
1、当有写操作时,其他读操作会被驳回
2、当有写操作时,其他写操作会被驳回
3、当开启事务时,在提交事务之前,其他写操作会被驳回
4、当开启事务时,在提交事务之前,其他事务请求会被驳回
5、当有读操作时,其他写操作会被驳回
6、读操作之间能够并发执行
SQLITE_OK = 0; 返回成功
SQLITE_ERROR = 1; SQL错误或错误的数据库
SQLITE_INTERNAL = 2; An internal logic error in SQLite
SQLITE_PERM = 3; 拒绝访问
SQLITE_ABORT = 4; 回调函数请求中断
SQLITE_BUSY = 5; 数据库文件被锁
SQLITE_LOCKED = 6; 数据库中的一个表被锁
SQLITE_NOMEM = 7; 内存分配失败
SQLITE_READONLY = 8; 试图对一个只读数据库进行写操作
SQLITE_INTERRUPT = 9; 由sqlite_interrupt()结束操作
SQLITE_IOERR = 10; 磁盘I/O发生错误
SQLITE_CORRUPT = 11; 数据库磁盘镜像畸形
SQLITE_NOTFOUND = 12; (Internal Only)表或记录不存在
SQLITE_FULL = 13; 数据库满插入失败
SQLITE_CANTOPEN = 14; 不能打开数据库文件
SQLITE_PROTOCOL = 15; 数据库锁定协议错误
SQLITE_EMPTY = 16; (Internal Only)数据库表为空
SQLITE_SCHEMA = 17; 数据库模式改变
SQLITE_TOOBIG = 18; 对一个表数据行过多
SQLITE_CONSTRAINT = 19; 由于约束冲突而中止
SQLITE_MISMATCH = 20; 数据类型不匹配
SQLITE_MISUSE = 21; 数据库错误使用(没有打开DB或者关闭后操作)并发操作
SQLITE_NOLFS = 22; 使用主机操作系统不支持的特性
SQLITE_AUTH = 23; 非法授权
SQLITE_FORMAT = 24; 辅助数据库格式错误
SQLITE_RANGE = 25; 2nd parameter to sqlite_bind out of range
SQLITE_NOTADB = 26; 打开的不是一个数据库文件
SQLITE_ROW = 100; sqlite_step() has another row ready
SQLITE_DONE = 101; sqlite_step() has finished executing
6.解析SQLite中的常见问题与总结详解
1、 创建数据
如果不往数据库里面添加任何的表,这个数据库等于没有建立,不会在硬盘上产生任何文件,如果数据库已经存在,则会打开这个数据库。
2、 如何通过sqlite3.dll与sqlite3.def生成sqlite3.lib文件
LIB /DEF:sqlite3.def /machine:IX86
3、 sqlite3_open打开一个数据库时,如果数据库不存在就会新生成一个数据库文件。如果接着执行其他查询语句就会失败,比如sqlite3_prepare,编程中出现明明指定了数据库而且里面也有数据,为什么查询失败了,主要是数据库名路径不对引起的。一般的做法是先检查数据库文件是否存在,如果存在就使用sqlite3_open打开数据库;否则创建一个新的数据库。
4、 如何建立自动增长字段
声明为INTEGER PRIMARY KEY的列将会自动增长。
5、SQLite3支持何种数据类型?
NULL
INTEGER
REAL
TEXT
BLOB
但实际上,sqlite3也接受如下的数据类型:
smallint 16位元的整数。
interger 32位元的整数。
decimal(p,s) p精确值和s大小的十进位整数,精确值p是指全部有几个数(digits)大小值,s是指小数点後有几位数。如果没有特别指定,则系统会设为p=5; s=0。
float 32位元的实数。
double 64位元的实数。
char(n) n长度的字串,n不能超过254。
varchar(n)长度不固定且其最大长度为n的字串,n不能超过4000。
graphic(n)和char(n)一样,不过其单位是两个字元double-bytes,n不能超过127。这个形态是为了支援两个字元长度的字体,例如中文字。
vargraphic(n)可变长度且其最大长度为n的双字元字串,n不能超过2000。
date包含了年份、月份、日期。
time包含了小时、分钟、秒。
timestamp包含了年、月、日、时、分、秒、千分之一秒。
6、SQLite允许向一个integer型字段中插入字符串
这 是一个特性,而不是一个bug。SQLite不强制数据类型约束。任何数据都可以插入任何列。你可以向一个整型列中插入任意长度的字符串,向布尔型列中插 入浮点数,或者向字符型列中插入日期型值。在CREATE TABLE中所指定的数据类型不会限制在该列中插入任何数据。任何列均可接受任意长度的字符串(只有一种情况除外:标志为INTEGER PRIMARY KEY的列只能存储64位整数,当向这种列中插数据除整数以外的数据时,将会产生错误。
但SQLite确实使用声明的列类型来指示你所期望的格式。所以,例如你向一个整型列中插入字符串时,SQLite会试图将该字符串转换成一个整数。如果可以转换,它将插入该整数;否则,将插入字符串。这种特性有时被称为类型或列亲和性(type or column affinity).
7、为什么SQLite不允许在同一个表不同的两行上使用0和0.0作主键?
主键必须是数值类型,将主键改为TEXT型将不起作用。
每一行必须有一个唯一的主键。对于一个数值型列,SQLite认为'0'和'0.0'是相同的,因为他们在作为整数比较时是相等的(参见上一问题)。所以,这样值就不唯一了。
8、多个应用程序或一个应用程序的多个实例可以同时访问同一个数据库文件吗?
多个进程可同时打开同一个数据库。多个进程可以同时进行SELECT操作,但在任一时刻,只能有一个进程对数据库进行更改。
SQLite使用读、写锁控制对数据库的访问。(在Win95/98/ME等不支持读、写锁的系统下,使用一个概率性的模拟来代替。)但使用时要注意:如果数据库文件存放于一个NFS文件系统上,这种锁机制可能不能正常工作。这 是因为fcntl()文件锁在很多NFS上没有正确的实现。在可能有多个进程同时访问数据库的时候,应该避免将数据库文件放到NFS上。在Windows 上,Microsoft的文档中说:如果使用FAT文件系统而没有运行share.exe守护进程,那么锁可能是不能正常使用的。那些在Windows上 有很多经验的人告诉我:对于网络文件,文件锁的实现有好多Bug,是靠不住的。如果他们说的是对的,那么在两台或多台Windows机器间共享数据库可能 会引起不期望的问题。
我们意识到,没有其它嵌入式的SQL数据库引擎能象SQLite这样处理如此多的并发。SQLite允许多个进程同时打开一个数据库,同时读一个数据库。当有任何进程想要写时,它必须在更新过程中锁住数据库文件。但那通常只是几毫秒的时间。其它进程只需等待写进程干完活结束。典型地,其它嵌入式的SQL数据库引擎同时只允许一个进程连接到数据库。
但 是,Client/Server数据库引擎(如PostgreSQL, MySQL,或Oracle)通常支持更高级别的并发,并且允许多个进程同时写同一个数据库。这种机制在Client/Server结构的数据库上是可能 的,因为总是有一个单一的服务器进程很好地控制、协调对数据库的访问。如果你的应用程序需要很多的并发,那么你应该考虑使用一个 Client/Server结构的数据库。但经验表明,很多应用程序需要的并发,往往比其设计者所想象的少得多。
当SQLite试图访问一个被其它进程锁住的文件时,缺省的行为是返回SQLITE_BUSY。可以在C代码中使用sqlite3_busy_handler()或sqlite3_busy_timeout() API函数调整这一行为。
9、SQLite线程安全吗?
线程是魔鬼(Threads are evil)。避免使用它们。
SQLite 是线程安全的。由于很多用户会忽略我们在上一段中给出的建议,我们做出了这种让步。但是,为了达到线程安全,SQLite在编译时必须将 SQLITE_THREADSAFE预处理宏置为1。在Windows和Linux上,已编译的好的二进制发行版中都是这样设置的。如果不确定你所使用的 库是否是线程安全的,可以调用sqlite3_threadsafe()接口找出。
10、在SQLite数据库中如何列出所有的表和索引?
如果你运行sqlite3命令行来访问你的数据库,可以键入“.tables”来获得所有表的列表。或者,你可以输入“.schema”来看整个数据库模式,包括所有的表的索引。输入这些命令,后面跟一个LIKE模式匹配可以限制显示的表。
11、SQLite数据库有已知的大小限制吗?
在Windows和Unix下,版本2.7.4的SQLite可以达到2的41次方字节(2T字节)。老版本的为2的31次方字节(2G字节)。
SQLite版本2.8限制一个记录的容量为1M。SQLite版本3.0则对单个记录容量没有限制。
表名、索引表名、视图名、触发器名和字段名没有长度限制。但SQL函数的名称(由sqlite3_create_function() API函数创建)不得超过255个字符。
12、在SQLite中,VARCHAR字段最长是多少?
SQLite不强制VARCHAR的长度。你可以在SQLITE中声明一个VARCHAR(10),SQLite还是可以很高兴地允许你放入500个字符。并且这500个字符是原封不动的,它永远不会被截断。
13、在SQLite中,如何在一个表上添加或删除一列?
SQLite有有限地ALTER TABLE支持。你可以使用它来在表的末尾增加一列,可更改表的名称。如果需要对表结构做更复杂的改变,则必须重新建表。重建时可以先将已存在的数据放到一个临时表中,删除原表,创建新表,然后将数据从临时表中复制回来。
如,假设有一个t1表,其中有"a", "b", "c"三列,如果要删除列c,以下过程描述如何做:
BEGIN TRANSACTION;
CREATE TEMPORARY TABLE t1_backup(a,b);
INSERT INTO t1_backup SELECT a,b FROM t1;
DROP TABLE t1;
CREATE TABLE t1(a,b);
INSERT INTO t1 SELECT a,b FROM t1_backup;
DROP TABLE t1_backup;
COMMIT;
14、在SQLite中支持分页吗?
SQLite分页是世界上最简单的。如果我要去11-20的Account表的数据Select * From Account Limit 9 Offset 10;
以上语句表示从Account表获取数据,跳过10行,取9行。这个特性足够让很多的web中型网站使用这个了。也可以这样写 select * from account limit10,9和上面的的效果一样。这种写法MySQL也支持。
7.30种mysql优化sql语句查询的方法
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by涉及的列上建立索引。
2.应尽量避免在 where 子句中使用 !=或<> 操作符,否则将引擎放弃使用索引而进行全表扫描。
3.应尽量避免在 where 子句中对字段进行 null 值 判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
4.应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20
5.下面的查询也将导致全表扫描:
select id from t where name like '%abc%'
对于 like '..%' (不以 % 开头),可以应用 colunm上的index
6.in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
7.如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
select id from t where num=@num
可以改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num
8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2
9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where substring(name,1,3)='abc'--name以abc开头的id
select id from t where datediff(day,createdate,'2005-11-30')=0--'2005-11-30'生成的id
应改为:
select id from t where name like 'abc%'
select id from t where createdate>='2005-11-30' and createdate<'2005-12-1'
10.不要在 where 子句中的“=”【左边】进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
11.在使用索引字段作为条件时,如果该索引是【复合索引】,那么必须使用到该索引中的【第一个字段】作为条件时才能保证系统使用该索引,否则该索引将不会被使用。并且应【尽可能】的让字段顺序与索引顺序相一致。(字段顺序也可以不与索引顺序一致,但是一定要包含【第一个字段】。)
12.不要写一些没有意义的查询,如需要生成一个空表结构:
select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(...)
13.很多时候用 exists 代替 in 是一个好的选择:
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)
14.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。
15.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
16.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
17.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
18.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
19.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
20.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
21.避免频繁创建和删除临时表,以减少系统表资源的消耗。
22.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。
23.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。
27.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。
28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。
29.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
30.尽量避免大事务操作,提高系统并发能力。
上面有几句写的有问题。
第二方面:
select Count (*)和Select Count(1)以及Select Count(column)区别
一般情况下,Select Count (*)和Select Count(1)两着返回结果是一样的
假如表沒有主键(Primary key), 那么count(1)比count(*)快,
如果有主键的話,那主键作为count的条件时候count(主键)最快
如果你的表只有一个字段的话那count(*)就是最快的
count(*) 跟 count(1) 的结果一样,都包括对NULL的统计,而count(column) 是不包括NULL的统计
第三方面:
索引列上计算引起的索引失效及优化措施以及注意事项
创建索引、优化查询以便达到更好的查询优化效果。但实际上,MySQL有时并不按我们设计的那样执行查询。MySQL是根据统计信息来生成执行计划的,这就涉及索引及索引的刷选率,表数据量,还有一些额外的因素。
简而言之,当MYSQL认为符合条件的记录在30%以上,它就不会再使用索引,因为mysql认为走索引的代价比不用索引代价大,所以优化器选择了自己认为代价最小的方式。事实也的确如此
是MYSQL认为记录是30%以上,而不是实际MYSQL去查完再决定的。都查完了,还用什么索引啊?!
MYSQL会先估算,然后决定是否使用索引。
VPN
按VPN的协议分类:
VPN的隧道协议主要有三种,PPTP、L2TP和IPSec,其中PPTP和L2TP协议工作在OSI模型的第二层,又称为二层隧道协议;IPSec是第三层隧道协议。
1.VLAN
全网最详细的VLAN的原理和配置 - 知乎 (zhihu.com)
VLAN ( Virtual Local Area Network )即虚拟局域网,是将一个物理的局域网在逻辑上划分成多个广播域的技术。通过在交换机上配置VLAN,可以实现在同一个VLAN内的用户可以进行二层互访,而不同VLAN间的用户被二层隔离。这样既能够隔离广播域,又能够提升网络的安全性。
VLAN间通信的限制:
Ÿ 每个VLAN都是一个独立的广播域,不同的VLAN之间二层就已经隔离,因此属于不同VLAN的节点之间无法直接访问
Ÿ 需要引入路由技术来实现不同VLAN之间的通信。VLAN路由可以使用路由器来实现,也可以通过三层交换机来实现
(1)VLAN的基本原理
交换机内部处理的数据帧都带有VLAN标签。而交换机连接的部分设备(如用户主机、服务器)只会收发不带VLAN tag的传统以太网数据帧。因此,要与这些设备交互,就需要交换机的接口能够识别传统以太网数据帧,并在收发时给帧添加、剥除VLAN标签。添加什么VLAN标签,由接口上的缺省VLAN(Port Default VLAN ID,PVID)决定。
要使交换机能够分辨不同VLAN的报文,需要在报文中添加标识VLAN信息的字段。IEEE 802.1Q协议规定,在以太网数据帧的目的MAC地址和源MAC地址字段之后、协议类型字段之前加入4个字节的VLAN标签(又称VLAN Tag,简称Tag),用以标识VLAN信息。如图8-5所示。
TPID:表示数据帧类型,取值为0x8100时表示IEEE 802.1Q的VLAN数据帧。如果不支持802.1Q的设备收到这样的帧,会将其丢弃。
PRI:表示数据帧的优先级,用于QoS。
CFI:在以太网中,CFI的值为0。
VID:表示该数据帧所属VLAN的编号。VLAN ID取值范围是0~4095。
(2)VLAN的划分方式
计算机发出的数据帧不带任何标签。对已支持VLAN特性的交换机来说,当计算机发出的Untagged帧一旦进入交换机后,交换机必须通过某种划分原则把这个帧划分到某个特定的VLAN中去。
VLAN的划分包括如下5种方法:
基于接口划分:根据交换机的接口来划分VLAN。
基于MAC地址划分:根据数据帧的源MAC地址来划分VLAN。
基于IP子网划分:根据数据帧中的源IP地址和子网掩码来划分VLAN。
基于协议划分:根据数据帧所属的协议(族)类型及封装格式来划分VLAN。
基于策略划分:根据配置的策略划分VLAN,能实现多种组合的划分方式,包括接口、MAC地址、IP地址等。
基于IP地址划分:
根据数据帧中的源IP地址和子网掩码来划分VLAN。网络管理员预先配置IP地址和VLAN ID映射关系表,当交换机收到的是Untagged帧,就依据该表给数据帧添加指定VLAN的Tag。然后数据帧将在指定VLAN中传输。 | 优点:当用户的物理位置发生改变,不需要重新配置VLAN。可以减少网络通信量,可使广播域跨越多个交换机。缺点:网络中的用户分布需要有规律,且多个用户在同一个网段 | 适用于对安全需求不高、对移动性和简易管理需求较高的场景中。比如,一台PC配置多个IP地址分别访问不同网段的服务器,以及PC切换IP地址后要求VLAN自动切换等场景。 |
(3)Access和Trunk原理详解
对于Access:
要处理的数据 进行的操作
收到一个不含VLAN ID的数据包 将该数据包打上Access的PVID
收到一个含VLAN ID的数据包 如果该VLAN ID与Access的PVID相同,则接收;如果该VLAN ID与Access的PVID不同,则丢弃
发送一个含VLAN ID的数据包 如果该VLAN ID与Access的PVID相同,则剥离VLAN标签后发送;如果该VLAN ID与Access的PVID不同,则丢弃
注:有的同学问如果要发送一个不含VLAN ID的数据包会怎么样,其实这种状况压根不会出现,因为华为和思科交换机在默认情况下每个端口都为Access类型,属于VLAN1。(华为默认为Hybrid类型,也可以按照上述理解)
对于Trunk:
要处理的数据 进行的操作
收到一个不含VLAN ID的数据包 将该数据包打上Trunk端口PVID的标签
收到一个含VLAN ID的数据包 如果该VLAN ID与Trunk的PVID相同,则接收;如果该VLAN ID与Trunk的PVID不同,则丢弃
发送一个含VLAN ID的数据包 如果Trunk端口配置允许该VLAN的数据包通过,则保留VLAN ID通过;如果Trunk端口配置不允许该VLAN的数据包通过,则丢弃
计算机网络误区——VLAN中Access和Trunk原理详解_access模式和trunk模式的工作原理-CSDN博客
2.VXLAN
两口交换机相当于一个网桥
(1)为什么需要 VXLAN?
VLAN ID 数量限制:
VLAN tag 总共有 4 个字节,其中有 12 bit 用来标识不同的二层网络(即 LAN ID),故而最多只能支持 $2^{12}$,即 4096 个子网的划分。而虚拟化(虚拟机和容器)的兴起使得一个数据中心会有成千上万的机器需要通信,这时候 VLAN 就无法满足需求了。而 VXLAN 的报文 Header 预留了 24 bit 来标识不同的二层网络(即 VNI,VXLAN Network Identifier),即 3 个字节,可以支持 $2^{24}$ 个子网。
交换机 MAC 地址表限制:
对于同网段主机的通信而言,报文到底交换机后都会查询 MAC 地址表进行二层转发。数据中心虚拟化之后,VM 的数量与原有的物理机相比呈数量级增长,而应用容器化之后,容器与 VM 相比也是呈数量级增长。。。而交换机的内存是有限的,因而 MAC 地址表也是有限的,随着虚拟机(或容器)网卡 MAC 地址数量的空前增加,交换机表示压力山大啊!
而 VXLAN 就厉害了,它用 VTEP(后面会解释)将二层以太网帧封装在 UDP 中,一个 VTEP 可以被一个物理机上的所有 VM(或容器)共用,一个物理机对应一个 VTEP。从交换机的角度来看,只是不同的 VTEP 之间在传递 UDP 数据,只需要记录与物理机数量相当的 MAC 地址表条目就可以了,一切又回到了和从前一样。
虚机或容器迁移范围受限:
VLAN 与物理网络融合在一起,不存在 Overlay 网络,带来的问题就是虚拟网络不能打破物理网络的限制。举个例子,如果要在 VLAN 100 部署虚拟机(或容器),那只能在支持 VLAN 100 的物理设备上部署。
(2)VXLAN协议原理
太网报文承载到某种隧道层面,差异性在于选择和构造隧道的不同,而底层均是 IP 转发。
那三层转发和二层转发有什么区别呢?
1.数据转发依靠的关键字不同,二层转发主要依靠MAC地址,而三层转发主要依靠IP地址。
2.数据交换的范围不同,二层交换指在同一网段内的通信,三层交换指跨网段的通信。
3.在三层转发的过程中,还要进行二层的封装。也就是说,在三层转发过程中二层帧头中的(源、目的)MAC地址是要改变的。但是IP数据报中的源IP和目的IP地址是不会改变的
所以,主机完成7层封包,通过二三层转发。
(3)数据报文转发和需要的配置
总的来说,VXLAN 报文的转发过程就是:原始报文经过 VTEP,被 Linux 内核添加上 VXLAN 头部以及外层的 UDP 头部,再发送出去,对端 VTEP 接收到 VXLAN 报文后拆除外层 UDP 头部,并根据 VXLAN 头部的 VNI 把原始报文发送到目的服务器。但这里有一个问题,第一次通信前双方如何知道所有的通信信息?这些信息包括:
哪些 VTEP 需要加到一个相同的 VNI 组?
发送方如何知道对方的 MAC 地址?
如何知道目的服务器在哪个节点上(即目的 VTEP 的地址)?
第一个问题简单,VTEP 通常由网络管理员来配置。要回答后面两个问题,还得回到 VXLAN 协议的报文上,看看一个完整的 VXLAN 报文需要哪些信息:
内层报文 : 通信双方的 IP 地址已经明确,只需要 VXLAN 填充对方的 MAC 地址,因此需要一个机制来实现 ARP 功能。
VXLAN 头部 : 只需要知道 VNI。一般直接配置在 VTEP 上,要么提前规划,要么根据内层报文自动生成。
UDP 头部 : 需要知道源端口和目的端口,源端口由系统自动生成,目的端口默认是 4789。
IP 头部 : 需要知道对端 VTEP 的 IP 地址,这个是最关键的部分。
实际上,VTEP 也会有自己的转发表,转发表通过泛洪和学习机制来维护,对于目标 MAC 地址在转发表中不存在的未知单播,广播流量,都会被泛洪给除源 VTEP 外所有的 VTEP,目标 VTEP 响应数据包后,源 VTEP 会从数据包中学习到 MAC,VNI 和 VTEP 的映射关系,并添加到转发表中,后续当再有数据包转发到这个 MAC 地址时,VTEP 会从转发表中直接获取到目标 VTEP 地址,从而发送单播数据到目标 VTEP。
(4)IP-link
ip link set
- ip link set DEVICE { up | down | arp { on | off } | name NEWNAME | address LLADDR }
- 选项说明:
- dev DEVICE:指定要操作的设备名
- up and down:启动或停用该设备
- arp on or arp off:启用或禁用该设备的arp协议
- name NAME:修改指定设备的名称,建议不要在该接口处于运行状态或已分配IP地址时重命名
- address LLADDRESS:设置指定接口的MAC地址
Ip link show
- 语法格式:
- ip [ -s | -h | -d ] link show [dev DEV]
- 选项说明:
- -s[tatistics]:将显示各网络接口上的流量统计信息;
- -h[uman-readable]:以人类可读的方式显式,即单位转换;
- -d[etails]:显示详细信息
- (选项说明可以通过ip help查看)
例:
ip -s -d -h link show vxlan13
各字段含义说明:
- BROADCAST:支持广播
- MULTICAST:支持组播
- UP:代表网卡开启状态;如果是关闭状态则不显示UP(重要)
- LOWER_UP:有说法是代表网卡的网线被接上,自己测试验证发现使用ifconfig eth0 down后,UP和LOWER_UP均不显示;
- 使用ifconfig eth0 up后,UP和LOWER_UP均显示(重要)
- 参考补充(3)
- mtu 1500:网络接口的最大传输单元(Maximum Transmission Unit ):1500字节。是包或帧的最大长度,一般以字节记。
- qdisc:排队规则
- state UNKNOWN :
- mode DEFAULT :
- group default :
- qlen 1000:
- link/ether 00:01:02:a4:71:28 表示物理网卡地址
- brd ff:ff:ff:ff:ff:ff
- promiscuity 0
- numtxqueues 8
- numrxqueues 8
(5)brctl
brctl 使用说明 - Linux操作系统:Ubuntu_Centos_Debian - 红黑联盟 (2cto.com)
Linux网关模式下将有线LAN和无线LAN共享网段实现局域网内互联 - Linux操作系统:Ubuntu_Centos_Debian - 红黑联盟 (2cto.com)
- addbr bridge的名称 #添加bridge;
- delbr bridge的名称 #删除bridge;
- addif bridge的名称device的名称 #添加接口到bridge;
- delif bridge的名称device的名称 #从bridge中删除接口
- setageing bridge的名称 时间 #设置老化时间,即生存周期
- setbridgeprio bridge的名称 优先级#设置bridge的优先级
- setfd bridge的名称 时间 #设置bridge转发延迟时间
- sethello bridge的名称 时间 #设置hello时间
- setmaxage bridge的名称 时间 #设置消息的最大生命周期
- setpathcost bridge的名称 端口 权重#设置路径的权值
- setportprio bridge的名称 端口 优先级#设置端口的优先级
- show #显示bridge列表
- showmacs bridge的名称 #显示MAC地址
- showstp bridge的名称 #显示bridge的stp信息
- stp bridge的名称{on|off} #开/关stp
# brctl addif br0 eth0 (让eth0成为br0的一个端口)
# brctl addif br0 eth1 (让eth1成为br0的一个端口)
5.进程信息/proc/pid/status
查看进程状态信息如下:
more status
Name: rsyslogd
State: S (sleeping)
Tgid: 987
Pid: 987
PPid: 1
TracerPid: 0
Uid: 0 0 0 0
Gid: 0 0 0 0
Utrace: 0
FDSize: 32
Groups:
VmPeak: 36528 kB
VmSize: 36528 kB
VmLck: 0 kB
VmHWM: 1432 kB
VmRSS: 1420 kB
VmData: 33980 kB
VmStk: 88 kB
VmExe: 320 kB
VmLib: 2044 kB
VmPTE: 56 kB
VmSwap: 0 kB
Threads: 3
SigQ: 1/7954
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000001001206
SigCgt: 0000000180014c21
CapInh: 0000000000000000
CapPrm: ffffffffffffffff
CapEff: ffffffffffffffff
CapBnd: ffffffffffffffff
Cpus_allowed: 3
Cpus_allowed_list: 0-1
Mems_allowed: 1
Mems_allowed_list: 0
voluntary_ctxt_switches: 1
nonvoluntary_ctxt_switches: 0
Tgid: 987
解释:Tgid是线程组的ID,一个线程一定属于一个线程组(进程组).
Pid: 987
解释:这个是进程的ID,更准确的说应该是线程的ID.
TracerPid: 0
解释:跟踪当前进程的进程ID,如果是0,表示没有跟踪.
例如:
用strace跟踪top程序
FDSize: 32
解释:
FDSize是当前分配的文件描述符,这个值不是当前进程使用文件描述符的上限.
我们看到这里是32,但实际并没有分配32个文件,如下:
ls -l /proc/`pgrep rsyslogd|grep -v grep`/fd
total 0
我们看到这里只用到了18个文件描述符.而如果超过32个文件描述符,将以32进行递增,如果是64位系统,将以64进行递增.
FDSize这个值不会减少,如果我们程序打开了300个文件,并不会因为关闭文件,而减少FDSize这个值.
VmPeak: 36528 kB
解释:这里的VmPeak代表当前进程运行过程中占用内存的峰值.
我们用下面的程序申请内存,然后释放内存,最后通pause()函数中止程序的运行
注:我们看到程序申请了10240kb(10MB)的内存,VmPeak的值为11852kb,为什么不是10MB呢,因为除了我们申请的内存外,程序还会为加载动态链接库而占用内存.
VmSize: 36528 kB
解释:VmSize代表进程现在正在占用的内存
这个值与pmap pid的值基本一致,如果略有不同,可能是内存裂缝所造成的
VmLck: 0 kB
解释:VmLck代表进程已经锁住的物理内存的大小.锁住的物理内存不能交换到硬盘.
if (mlock((const void *)array, sizeof(array)) == -1) {
perror("mlock: ");
return -1;
}
我们看到Vmlck的值为4Kb,这是因为分配的最少单位是4KB,以后每次递增都是4KB的整数倍.
VmHWM: 1432 kB
VmRSS: 1420 kB
解释:
VmHWM是程序得到分配到物理内存的峰值.
VmRSS是程序现在使用的物理内存.
const size_t stride = sysconf(_SC_PAGE_SIZE);
for (i = 0;i < nbytes; i+= stride) {
ptr[i] = 0;
}
注意这个程序在每页都修改一个字节的数据,导致系统必须为它分配占用物理内存.
看到执行后还有40MB空闲物理内存.
我们下面再申请100MB的内存,此时系统会通过物理内存和SWAP的置换操作,把第1次运行的test进程所占用的物理内存置换到SWAP,把空出来的物理内存分配给第2次运行的程序,
最后我们看到VmHWM没有变化,因为它表示的是该进程所占用物理内存的峰值,不会因为把内存置换到SWAP,而做改变.
而VmRSS则由461208KB变成了386704KB,说明它占用的物理内存因为置换所以减少.
VmData:表示进程数据段的大小.BSS段存储未初始化或初始化为0的全局变量、静态变量,具体体现为一个占位符,并不给该段的数据分配空间,只是记录数据所需空间的大小。数据段存储经过初始化的全局和静态变量。
VmStk:表示进程堆栈段的大小.
VmExe:表示进程代码的大小.
VmLib:表示进程所使用LIB库的大小.
6.Jansson
API
(1)int json_object_set(json_t *object, const char *key, json_t *value)
在object中设置键值对,如果object中键值key本来就存在覆盖原来的值(json型),如果键值key不存在就插入新的键值对.键值必须是有效的以null结尾的UTF-8编码的Unicode字符串。 成功返回0,失败返回-1
(2)int json_object_set_new(json_t *object, const char *key, json_t *value)
与 json_object_set类似,但是调用后会对value的引用计数减一(适用于:value为新建的json并且被调用后不再使用),可能会把value清空。
(3)json_value_get()传入NULL值会崩溃
原因是strcpy传入null会崩溃。
Libevent学习
1.API及调用顺序为:
event_base()初始化event_base
- event_set()初始化event(或者用evtimer_set设置定时事件)
- event_base_set()将event绑定到指定的event_base上
- event_add()将event添加到事件链表上,注册事件
- event_base_dispatch()循环、检测、分发事件
Event_new()好像是设置多个base的触发。
在Libevent2.0之前的版本中,没有event_assign或者event_new函数,而只有event_set函数,该函数返回的event与“当前”base相关联。如果有多个event_base,则还需要调用event_base_set函数指明event与哪个base相关联。
理解dtu的写出IP
2.evbuffer
`evbuffer`用于处理缓冲网络IO的“缓冲”部分。它不提供调度IO或者当IO就绪时触发IO的功能:这是bufferevent的工作。
void evbuffer_lock(struct evbuffer *buf);
void evbuffer_unlock(struct evbuffer *buf);
默认情况下,在多个线程中同时访问evbuffer是不安全的。如果需要这样的访问,可以在evbuffer 上调用evbuffer_enable_locking()。如果lock参数为NULL,libevent会使用evthread_set_lock_creation_callback提供的锁创建函数创建一个锁。否则,libevent将lock参数用作锁。
int evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen);
- 1
这个函数向buf 的末尾添加 datalen 字节的数据data 。 函数在成功时返回0, 失败时返回-1.
int evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...)
int evbuffer_add_vprintf(struct evbuffer *buf, const char *fmt, va_list ap);
这些函数添加格式化的数据到buf末尾。格式参数fmt和其他参数的处理分别与C库函数printf和vprintf相同。函数返回添加的字节数。
IP
1.IPV4
(1)表示方法
掩码标识网络号和主机号
(2)注(理解):套接字和协议报文对应关系
个人理解:套接字封装的socket有格式:
流格式套接字(SOCK_STREAM):使用TCP协议,只包含端口号,还需使用IP协议包含IP地址。还有其他格式的套接字。
2.ipv6
(1)表示方法
IPv6大致由前缀,子网ID,接口ID组成
前缀:相当于v4地址中的网络ID
接口ID:相当于v4地址中的主机ID
IPv6的长分布式结构图
IPv6有3种表示方法。
一、冒分十六进制表示法
格式为X:X:X:X:X:X:X:X,其中每个X表示地址中的16b,以十六进制表示,例如:
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
这种表示法中,每个X的前导0是可以省略的,例如:
2001:0DB8:0000:0023:0008:0800:200C:417A→ 2001:DB8:0:23:8:800:200C:417A
2001:C3:0:2C6A::/64表示一个子网
而2001:C3:0:2C6A:C9B4:FF12:48BC:1A22/64表示该子网下的一个节点地址。
可以看到,一个IPv6的地址有子网前缀+接口ID构成,子网前缀由地址分配和管理机构定义和分配,而接口ID可以由各操作系统实现生成
二、0位压缩表示法
在某些情况下,一个IPv6地址中间可能包含很长的一段0,可以把连续的一段0压缩为“::”。但为保证地址解析的唯一性,地址中”::”只能出现一次,例如:
FF01:0:0:0:0:0:0:1101 → FF01::1101
0:0:0:0:0:0:0:1 → ::1
0:0:0:0:0:0:0:0 → ::
三、内嵌IPv4地址表示法
过渡地址:内嵌IPv4地址的IPv6地址
为了实现IPv4-IPv6互通,IPv4地址会嵌入IPv6地址中,此时地址常表示为:X:X:X:X:X:X:d.d.d.d,前96b采用冒分十六进制表示,而最后32b地址则使用IPv4的点分十进制表示,例如::192.168.0.1与::FFFF:192.168.0.1就是两个典型的例子,注意在前96b中,压缩0位的方法依旧适用.主要用于某些场景下IPv6节点与IPv4节点通信,Linux内核对这类地址很好地支持
IPv6全球单播地址结构
前缀2000::/3,相当于IPv4的公网地址(IPv6的诞生根本上就是为了解决IPv4公网地址耗尽的问题)。这种地址在全球的路由器间可以路由。
链路本地地址结构
前缀FE80::/10,顾名思义,此类地址用于同一链路上的节点间的通信,主要用于自动配置地址和邻居节点发现过程。Windows和Linux支持或开启IPv6后,默认会给网卡接口自动配置一个链路本地地址。也就是说,一个接口一定有一个链路本地地址。
注意:很容易会把链路本地地址和IPv4的私网/内网地址对应起来,其实链路本地地址对应于IPv4的APIPA地址,也就是169.254开头的地址(典型场景就是windows开启自动获取地址而获取失败后自动分配一个169.254的地址)。而IPv4私网对应于IPv6的什么地址,后面会介绍。
唯一本地地址结构
前缀FC00::/7,相当于IPv4的私网地址(10.0.0.0、172.16.0.0、192.168.0.0),在RFC4193中新定义的一种解决私网需求的单播地址类型,用来代替废弃使用的站点本地地址。
IPv6不是为了解决IPv4地址耗尽的问题吗,既然IPv6的地址空间那么大,可以为每一个网络节点分配公网IPv6的节点,那为什么IPv6还需要支持私网?
因此,在安全性和私密性要求下,IPv6中同样需要支持私网,并且也需要支持NAT。在Linux内核3.7版本开始加入对IPv6 NAT的支持,实现的方式和IPv4下的差别不大(Linux内核代码中变量和函数的命名几乎就是ctrl+c和ctrl+v过来的-_-||)。
(2)抓包分析报文头部
- 当IPv6数据报文承载的是上层协议ICMPv6、TCP、UDP等的时候,Next Header的值分别为58、6、17,这个时候和IPv4报文头部中的Protocol字段很类似;
- 当不是以上3种协议类型的时候,IPv6报文头部紧接的是扩展头部。扩展头部是IPv6引入的一个新的概念,每个IPv6的数据报文可以承载0个或多个扩展头部,扩展头部通过链表的形式组织起来。当IPv6数据报文承载着扩展头部的时候,Next Header的数值为扩展头部的类型值。
引入扩展头部这个概念,这里也是IPv6对IPv4改进的一个方面,用扩展头部取代了IPv4的可选项信息,精简了IPv6的头部,增强了IPv6的扩展性。有同学会不会有疑问,IPv6的分片数据报文怎么处理?其实就是使用了IPv6扩展头部。我们来抓一个UDP分片报文来看看。
如图IPv6报文头部Next Header字段值为44表示存在扩展头部,扩展头部是IPv6分片数据信息。
IPv6技术详解:基本概念、应用现状、技术实践(上篇)-网络编程/专项技术区 - 即时通讯开发者社区! (52im.net)
(3)过渡技术
IPv4升级到IPv6肯定不会是一蹴而就的,是需要经历一个十分漫长的过渡阶段(用我厂通用的术语说,就是IPv4升级IPv6这个灰度的时间非常长),要数十年的时间都不为过。现阶段,就出现了IPv4慢慢过渡到IPv6的技术(或者叫过渡时期的技术)。过渡技术要解决最重要的问题就是,如何利用现在大规模的IPv4网络进行IPv6的通信。
要解决上面的问题,这里主要介绍3种过渡技术:
- 1)双栈技术;
- 2)隧道技术;
- 3)转换技术(有一些文献叫做翻译技术)
什么是双栈技术?
这种技术其实很好理解,就是通信节点同时支持IPv4和IPv6双栈。例如在同一个交换机下面有2个Linux的节点,2个节点都是IPv4/IPv6双栈,节点间原来使用IPv4上的UDP协议通信传输,现在需要升级为IPv6上的UDP传输。由于2个节点都支持IPv6,那只要修改应用程序为IPv6的socket通信基本达到目的了。
上面的例子在局域网通信的改造是很容易的。但是在广域网,问题就变得十分复杂了。因为主要问题是在广域网上的2个节点间往往经过多个路由器,按照双栈技术的部署要求,之间的所有节点都要支持IPv4/IPv6双栈,并且都要配置了IPv4的公网IP才能正常工作,这里就无法解决IPv4公网地址匮乏的问题。因此,双栈技术一般不会直接部署到网络中,而是配合其他过渡技术一起使用,例如在隧道技术中,在隧道的边界路由器就是双栈的,其他参与通信的节点不要求是双栈的。
什么是隧道技术?
当前的网络是以IPv4为主,因此尽可能地充分利用IPv4网络进行IPv6通信是十分好的手段之一。隧道技术就是这样子的一种过渡技术。
隧道将IPv6的数据报文封装在IPv4的报文头部后面(IPv6的数据报文是IPv4的载荷部分),IPv6通信节点之间传输的IPv6数据包就可以穿越IPv4网络进行传输。隧道技术的一个很重要的优点是透明性,通过隧道进行通信的两个IPv6节点(或者节点上的应用程序)几乎感觉不到隧道的存在。
在介绍具体的隧道技术前,特别要说明一下,Linux内核原生支持一种叫做sit(Simple Internet Transition)隧道。这个隧道专门用于IPv6-in-IPv4的数据封装解封和传输,应用十分之广泛,现在很多主流的IPv6隧道技术都能基于sit隧道实现。关于sit隧道的技术实现,可以查阅Linux内核源码 net/ipv6/sit.c 。
隧道技术之6to4隧道
用于两个拥有v4公网地址的IPv6 only子网的互相访问
6to4是当前使用得比较广泛的一种自动配置隧道技术,这种技术采用特殊的IPv6地址,称为6to4地址,这种地址是以2002开头,接着后面的32位就是内嵌的隧道对端的IPv4地址。当边界路由器收到这类目的地址,取出IPv4地址建立隧道。
6to4隧道一般用在路由器-路由器、主机-路由器、路由器-主机场景,典型的应用场景是两个IPv6的站点内主机通过6to4隧道进行相互访问。
6to4隧道的一个限制是内嵌的IPv4地址必须是公网地址。
6to4隧道就是自动隧道的一种,所谓自动隧道,就是用户仅需要配置设备隧道的起点,隧道的终点由设备自动生成。
6to4地址的报文格式:
FP:可聚合全球单播地址的格式前缀(Format Prefix),其值为001。
TLA:顶级聚合标识符(Top Level Aggregator),其值为0x0002。
SLA:站点级聚合标识符(Site Level Aggregator)。
关于IPv6中6to4隧道的专用地址说明:
1.2002::/16是6to4的专用地址段,也就是报文中的FP和TLA字段,
2.后面的32位是IPv4的地址,
前48bit不能改变,后面的看你心情设置。
数据包的流转过程:
当PC1给PC2发送数据包时,其源ip为自身的IPv6地址:2002:0101:0101:ffff::1,目的ip为PC2的IPv6地址:2002:0303:0303:dddd::1。
当数据包到达R1后,路由器(双栈)发现是以目的ip是以2002开头,查找路由表(在配置中,我们配置了一条2002::/16的静态路由),将该报文从tunnel sit接口上处理。
隧道接口的协议是6to4协议,所以会提取出IPv6地址中的ipv4地址(目的IPV6的地址中含有IPV4的IP),然后查看ipv4的路由表,封装上ipv4的报头,将该报文发送出去。
R3收到报文后,进行拆封,查看IPv6地址(也可以分发IPv4),并将其进行转发,其中SLA ID用于区分子网。
隧道技术之ISATAP隧道
ISATAP全称是站点内自动隧道寻址协议(Intra-Site Automatic Tunnel Addressing Protocol),用来为IPv4网络中的IPv6双栈节点可以跨越IPv4网络访问外部的IPv6节点。
ISATAP隧道一般用于主机-主机、主机-路由器的场景。
个人理解:通过IPV4与对端建立连接后,返回一个IPV6前缀,然后直接与对端直接进行IPV6通信。
隧道技术之Teredo隧道
前面的隧道技术,主要是在IPv4的数据报文承载着IPv6的数据报文,这是一种特殊的数据包格式(IPV6-in-IPv4),不同于我们熟悉的TCP、UDP等传输层协议。而我们平常接触到的网络都存在于NAT架构中(例如我们的办公网络和家庭网络),在这种网络架构中,路由器仅对于TCP、UDP等传输层协议做NAT处理,而无法正确处理IPv6-in-IPv4这种报文,例如使用ISATAP隧道,IPv6双栈节点与ISATAP路由器之前如果存在NAT,ISATAP建立隧道失败;6to4隧道也会遇到同样的问题。
Teredo隧道是有微软公司主导的一项隧道技术,主要用于在NAT网络架构下建立穿越NAT的隧道。
Teredo隧道的核心思路,是将IPv6的数据封装成IPv4的UDP数据包,利用NAT对IPv4的UDP支持进行穿越NAT的传输,当UDP包到达隧道的另外一端后,再把IPv4的包头、UDP包头剥离,还原IPv6的数据包,再进行下一步的IPv6数据通信转发。Teredo节点会分配一个以2001::/32的前缀,而且地址中还包含Teredo的服务器、标志位和客户端外部映射模糊地址和端口号等信息。
Teredo的实现还会遇到NAT的类型不同而被限制的问题。NAT的类型有锥形NAT、受限制的NAT、对称NAT几种,Teredo只能在锥形NAT和受限制的NAT的环境下正常工作,而且在这两种NAT需要处理的逻辑又是不一样的。因此Teredo整体的实现会比较复杂。
转换技术之NAT64/DNS64
(4)子网划分和VLAN划分
VLAN划分是基于数据链路层进行划分,子网划分是基于网络层进行划分,从原理上说这两种划分之间没有什么“联系”可言,区别大大的。如果一定要说两种划分之间有什么联系,联系就是实际运用时候需要对两种划分进行综合规划才能成功组网,规划了VLAN就必须要为每个VLAN规划相应的IP子网。
划分IP子网的目的是为了便于IP协议选择数据包输送的路径,对目的地进行近邻和远程两级规划,减轻网关负担提高路由效率。划分子网的目的不是为了阻断子网之间进行通讯,虽然结果上而言子网之间无法直接进行单播通讯。
1.众所周知,IPV6 地址为 128 bit,而且要求所有单播地址(全球公网地址)中子网必须是64bit,也就是如下:
子网(64bit) 接口ID(64bit)
并不是说子网不能占用后 64bit 的接口ID位,只是常规操作中,默认是这样,占用主机位做子网会造成某些功能不可用。
2.理解
子网前缀如果固定设置了,那么子网就固定了??
(5)地址获取
(理解:前缀委派就是RA路由通告前缀,需要客户端发送RS消息(怎么发送?),radvd可以作为服务器通告RA消息。现在设备本地子网只有静态和自动,那么自动就是DHCPv6下发一个完整的(路由通告还没做);本地设备获取IPv6:静态和自动(自动由odhcp6c获取))可以通过获取的前缀长度判断是RA还是DHCP的。
确定自动获取的是DHCP还是RA??Dns也是分开的,都获取,
前缀共享功能,需要在wan或者cellular上运行 npd6(odhcpd也能做,目前向下分配是由这个固定分配64长度的前缀),和radvd两个区别和作用。DHCP-PD又有什么关系?
ipv6地址获取有两种方式:
- 路由器通告的前缀 + 自己编的后缀(无状态,stateless)
对于无状态自动配置的ipv6测试需要搭建radvd服务器,这样在路由器/设备发送RS请求(icmpv6 type133)的时候,radvd服务器就可以返回RA消息(icmpv6 type134),告诉设备全局地址的前缀,设备自己再结合接口ID算出一个可聚集全局单播地址。
2、DHCP给一个/128的地址(有状态,stateful)
还有一个静态,
一般,运营商下发/56或者/60的前缀,路由器再自行从里面挑选一个/64的子网用来给lan中的设备分发ip地址。如果,你的lan中要接入另外一个路由器,那么,就需要:1. 上级路由开pd服务器,给下级分发/61, /62, /63的前缀 2. 或者,下级路由设置成交换机模式(有的叫穿透,passthrough)
(6)向下分配
PD,DHCPv6, ICMPv6 (radvd,先不做)
前缀共享odhcpd也能做,目前向下分配是由这个固定分配64长度的前缀。
dhcp-pd(dhcp prefix delegation)是地址委派。
委派一般指IPv6-PD(IPv6 Prefix delegation)用于,可以从上端设备获取一个长度小于64的前缀,设备一般处于整个网络的中间节点,参考RFC 3769。Auto时如果wan或者cellular没有从ISP拿到多个IPv6前缀,lan IPv6前缀和wan IPv6前缀一样, lan向下通告(RA),下端设备用同一个IPv6前缀,这就是IR924的前缀共享功能,需要在wan或者cellular上运行 npd6(Neighbor Proxy Daemon)下端设备才能正常访问外网 。如果wan或者cellular拿到多个IPv6前缀,lan可以从拿到的前缀池里取一个前缀,并向下通告(RA),LAN和下端设备用同一个IPv6前缀,WAN和LAN不在同一个网络(前缀不一样)。未来如果下端设备也有IPv6-PD的需求,我们再添加。
(6)socket编程
SOCKET 缓冲区是先缓冲到系统上,可以设置缓冲区大小,滑动窗口的原理。
1.一、sockaddr
sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下:
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
二、sockaddr_in
sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,
二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
结果强制转换
由此我们发现结构 sockaddr 和 sockaddr_in 字节数完全相同,都是16个字节,所以可以直接强转,但是结构 sockaddr_in6 有28个字节,为什么在使用的时候也是直接将地址强制转化成(sockaddr*)类型呢?
强转的可能性
其实sockaddr 和 sockaddr_in 之间的转化很容易理解,因为他们开头一样,内存大小也一样,但是sockaddr和sockaddr_in6之间的转换就有点让人搞不懂了,其实你有可能被结构所占的内存迷惑了,这几个结构在作为参数时基本上都是以指针的形式传入的,我们拿函数bind()为例,这个函数一共接收三个参数,第一个为监听的文件描述符,第二个参数是sockaddr*类型,第三个参数是传入指针原结构的内存大小,所以有了后两个信息,无所谓原结构怎么变化,因为他们的头都是一样的,也就是uint16 sa_family,
个人理解:socketaddr_in使用更方便,但socket编程使用的socketaddr。所以两者进行转换。
sockaddr的存储sockaddr_storage ??
这里千万不要犯傻用sockaddr存储sockaddr_in6数据,IOS上sockaddr的大小是16,和sockaddrin一致的,但是sockaddrin6大小是28(不要问我为什么会知道,都是泪)。通用的sockaddr的存储的结构体是sockaddr_storage,它是能存储任何sockaddr的结构。 你可能会问,如果socket用AF_INET6的时候,用sockaddr_in6结构体不就好了。不是说不可以,就是代码会变成IPv6专用的了,如果用到其他地方可能会出错。但是如果用AF_INET呢,虽然强转成sockaddrin没有任何问题,但是程序逻辑上蛋疼,如果大家要写v4/v6通用的逻辑的话,最好还是用sockaddr_storage存储,然后通过ss_family进行判断,最后做不同分支的处理。
通常sockaddr是指针传入,而且头部都是地址族,所以可以转换,对于网络地址的ipv4和6,存储用相应的sockaddr_in4或者6
看Ipv6编程
3.IP命令
(1)