阳光男孩

Never give up!

Entries Tagged ‘Linux’

Linux的Netfilter框架深度思考-对比Cisco的ACL

0顶一下0.1.本文不涉及具体实现,也不涉及源代码,不剖析代码 0.2.本文不争辩Linux或者Cisco IOS不同版本之间的实现细节 0.3.本文不正确处请指出 Cisco无疑是网络领域的领跑者,而Linux则是最具活力的操作系统内核,Linux几乎可以实现网络方面的所有特性,然而肯定还有一定的优化空间,本文首先向Cisco看齐,然后从不同的角度分析Netfilter的对应特性,最终提出一个ip_conntrack的优化方案。 0....[阅读全文]

0
顶一下

0.1.本文不涉及具体实现,也不涉及源代码,不剖析代码
0.2.本文不争辩Linux或者Cisco IOS不同版本之间的实现细节
0.3.本文不正确处请指出
Cisco无疑是网络领域的领跑者,而Linux则是最具活力的操作系统内核,Linux几乎可以实现网络方面的所有特性,然而肯定还有一定的优化空间,本文首先向Cisco看齐,然后从不同的角度分析Netfilter的对应特性,最终提出一个ip_conntrack的优化方案。

0.4.昨天女儿出生,她不哭也不闹,因此才能整理出这篇文档,这几天累坏了,但还是撑着整理了这篇文档

1.设计的异同
Netfilter是一个设计良好的框架,之所以说它是一个框架是因为它提供了最基本的底层支撑,而对于实现的关注度却没有那么高,这种底层支撑实际上就是其5个HOOK点:
PREROUTING:数据包进入网络层马上路由前
FORWARD:数据包路由之后确定要转发之后
INPUT:数据包路由之后确定要本地接收之后
OUTPUT:本地数据包发送(详情见附录4)
POSTROUTING:数据包马上发出去之前
1).HOOK点的设计:
Netfilter的hook点其实就是固定的“检查点”,这些检查点是内嵌于网络协议栈的,它将检查点无条件的安插在协议栈中,这些检查点的检查是无条件执行的 ;对比Cisco,我们知道其ACL也是经过精心设计的,但是其思想却和Netfilter截然相反,ACL并不是内嵌在协议栈的,而是一种“外部的列表”,策略包含在这些列表中,这些列表必须绑定在具体的接口上方能生效,除了绑定在接口上之外,检查的数据包的方向也要在绑定时指定,这说明ACL只是一个外接的策略,可以动态分派到任何需要数据包准入检查的地方。

2.数据流的异同-仅考虑转发情况
1).对于Cisco,数据包的通过路径如下:

2).对于Linux的Netfilter,数据包的通过路径如下:

3.效率和灵活性
3.1.过滤的位置
从数据流的图示中可以看出Netfilter的数据包过滤发生在网络层,这实际上是一个很晚的时期,从安全性上考虑,很多攻击-特别是针对路由器/服务器本身的Dos攻击-此时已经形成了,一个有效的预防方式就是在更早的时候丢弃数据包,这也正是Cisco的策略:“在尽可能早的时候丢弃数据包”。而Cisco也正是这么做的,这个从上面的图示中可以看出。Cisco的过滤发生在路由之前。

3.2.过滤表的条目
由于Netfilter是内嵌在协议栈中的全局的过滤框架,加之其位置较高,很难对“哪些包应该匹配哪些策略”进行区分,而Cisco的ACL配置在网卡接口,并且指定了匹配数据包的方向,因此通过区分网卡接口和方向,最终一个数据包只需要经过“一部分而不是全部”的策略的匹配。比如从Ether0进入的数据包只会匹配配置在Ether0上入站方向的ACL。

3.3.NAT的位置
Netfilter的NAT发生在filter之前和之后,而Cisco的nat也发生在filter之中,这对二者filter策略的配置有很大的影响,对于使用Netfilter的系统,需要配置DNAT之后或者SNAT之前的地址,而对于Cisco,则需要配置DNAT之前或者SNAT之后的地址。

3.4.配置灵活性
3.4.1.Cisco的acl配置很灵活,甚至“配置到接口”,“指明方向”这一类信息都是外部的,十分符合UNIX哲学的KISS原则,但是在具体的配置上对工程师的要求更高一些,他们不仅仅要考虑匹配项等信息,而且还要考虑接口的规划。
3.4.2.Netfilter的设计更加集成化,它将接口和方向都统一地集成在了“匹配项”中,工程师只需要知道ip信息或者传输层信息就可以配置了,如果他们不关心接口,甚至不需要指明接口信息,实际上在iptables中,不使用-i和-o选项的有很多。

4.Netfilter优化
4.1.防火墙策略查找优化
4.1.1.综述
传统意义上,Netfilter将所有的规则按照配置的顺序线性排列在一起,每一个数据包都要经过所有的这些规则,这大大降低了效率,随着规则的增加,效率会近似线性的下降,如果能让一个数据包仅仅通过一部分的规则的匹配就比较好了。这就是说,我们要对规则进行分类,然后先将过往的数据包用高效的算法匹配到一个特定的分类,然后该数据包只需要再继续匹配该分类中规则就可以了。
分类实际山很简单,它基于一个再简单不过的解析几何事实:在一条线段上,一个点将整个线段分为3部分:

因此,任何一个匹配项都可以归结为一个所谓的“键值”,在该键值空间中,一定有某种顺序可供排序,那么一个键值,就能将这个键值空间分为三个部分:大于,等于,小于。一维空间如此,N维空间亦如此,只是更精确,这里N是我们挑选出来的匹配域。为了更好的理解下面的论述,先给出两幅图。传统意义的防火墙规则匹配操作如下图所示,它是平坦的:

而优化后的防火墙规则匹配操作如下图所示,它是分维度的:

最终,只有虚线所围的区域有规则要匹配,只有数据包“掉进了”这些区域,才需要匹配规则,否则全部按照“策略”行事。当然,一个数据包不可能掉进两个区域。这里只考虑了源IP地址和目的IP地址这种二维的情形,如果加上第四层协议,端口等信息维度,匹配就更加精确了,并且,只要使用的“类”匹配算法足够精巧,操作是不会随着规则的增加而增加的,而这一部分内容正是我们马上就要讨论的内容

4.1.2.Cisco的优化策略
很多用过Cisco的人都知道,Cisco有一个叫做Turbo ACL的概念,这个Turbo ACL的要旨就是“不再用规则匹配数据包,而成为了使用数据包的信息查找需要匹配它的规则”。这就意味着在ACL插入系统的时候就要对其进行排序,然后数据包进入的时候,通过数据包的信息去查找排过续的规则集。
想了解Cisco的技术细节,直接浏览其官方网站的Support是很有必要的,这里有最直接的讲述,Cisco的技术Support有一个很好的地方,那就是它有情景分析。我下面就用那上面的例子来进行分析,基本上基于一篇文档:《TURBO ACL》。
Turbo ACL定义了一系列的匹配域,如下图所示:

其中绿色的表示三层信息,红色的表示四层信息,粉色的表示第三层+第四层的信息。针对于每一个匹配域,都存在一个表,我们称为“值表”:

其中索引是为了查找和管理方便,而值则被填入规则中对应该表的匹配域的值,ACL位图指示该表的该记录匹配哪些ACL。因此,对于所有的匹配域,由于一共有8个匹配域,那么就有8个这样的表。为了更加容易理解,给出一个例子,首先看4条acl规则:
#access-list 101 deny tcp 192.168.1.0 0.0.0.255  192.168.2.0 0.0.0.255 eq telnet
#access-list 101 permit tcp 192.168.1.0 0.0.0.255 192.168.2.0 0.0.0.255 eq http
#access-list 101 deny tcp 192.168.1.0 0.0.0.255  192.168.3.0 0.0.0.255 eq http
#access-list 101 deny icmp 192.168.1.0 0.0.0.255 200.200.200.0 0.0.0.255
这些规则填入匹配域表格后,匹配域表格如下:

然后仅给出一个“源地址1”的值表:

到此为止,我们已经给出了所有的静态的数据结构,接下来就是具体的动态操作了,归为一种算法。Cisco的规则匹配算法是分层次的,并且是可并行运算的,因此它的效率极其高效,整个算法分为两大部分:
1).数据包基于所有匹配域的位图查找
这个步骤是可以并行的,比如可以同时在两个处理器上查找“源地址1”的值表和“源地址2”的值表,从而最大化CPU利用率,以最快的速度得到两个位图,算法对于采用何种查找算法没有规定,取决于添加ACL时如何将匹配域的值插入对应值表。另外,哪种查找快用哪种,这是不争的事实,我们一般很少有动态插入的,一般都是静态插入的,因此对数据插入的性能要求并不高,关键要素是查找。这个查找算法的查找效率非常重要,好的算法如果是O(1)的,那就意味着匹配规则的过程消耗的时间不会随着规则的增加而增加,事实上即使是O(n)的查找算法, 也将N次的匹配操作转化为了按照一个比例小得多的a*N次的查找操作,往往a是一个很小的且小于1的数字…
2).多个位图多次的AND操作
取多个结果的交集,最终得到一条或者几条ACE。这种位图的算法是Cisco惯用的用空间换时间的策略,传说中的256叉树使用的也是这样的策略。
下面给出操作的流程图:

作为一个情景分析,我们考虑一个数据包到来,它的匹配域的值如下:
源地址1 : 192.168
源地址2 : 1.1
目的地址1 : 200.200
目的地址2 : 200.1
四层协议 : 0001 (ICMP)
针对此包的操作流程图如下,假设仅有上述举例的acl可用:

最终得到了0001,也就是仅有最后一条规则是匹配的。
这样我们就结束了Turbo ACL的讨论,接下来就要看看Linux的Netfilter有没有什么对等的优化策略

4.1.3.Netfilter的filter优化策略
Netfilter有一个项目,叫做nf-HiPAC ,它的代码极端复杂,文档极端稀缺,功能相比iptables更加有限,加之Linux面对巨量规则的需求不多,因此HiPAC的受用性不高,然而从理论的角度去分析一下它也是有好处的。虽然啃HiPAC的代码是一件很恐怖的事情,然而浏览一下它并不很难,最终我们发现,它的实现和Turbo ACL基本是一致的,也是基于数据包首先匹配匹配域从而先得到分类,它使用了几乎相同然而更多一些的匹配域,和Turbo ACL不同的是,它没有使用位图,因为Linux可能不允许以空间换时间,呵呵…
HiPAC没有使用位图,这是因为它根本不需要位图, 因为Cisco并行的同时得到了所有匹配域值表的位图,因此只要将它们AND,就能得到最终结果,可是HiPAC并不是并行操作的,而是串行的,HiPAC对于每一个匹配域也有一个值表,由于一系列的匹配域按照一定的顺序排列好,比如:源地址-目的地址-协议-源端口-目的端口,因此其值表也有这样的串接关系,见下面:

在找到目的地址的匹配之前,是不会匹配协议以及后面的匹配域的。具体的规则挂接在最后的匹配域值表中。HiPAC并没有保留原始的配置规则,然后通过位图找到它们,而是直接将规则挂接在了它“应该在”的位置。一个HiPAC的流程图如下:

4.1.4.Cisco和Netfilter的对比
它们使用的查找算法十分一致,然而具体的操作却大相径庭,我们看到Cisco完全是在并行的处理,而Netfilter则一串到底。如果形象的理解,我们可以将整个操作比作在一个多维空间查找一个点。有两种方式:
1).N个维度同时向前推进,最终找到它们路径的相交区域;
2).先在第一个维度匹配,然后再匹配第二个维度…
我们发现,Cisco使用了第一个方法,而Netfilter使用了第二个。我想Netfilter不使用并行方式的原因有二:第一,Netfilter一般应用于Linux,而Linux是一个通用的操作系统,对于协议栈的支持只是其一部分功能而已,如果为协议栈引入并行机制,势必会造成一种不均衡的态势。第二,Linux一般情况下不会有成千上万的防火墙条目,而上述的优化算法在规则条目越多的情形下效果越明显。另外,对于Netfilter的nf-HiPAC的查找机制,又使得我想起了Linux的页表查找和路由表的trie树查找算法。

4.2.ip_conntrack优化
Netfilter的ip_conntrack模块实现了连接跟踪的功能,然而这个实现我总觉得有个美中不足的地方。那就是它对于IP分片的处理,Netfilter的ip_conntrack对于分段的处理就是合并分段,理由就是IP层是无连接的,而IP分段则无法得到第四层信息,因此为了得到第四层信息,必须等待所有分段到达,然后才能继续处理。这是个理由,并且说的很充分,然而我个人认为这肯定还是可以再优化的,我们可以再做一个层次来解决这件事,正如我们“仅仅保留一个流的五元素就能识别一个数据包是否属于该流”一样,我们也能为一个ip数据报保留一个“源ip/目的ip/协议/三层id”四元素,这四个元素唯一确定一个ip数据报(理由见附录) ,我们仅仅需要用一个ip分片匹配这四元素就能确定它属于哪个ip初始分片,而这也就知道了它属于哪个流,当然仅仅针对分段数据报保留这四元素即可,但是由于ip是不保证顺序的,如果到来的一个ip分片不是第一个分片,那怎么办?这个很简单,那只能等,等待第一个分片到来,得到四元素信息,然后再处理。
这里给出一个流程图,原因是也只能给出这个图了,这篇文档是在医院写的,我家小小估计快要出生了…回头有时间再改代码吧,如果哪位大侠看了,觉得有点意思并且感兴趣的话,请一定尝试一下,然后给内核的Netfilter组提交一个patch,小弟在此大谢:

总之,nf-HIpac,采用串行(可以修正为并行)多维树查找算法,源于一种包分类算法,不再是规则匹配包,而成为了包寻找规则。维度的增加,约束相应增加,定位就更准确。查找所需的时间不管怎样要比依次匹配规则的时间更少,最终,最多只有一部分规则参与抉择。

4.3.基于优化后ip_conntrack的有状态防火墙
既然ip_conntrack被优化了,那它就不会为ip分片所累了(其实它原来就不会为其所累)。基于ip_conntrack实现一个有状态防火墙也不是一件难事,ip_conntrack中保留着该流第一个包到达时的数据包经过filter表时的匹配策略,具体来讲就是一个target,然后对于后续的包都直接按照这个target来执行。
然而这种防火墙究竟和HiPAC相比有何不同的,这种防火墙在PREROUTING时去匹配流,第一个数据包还在在filter中匹配规则,而HiPAC只需要在filter中匹配规则即可,对于大量连接而言,流匹配肯定会慢,然而如果有大量规则,HiPAC不会降速的,这正是它的优势所在,正和Cisco的Turbo ACL一样。

5.一个细节-防火墙对IP分片的处理
5.1.问题之所在
在RFC1858中指明了两类ip分片的攻击
1).TCP小包攻击
对于这类攻击,很容易理解,首先给出一个IP数据报分片的第一个片:

然后再看第二个片:

分片的offset字段指示了tcp载荷的偏移,这样,攻击者认为防火墙无法识别分段的第四层信息,从而成功的绕过了防火墙的检测,攻击要点在于,将一个完整的TCP协议头硬拆成两段,咋可好!。实际上很长一段时间,Cisco的ACL只要匹配到数据报分片的第三层信息并且规则是permit,那么是一律放过的。实际上,RFC1858中给出了解决方案,需要限制TCP载荷分片的最小值,这也是RFC的建议(然则Cisco并不一定遵守)。
2).TCP重叠攻击(依赖重组算法)
和1)相比,这是一种间接的攻击方式,请看第一个IP分片:

再看第二个分片:

我们看到第一个都是正常的,只是第二个不正常,如果目的地主机的IP分片的合并算法有问题,第二个分片的信息就会覆盖掉第一个分片的tcp协议头信息,由于过滤规则无法从IP分段中获取四层信息,因此数据轻松绕过防火墙,从而实施攻击。标准并没有规定IP分片合并的具体约束,这是导致这个攻击得意存在的根本原因。

5.2.Cisco的处理
Cisco处理IP分片完全采用一种统一的方式,将是否允许其通过这件事完全交给了配置工程师们,它的流程图如下所示,流程图显示单个数据包匹配ACE的情形:

Cisco的工程师必须显示配置哪些分片不能通过。然而Cisco的IOS的新版本还是限制了RFC1858中提到的攻击分片的通过

5.3.Netfilter的处理
Netfilter直接禁止了RFC1858中提到的攻击分片的通过,流程图就不画了,给出一段代码即可:
view plaincopy to clipboardprint?
static int
tcp_match(const struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
const void *matchinfo,
int offset,
int *hotdrop)
{
struct tcphdr tcph;
const struct ipt_tcp *tcpinfo = matchinfo;
if (offset) {

if (offset == 1) {
duprintf(“Dropping evil TCP offset=1 frag.\n”);
*hotdrop = 1;  //不允许这样的包通过
}
return 0;
}

}
static int
tcp_match(const struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
const void *matchinfo,
int offset,
int *hotdrop)
{
struct tcphdr tcph;
const struct ipt_tcp *tcpinfo = matchinfo;
if (offset) {

if (offset == 1) {
duprintf(“Dropping evil TCP offset=1 frag.\n”);
*hotdrop = 1;  //不允许这样的包通过
}
return 0;
}

}

另外,Netfilter对于非第一个的IP分片,对于高于网络层的一切匹配项一律命中匹配,比如来了一个ip分片,对于tcp/udp的端口信息,一律匹配,然后直接执行target,这和Cisco的策略是不一样的,这一点从其流程图中可以看出来。

5.4.对比
不管是Cisco还是Netfilter,它们都将匹配项分为了两类,一类是隐含的匹配项,这些项只包含三层信息,另一类是明确匹配项,这类匹配项包含更高层的信息 -对于Linux的Netfilter而言,这类隐含匹配项不需要注册,而明确匹配项需要注册,Cisco的方式未知,但是猜测不是这样的,应该都需要或者都不要注册。对于Netfilter的filter和Cisco的ACL,都是在隐含匹配项匹配的基础上才匹配明确匹配项的,可以参见Cisco处理ACL的流程图以及Linux内核的Netfilter代码:
view plaincopy to clipboardprint?
do {
//判断是否和隐含匹配项匹配,offset指示是否为ip分片
if (ip_packet_match(ip, indev, outdev, &e->ip, offset)) {
针对每一个明确匹配项进行匹配,只要只要有一个不匹配,则跳到not-match,否则执行target。由于几乎所有注册的match对于ip分片都直接返回“匹配”,所有IP分片只需匹配隐含匹配则就算是匹配了。
} else {
not-match:
下一条规则
}
} while (还有其它规则)
do {
//判断是否和隐含匹配项匹配,offset指示是否为ip分片
if (ip_packet_match(ip, indev, outdev, &e->ip, offset)) {
针对每一个明确匹配项进行匹配,只要只要有一个不匹配,则跳到not-match,否则执行target。由于几乎所有注册的match对于ip分片都直接返回“匹配”,所有IP分片只需匹配隐含匹配则就算是匹配了。
} else {
not-match:
下一条规则
}
} while (还有其它规则)

6.总结
站在一个比较高的层面上仔细观测Linux和Cisco IOS的网络设计,IOS的优势更多的在于它将几乎所有的精力都用到了网络方面,IOS的内核机制实际上要比Linux的简单得多,然而它依托于一个总体的良好设计,使得几乎任何事情都可以被配置出来,在IOS中,任何策略都是配置出来的,虽然它有一个默认配置文件,然而那也是配置出来的。
而Linux的做法就完全不同,Linux的网络策略实际上是Netfilter和硬编码的结合,在Linux内核中(网络方面的代码),我们可以看到很多注释,这些注释大多数是Alan Cox 添加的,很多都是说“为了遵循RFCXXXX…”。当然,这种硬编码也是可以配置的,比如使用sysctl工具,然而它不能使用一个统一的工具来配置,比如你不能使用ip命令打开ip_forward…
我知道,使用Netfilter可以实现几乎Cisco IOS的所有功能,并且也可以做和IOS类似的优化,这正是Netfilter框架的优越性所在,然而虽然从外部看起来是一样的,但是要明白其实质是有很大差别的,另外Linux没有必要追赶Cisco IOS,这是没有意义的,即使做得比Cisco好,我相信大部分人还是会买Cisco的,因为市场的竞争中技术因素只占很小的一部分份额,正如很多人都在大搞Linux的Windows的兼容,这有必要吗,在Windows中有个注册表,Linux中就一定要有类似的吗?一切安好,在纯技术领域的讨论如果放到整个产品层面就会认为是倔强和顽固。
Netfilter框架设计的很好,每一个细节都值得细细品味,使用它,理解它,修改它,优化它,完善它,使用它… 这是一个很不错的学习过程,你也可以试试。

附录
0.Netfilter到底属于谁?
0.1.Netfilter是一个框架 ,它是独立于Linux内核的,它有自己的网站:http://www.netfilter.org/
0.2.Netfilter拥有几乎无限的可扩展性, Liuux中使用的仅仅是它的一个很小的部分,大部分的内容作为可插拔的module处于待命状态
0.3.Netfilter的机制集成在Linux内核中, 然而它的策略扩展却处于一个独立的空间,我们说这种所谓的机制也仅仅是5个HOOK点。我们浏览netfilter.org就会知道,它里面融合了大量的策略,我们最熟悉的就是iptables了。上述提到的HiPAC也是Netfilter的扩展之一
0.4.足以看出,Netfilter有多强大,内核仅仅给出钩子点而已。 如果你嫌某些不好,你可以自己实现一个更好的
0.5.事实上,Netfilter中有很多的东西并没有集成在Linux内核。

1.一幅图:数据包的内核路径图
为了给出Linux内核中Netfilter的全景,给出一幅图,图中详细标示了其各个部分

2.ip_conntrack优化中使用四元素的理由:
ip层给出了4元素,明确跟踪了一个IP数据报,实际上TCP/IP的每一个层次的协议头都会提供一些该层PDU的跟踪信息,由于IP层是基于报文的,因此其跟踪信息完全标示一个IP数据报,一个分片的IP数据报的所有报文片段的这些跟踪信息相同。理解这一点十分简单和直接,正如TCP/UDP协议的端口号信息加上更低层次的跟踪信息就能标示一个流一样-一个流标示一个会话,有很多的数据报组成。在RFC791(非常非常重要的IP协议RFC)中,明确的指明了这一点,《tcp/ip详解》中也指示了这一点:
“标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1。”“RFC 791 [Postel 1981a]认为标识字段应该由让IP发送数据报的上层来选择。假设有两个连续的IP数据报,其中一个是由TCP生成的,而另一个是由UDP生成的,那么它们可能具有相同的标识字段。尽管这也可以照常工作(由重组算法来处理),但是在大多数从伯克利派生出来的系统中,每发送一个IP数据报,IP层都要把一个内核变量的值加1,不管交给IP的数据来自哪一层。内核变量的初始值根据系统引导时的时间来设置。”《TCP/IP详解(第一卷)》

3.conntrack-tools
首先声明:这不是Linux的错!也许,有时候,你的iptables规则清除了,然而数据包地址转换还在进行。这是ip_conntrack的chache引起的,然而这并不是问题,只要能使用工具解决的事情都不是问题,这个问题也能用工具解决,这个工具就是conntrack-tools,它能在任意时间删除任意的ip_conntrack的cache,具体怎么用,教你:1.下载;2.安装;3.man

4.Netfilter的HOOK点之OUTPUT位置设计
Netfilter中output这个hook点比较特殊,按照常理,output应该设计在路由前的,这也符合过滤尽量在早期发生的原则,然而我们发现Netfiler的output链却在路由之后,这里面到底有什么蹊跷呢?
4.1.output链在路由之后,侧重于“到底是容易被过滤还是容易没有路由”
4.2.过滤发生在路由之后,权衡点在于“可能没有路由还是可能被drop”。
4.3.skb的output函数是个回调函数,而这个回调函数是根据路由的结果以及路由策略设置的,因此最好将output链设置于路由之后, 这样就可以将ip的发送函数简单的写成:
view plaincopy to clipboardprint?
int __ip_local_out(struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
ip_send_check(iph);
return nf_hook(PF_INET, NF_INET_LOCAL_OUT, skb, NULL, skb->dst->dev,
dst_output);
}
static inline int dst_output(struct sk_buff *skb)
{
return skb->dst->output(skb);
}
int __ip_local_out(struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
ip_send_check(iph);
return nf_hook(PF_INET, NF_INET_LOCAL_OUT, skb, NULL, skb->dst->dev,
dst_output);
}
static inline int dst_output(struct sk_buff *skb)
{
return skb->dst->output(skb);
}
注意,以上的dst是根据路由查找的结果初始化的。将DNAT挂在OUTPUT链上是没有问题的,因为DNAT改变了目的地址后,会重新路由,然后就会重新初始化dst字段,新的output函数也将获得。Netfilter将output确定在了路由之后以及在output上实施dnat保证了必定在路由之后确定skb的dst字段,否则dsk字段不确定的话,nf_hook函数就不好写了。
总结起来就一点,Linux的IP层往下的发送例程是“路由查找结果”决定的,因此只有在路由查找之后才可确定发送函数,才可以挂载继续发送的钩子。

5.Cisco IOS/H3C VRP/GNU Linux
04年那年,接触了华三的设备,随后又使用了Cisco的,大概2年后,我看到Linux的shell界面时,我还以为这是Cisco呢…IOS和VRP的操作界面很类似,它们都属于核心网络设备这一块,侧重于核心路由和防火墙,配置可以很难,但是一定要灵活,迎合各种需求,无可非议,VRP借鉴了Cisco-虽然它的内核是BSD化的,Linux属于一个通用操作系统,核心网络不是它的应用场合。

6.预防IP欺骗的基于RFC2827经典配置
(1) 任何进入网络的数据包不能把网络内部的地址作为源地址。
(2) 任何进入网络的数据包必须把网络内部的地址作为目的地址。
(3) 任何离开网络的数据包必须把网络内部的地址作为源地址。
(4) 任何离开网络的数据包不能把网络内部的地址作为目的地址。
(5) 任何进入或离开网络的数据包不能把一个私有地址(Private Address)或在RFC1918中列出的属于保留空间(包括10.x.x.x/8、172.16.x.x/12 或192.168.x.x/16 和网络回送地址127.0.0.0/8.)的地址作为源或目的地址。
(6) 阻塞任意源路由包或任何设置了IP选项的包。

 

Comments (36)

在linux下利用crontab定时执行PHP脚本

0顶一下最近急需这种方法,记录一下,有空的时候尝试^^ 在 linux下,可以使用crontab + php的方法: 1、使用crontab –e编辑定时任务 内容为: xx:xx:xx 执行一个test.php文件 2、php文件必须在文件头一行,加上解释器路径(就象perl做的那样) #!/usr/local/bin/php PHP的执行需要Apache的支持,shell脚本的执行需要Linux的支持,而Linux支持定时运行某个程序的功能 ———————————————...[阅读全文]

0
顶一下

最近急需这种方法,记录一下,有空的时候尝试^^

在 linux下,可以使用crontab + php的方法:

1、使用crontab –e编辑定时任务

内容为:

xx:xx:xx 执行一个test.php文件

2、php文件必须在文件头一行,加上解释器路径(就象perl做的那样)

#!/usr/local/bin/php

PHP的执行需要Apache的支持,shell脚本的执行需要Linux的支持,而Linux支持定时运行某个程序的功能

—————————————————————

将PHP作为Shell脚本语言使用(转载)

–英文原着:Darrell Brogdon,发表于 http://www.phpbuilder.com/columns/darrell20000319.php3)

可能很多人都想过使用PHP编写一些定时发信之类的程序,但是却没有办法定时执行PHP;一次去PHPBuilder的时候,发现了这一篇文 章,于是想给大家翻译一下(同时做了一些修改),希望对大家有用。

———————————————————————————-

我们都知道,PHP是一种非常好的动态网页开发语言(速度飞快,开发周期短……)。但是只有很少数的人意识到PHP也可以很好的作为编写 Shell脚本的语言,当PHP作为编写Shell脚本的语言时,他并没有Perl或者Bash那么强大,但是他却有着很好的优势,特别是对于我这种熟悉 PHP但是不怎么熟悉Perl的人。

要使用PHP作为Shell脚本语言,你必须将PHP作为二进制的CGI编译,而不是Apache模式;编译成为二进制CGI模式运行的PHP 有一些安全性的问题,关于解决的方法可以参见PHP手册(http://www.php.net)。

一开始你可能会对于编写Shell脚本感到不适应,但是会慢慢好起来的:将PHP作为一般的动态网页编写语言和作为Shell脚本语言的唯一不 同就在于一个Shell脚本需要在第一行生命解释本脚本的程序路径:

#!/usr/local/bin/php -q

我们在PHP执行文件后面加入了参数“-1”,这样子PHP就不会输出HTTPHeader(如果仍需要作为Web的动态网页,那么你需要自己 使用header函数输出HTTPHeader)。当然,在Shell脚本的里面你还是需要使用PHP的开始和结束标记:

现在让我们看一个例子,以便于更好的了解用PHP作为Shell脚本语言的使用:

#!/usr/local/bin/php -q

print(“Hello, world!\n”);

?>

上面这个程序会简单的输出“Hello, world!”到显示器上。

一、传递Shell脚本运行参数给PHP:

作为一个Shell脚本,经常会在运行程序时候加入一些参数,PHP作为Shell脚本时有一个内嵌的数组“$argv”,使用“$argv” 数组可以很方便的读取Shell脚本运行时候的参数(“$argv[1]”对应的是第一个参数,“$argv[2]”对应的是第二个参数,依此类推)。比 如下面这个程序:

#!/usr/local/bin/php -q

$first_name = $argv[1];

$last_name = $argv[2];

printf(“Hello, %s %s! How are you today?\n”, $first_name, $last_name);

?>

上面的代码在运行的时候需要两个参数,分别是姓和名,比如这样子运行:

[dbrogdon@artemis dbrogdon]$ scriptname.ph Darrell Brogdon

Shell脚本在显示器上面会输出:

Hello, Darrell Brogdon! How are you today?

[dbrogdon@artemis dbrogdon]$

在PHP作为动态网页编写语言的时候也含有“$argv”这个数组,不过和这里有一些不同:当PHP作为Shell脚本语言的时候 “$argv[0]”对应的是脚本的文件名,而当用于动态网页编写的时候,“$argv[1]”对应的是QueryString的第一个参数。

二、编写一个具有交互式的Shell脚本:

如果一个Shell脚本仅仅是自己运行,失去了交互性,那么也没有什么意思了。当PHP用于Shell脚本的编写的时候,怎么读取用户输入的信 息呢?很不幸的是PHP自身没有读取用户输入信息的函数或者方法,但是我们可以效仿其他语言编写一个读取用户输入信息的函数“read”:

function read() {

$fp = fopen(‘/dev/stdin’, ‘r’);

$input = fgets($fp, 255);

fclose($fp);

return $input;

}

?>

需要注意的是上面这个函数只能用于Unix系统(其他系统需要作相应的改变)。上面的函数会打开一个文件指针,然后读取一个不超过255字节的 行(就是fgets的作用),然后会关闭文件指针,返回读取的信息。

现在我们可以使用函数“read”将我们前面编写的程序1修改一下,使他更加具有“交互性”了:

#!/usr/local/bin/php -q

function read() {

$fp = fopen(‘/dev/stdin’, ‘r’);

$input = fgets($fp, 255);

fclose($fp);

return $input;

}

print(“What is your first name? “);

$first_name = read();

print(“What is your last name? “);

$last_name = read();

print(“\nHello, $first_name $last_name! Nice to meet you!\n”);

?>

将上面的程序保存下来,运行一下,你可能会看到一件预料之外的事情:最后一行的输入变成了三行!这是因为“read”函数返回的信息还包括了用 户每一行的结尾换行符“\n”,保留到了姓和名中,要去掉结尾的换行符,需要把“read”函数修改一下:

function read() {

$fp = fopen(‘/dev/stdin’, ‘r’);

$input = fgets($fp, 255);

fclose($fp);

$input = chop($input); // 去除尾部空白

return $input;

}

?>

三、在其他语言编写的Shell脚本中包含PHP编写的Shell脚本:

有时候我们可能需要在其他语言编写的Shell脚本中包含PHP编写的Shell脚本。其实非常简单,下面是一个简单的例子:

#!/bin/bash

echo This is the Bash section of the code.

/usr/local/bin/php -q << EOF

print(“This is the PHP section of the code\n”);

?>

EOF

其实就是调用PHP来解析下面的代码,然后输出;那么,再试试下面的代码:

#!/bin/bash

echo This is the Bash section of the code.

/usr/local/bin/php -q << EOF

$myVar = ‘PHP’;

print(“This is the $myVar section of the code\n”);

?>

EOF

可以看出两次的代码唯一的不同就是第二次使用了一个变量“$myVar”,试试运行,PHP竟然给出出错的信息:“Parse error: parse error in – on line 2”!这是因为Bash中的变量也是“$myVar”,而Bash解析器先将变量给替换掉了,要想解决这个问题,你需要在每个PHP的变量前面加上“\” 转义符,那么刚才的代码修改如下:

#!/bin/bash

echo This is the Bash section of the code.

/usr/local/bin/php -q << EOF

\$myVar = ‘PHP’;

print(“This is the \$myVar section of the code\n”);

?>

Comments (348)

Linux的Shell编程 bash的内部命令

0顶一下bash命令解释套装程序包含了一些内部命令。内部命令在目录列表时是看不见的,它们由Shell本身提供。常用的内部命令有:echo, eval, exec, export, readonly, read, shift, wait和点(.)。下面简单介绍其命令格式和功能。 1.echo 命令格式:echo arg 功能:在屏幕上显示出由arg指定的字串。 2.eval 命令格式:eval args 功能:当Shell程序执行到eval语句时,Shell读入参数args,并将它们...[阅读全文]

0
顶一下

bash命令解释套装程序包含了一些内部命令。内部命令在目录列表时是看不见的,它们由Shell本身提供。常用的内部命令有:echo, eval, exec, export, readonly, read, shift, wait和点(.)。下面简单介绍其命令格式和功能。

1.echo

命令格式:echo arg

功能:在屏幕上显示出由arg指定的字串。

2.eval

命令格式:eval args

功能:当Shell程序执行到eval语句时,Shell读入参数args,并将它们组合成一个新的命令,然后执行。

3.exec

命令格式:exec命令参数

功能:当Shell执行到exec语句时,不会去创建新的子进程,而是转去执行指定的命令,当指定的命令执行完时,该进程(也就是最初的 Shell)就终止了,所以Shell程序中exec后面的语句将不再被执行。

4.export

命令格式:export变量名 或:export变量名=变量值

功能:Shell可以用export把它的变量向下带入子Shell,从而让子进程继承父进程中的环境变量。但子Shell不能用export 把它的变量向上带入父Shell。

注意:不带任何变量名的export语句将显示出当前所有的export变量。

5.readonly

命令格式:readonly变量名

功能:将一个用户定义的Shell变量标识为不可变。不带任何参数的readonly命令将显示出所有只读的Shell变量。

6.read

命令格式:read变量名表

功能:从标准输入设备读入一行,分解成若干字,赋值给Shell程序内部定义的变量。

7.shift语句

功能:shift语句按如下方式重新命名所有的位置参数变量,即$2成为$1,$3成为$2…在程序中每使用一次shift语句,都使所有的位 置参数依次向左移动一个位置,并使位置参数$#减1,直到减到0为止。

8.wait

功能:使Shell等待在后台启动的所有子进程结束。wait的返回值总是真。

9.exit

功能:退出Shell程序。在exit之后可有选择地指定一个数位作为返回状态。

10.“.”(点)

命令格式:. Shell程序文件名

功能:使Shell读入指定的Shell程序文件并依次执行文件中的所有语句。

Comments (337)

Linux下的tty,console与串口

1顶一下终端设备 终端(或TTY)设备是一种特殊的字符设备。终端设备是可以在会话中扮演控制终端角色的任何设备,包括:虚拟控制台、串行接口(已废弃)、伪终端(PTY)。 所有的终端设备共享一个通用的功能集合:line discipline,它既包含通用的终端 line discipline 也包含SLIP和PPP模式。所有的终端设备的命名都很相似。这部分内容将解释命名规则和各种类型的TTY(终端)的使用。需要注意的是这些命名...[阅读全文]

1
顶一下

终端设备

终端(或TTY)设备是一种特殊的字符设备。终端设备是可以在会话中扮演控制终端角色的任何设备,包括:虚拟控制台、串行接口(已废弃)、伪终端(PTY)。

所有的终端设备共享一个通用的功能集合:line discipline,它既包含通用的终端 line discipline 也包含SLIP和PPP模式。所有的终端设备的命名都很相似。这部分内容将解释命名规则和各种类型的TTY(终端)的使用。需要注意的是这些命名习惯包含了几个历史遗留包袱。其中的一些是Linux所特有的,另一些则是继承自其他系统,还有一些反映了Linux在成长过程中抛弃了原来借用自其它系统的一些习惯。井号(#)在设备名里表示一个无前导零的十进制数。
虚拟控制台(Virtual console)和控制台设备(console device)

虚拟控制台是在系统视频监视器上全屏显示的终端。虚拟控制台被命名为编号从 /dev/tty1 开始的 /dev/tty# 。/dev/tty0 是当前虚拟控制台。/dev/tty0 用于在不能使用帧缓冲设备(/dev/fb*)的机器上存取系统视频卡,注意,不要将 /dev/console 用于此目的。/dev/console 由内核管理,系统消息将被发送到这里。单用户模式下必须允许 login 使用 /dev/console 。
串行接口

这里所说的”串行接口”是指 RS-232 串行接口和任何模拟这种接口的设备,不管是在硬件(例如调制解调器)还是在软件(例如ISDN驱动)中模拟。在linux中的每一个串行接口都有两个设备名:主设备或呼入(callin)设备、交替设备或呼出(callout)设备。设备类型之间使用字母的大小写进行区分。比如,对于任意字母X,”tty”设备名为 /dev/ttyX# ,而”cu”设备名则为 /dev/cux# 。由于历史原因,/dev/ttyS# 和 /dev/ttyC# 分别等价于 /dev/cua# 和 /dev/cub# 。名称 /dev/ttyQ# 和 /dev/cuq# 被保留为本地使用。
伪终端(PTY)

伪终端用于创建登陆会话或提供其它功能,比如通过 TTY line discipline (包括SLIP或者PPP功能)来处理任意的数据生成。每一个 PTY 都有一个master端和一个slave端。按照 System V/Unix98 的 PTY 命名方案,所有master端共享同一个 /dev/ptmx 设备节点(打开它内核将自动给出一个未分配的PTY),所有slave端都位于 /dev/pts 目录下,名为 /dev/pts/# (内核会根据需要自动生成和删除它们)。

一旦master端被打开,相应的slave设备就可以按照与 TTY 设备完全相同的方式使用。master设备与slave设备之间通过内核进行连接,等价于拥有 TTY 功能的双向管道(pipe)。
===============================
公司作一个嵌入式产品,用ARM内核,LINUX操作系统(不是uclinux)。我最近的工作是把一个原来作好的模块(用串口来通信)挂到系统上,通过串口来控制该模块的一系列工作,并要求
作成单独的驱动程序(不是通过应用程序来控制)。同时也想借此熟悉LINUX下设备驱动程序的开发方法。我们买的别的公司的开发板,LINUX现在已经能跑起来,但技术支持和文档基本没有。最近刚开始学习LINUX,算是有了一些了解,但对TTY设备、CONSOLE、串口之间的关系觉得比较混乱。这里有几个问题请教:

1、LINUX下TTY、CONSOLE、串口之间是怎样的层次关系?具体的函数接口是怎样的?串口是如何被调用的?

2、printk函数是把信息发送到控制台上吧?如何让PRINTK把信息通过串口送出?或者说系统在什么地方来决定是将信息送到显示器还是串口?
3、start_kernel中一开始就用到了printk函数(好象是printk(linux_banner什么的),在 这个时候整个内核还没跑起来呢。那这时候的printk是如何被调用的?在我们的系统中,系统启动是用的现代公司的BOOTLOADER程序,后来好象跳到了LINUX下的head-armv.s, 然后跳到start_kernel,在bootloader 里串口已经是可用的了,那么在进入内核后是不是要重新设置?
以上问题可能问的比较乱,因为我自己脑子里也比较乱,主要还是对tty,console,serial之间的关系,特别是串口是如何被调用的没搞清。这方面的资料又比较少(就情景分析中讲了一点),希望
高手能指点一二,非常感谢!
===============================================================================
看到你们的问题后,感觉很有典型性,因此花了点工夫看了一下,做了一些心得贴在这里,欢迎讨论并指正:
1、LINUX下TTY、CONSOLE、串口之间是怎样的层次关系?具体的函数接口是怎样的?串口是如何被调用的?
tty和console这些概念主要是一些虚设备的概念,而串口更多的是指一个真正的设备驱动。
Tty实际是一类终端I/O设备的抽象,它实际上更多的是一个管理的概念,它和tty_ldisc(行规程)和tty_driver(真实设备驱动)组合在一起,目的是向上层的VFS提供一个统一的接口。通过file_operations结构中的tty_ioctl可以对其进行配置。查tty_driver,你将得到n个结果,实际都是相关芯片的驱动。因此,可以得到的结论是(实际情况比这复杂得多):每个描述tty设备的tty_struct在初始化时必然挂如了某个具体芯片的字符设备驱动(不一定是字符设备驱动),可以是很多,包括显卡或串口chip。不知道你的ARM Soc是那一款,不过看情况你们应该用的是常见的chip,这些驱动实际上都有。
而console是一个缓冲的概念,它的目的有一点类似于tty。实际上console不仅和tty连在一起,还和framebuffer连在一起,具体的原因看下面的键盘的中断处理过程。Tty的一个子集需要使用console(典型的如主设备号4,次设备号1―64),但是要注意的是没有console的tty是存在的。
而串口则指的是tty_driver。
举个典型的例子:
分析一下键盘的中断处理过程:
keyboard_interrupt―>handle_kbd_event―>handle_keyboard_event―>handle_scancode
void handle_scancode(unsigned char scancode, int down)
{
……..
tty = ttytab? ttytab[fg_console]: NULL;
if (tty && (!tty->driver_data)) {
……………
tty = NULL;
}
………….
schedule_console_callback();
}
这段代码中的两个地方很值得注意,也就是除了获得tty外(通过全局量tty记录),还进行了console 回显schedule_console_callback。Tty和console的关系在此已经很明了!!!

2、printk函数是把信息发送到控制台上吧?如何让PRINTK把信息通过串口送出?或者说系统在什么地方来决定是将信息送到显示器还是串口?
具体看一下printk函数的实现就知道了,printk不一定是将信息往控制台上输出,设置kernel的启动参数可能可以打到将信息送到显示器的效果。
函数前有一段英文,很有意思:
/*This is printk. It can be called from any context. We want it to work.
*
* We try to grab the console_sem. If we succeed, it’s easy – we log the output and
* call the console drivers. If we fail to get the semaphore we place the output
* into the log buffer and return. The current holder of the console_sem will
* notice the new output in release_console_sem() and will send it to the
* consoles before releasing the semaphore.
*
* One effect of this deferred printing is that code which calls printk() and
* then changes console_loglevel may break. This is because console_loglevel
* is inspected when the actual printing occurs.
*/
这段英文的要点:要想对console进行操作,必须先要获得console_sem信号量。如果获得console_sem信号量,则可以“log the output and call the console drivers”,反之,则“place the output into the log buffer and return”,实际上,在代码:
asmlinkage int printk(const char *fmt, …)
{
va_list args;
unsigned long flags;
int printed_len;
char *p;
static char printk_buf[1024];
static int log_level_unknown = 1;
if (oops_in_progress) { /*如果为1情况下,必然是系统发生crush*/
/* If a crash is occurring, make sure we can’t deadlock */
spin_lock_init(&logbuf_lock);
/* And make sure that we print immediately */
init_MUTEX(&console_sem);
}
/* This stops the holder of console_sem just where we want him */
spin_lock_irqsave(&logbuf_lock, flags);
/* Emit the output into the temporary buffer */
va_start(args, fmt);
printed_len = vsnprintf(printk_buf, sizeof(printk_buf), fmt, args);/*对传入的buffer进行处理,注意还不是
真正的对终端写,只是对传入的string进行格式解析*/
va_end(args);
/*Copy the output into log_buf. If the caller didn’t provide appropriate log level tags, we insert them here*/
/*注释很清楚*/
for (p = printk_buf; *p; p++) {
if (log_level_unknown) {
if (p[0] != ‘<’ || p[1] < ’0′ || p[1] > ’7′ || p[2] != ‘>’) {
emit_log_char(‘<’);
emit_log_char(default_message_loglevel + ’0′);
emit_log_char(‘>’);
}
log_level_unknown = 0;
}
emit_log_char(*p);
if (*p == ‘\n’)
log_level_unknown = 1;
}
if (!arch_consoles_callable()) {
/*On some architectures, the consoles are not usable on secondary CPUs early in the boot process.*/
spin_unlock_irqrestore(&logbuf_lock, flags);
goto out;
}
if (!down_trylock(&console_sem)) {
/*We own the drivers. We can drop the spinlock and let release_console_sem() print the text*/
spin_unlock_irqrestore(&logbuf_lock, flags);
console_may_schedule = 0;
release_console_sem();
} else {
/*Someone else owns the drivers. We drop the spinlock, which allows the semaphore holder to
proceed and to call the console drivers with the output which we just produced.*/
spin_unlock_irqrestore(&logbuf_lock, flags);
}
out:
return printed_len;
}
实际上printk是将format后的string放到了一个buffer中,在适当的时候再加以show,这也回答了在start_kernel中一开始就用到了printk函数的原因

3、start_kernel中一开始就用到了printk函数(好象是printk(linux_banner什么的),在这个时候整个内核还没跑起来呢。那这时候的printk是如何被调用的?在我们的系统中,系统启动是用的现代公司的BOOTLOADER程序,后来好象跳到了LINUX下的head-armv.s, 然后跳到start_kernel,在bootloader 里串口已经是可用的了,那么在进入内核后是不是要重新设置?
Bootloader一般会做一些基本的初始化,将kernel拷贝物理空间,然后再跳到kernel去执行。可以肯定的是kernel肯定要对串口进行重新设置,原因是Bootloader有很多种,有些不一定对串口进行设置,内核不能依赖于bootloader而存在。
===============================================================================
多谢楼上大侠,分析的很精辟。我正在看printk函数。

我们用的CPU是hynix的hms7202。在评估板上是用串口0作
控制台,所有启动过程中的信息都是通过该串口送出的。
在bootloader中定义了函数ser_printf通过串口进行交互。

但我还是没想明白在跳转到linux内核而console和串口尚未
初始化时printk是如何能够工作的?我看了start_kernel
的过程(并通过超级终端作了一些跟踪),console的初始化
是在console_init函数里,而串口的初始化实际上是在1号
进程里(init->do_basic_setup->do_initcalls->rs_init),
那么在串口没有初始化以前prink是如

Comments (527)

linux 查看文件编码以及修改编码

1顶一下  如果你需要在Linux中操作windows下的文件,那么你可能会经常遇到文件编码转换的问题。Windows中默认的文件格式是GBK(gb2312),而Linux一般都是UTF-8。下面介绍一下,在Linux中如何查看文件的编码及如何进行对文件进行编码转换。   查看文件编码   在Linux中查看文件编码可以通过以下几种方式:   1.在Vim中可以直接查看文件编码   :set fileencoding   即可显示文件编码格式...[阅读全文]

1
顶一下

  如果你需要在Linux中操作windows下的文件,那么你可能会经常遇到文件编码转换的问题。Windows中默认的文件格式是GBK(gb2312),而Linux一般都是UTF-8。下面介绍一下,在Linux中如何查看文件的编码及如何进行对文件进行编码转换。

  查看文件编码

  在Linux中查看文件编码可以通过以下几种方式:

  1.在Vim中可以直接查看文件编码

  :set fileencoding

  即可显示文件编码格式。

  如果你只是想查看其它编码格式的文件或者想解决用Vim查看文件乱码的问题,那么你可以在

  ~/.vimrc 文件中添加以下内容:

  set encoding=utf-8 fileencodings=ucs-bom,utf-8,cp936

  这样,就可以让vim自动识别文件编码(可以自动识别UTF-8或者GBK编码的文件),其实就是依照fileencodings提供的编码列表尝试,如果没有找到合适的编码,就用latin-1(ASCII)编码打开。

  2. enca (如果你的系统中没有安装这个命令,可以用sudo yum install -y enca 安装 )查看文件编码

  $ enca filename

  filename: Universal transformation format 8 bits; UTF-8

  CRLF line terminators

  需要说明一点的是,enca对某些GBK编码的文件识别的不是很好,识别时会出现:

  Unrecognized encoding

  文件编码转换

  1.在Vim中直接进行转换文件编码,比如将一个文件转换成utf-8格式

  :set fileencoding=utf-8

  2. enconv 转换文件编码,比如要将一个GBK编码的文件转换成UTF-8编码,操作如下

  enconv -L zh_CN -x UTF-8 filename

  3. iconv 转换,iconv的命令格式如下:

  iconv -f encoding -t encoding inputfile

  比如将一个UTF-8 编码的文件转换成GBK编码

  iconv -f GBK -t UTF-8 file1 -o file2

Comments (336)