404频道

学习笔记

keepalived的作用为保持存活服务,服务启动后会在两台物理机器之间维护一个vip,但是仅有一台物理机器拥有该vip,这样就保证了两台机器之间是主备。

安装

在ubuntu下直接执行:sudo apt-get install keepalived.

使用

本例子两台机器的物理ip地址分别为10.101.185和10.101.1.186,要增加的虚拟ip地址为10.101.0.101、10.101.0.102、10.101.0.107和10.101.0.108,其中10.101.0.101和10.101.0.102在10.101.185上为主,10.101.0.107和10.101.0.108在10.101.1.186上为主。

keepalived的默认配置文件位于/etc/keepalived/keepalived.conf目录下,由于两台物理机器之间的主辅关系不同,配置文件也不相同。

10.101.185机器上的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
! Configuration File for keepalived

global_defs {
# 报警邮箱配置
notification_email {
ops@yidian-inc.com
}
smtp_server 10.101.1.139
smtp_connect_timeout 30
router_id 101-1-185-lg-201-l10.yidian.com // 运行机器的唯一标识,每个机器应该都不一样,可以直接使用hostname代替,具体用在什么地方暂时不是很清楚
}

vrrp_instance ha-internal-1 {
state MASTER
interface eth0
virtual_router_id 1 // VRID标记,可以设置为0-255,对应VRRD协议中的Virtual Rtr Id
priority 100 // 对应VRRD协议中的priority选项
advert_int 1 // 检测间隔,默认为1s,对应VRRD协议中的adver int
authentication {
auth_type PASS // 认证方式,支持PASS和AH
auth_pass 1-internal-ha // 认证的密码,从抓取的包中看到
}
// 声明的虚拟ip地址,这些ip会在VRRP一些的一个包发送
// 另外VRRP协议中还有一个Count IP Addrs用来指明需要声明多少个VIP
virtual_ipaddress {
10.101.0.101/22 dev eth0
10.101.0.102/22 dev eth0
}
}

vrrp_instance ha-internal-2 {
state BACKUP
interface eth0
virtual_router_id 2
priority 99
advert_int 1
authentication {
auth_type PASS
auth_pass 2-internal-ha
}
virtual_ipaddress {
10.101.0.107/22 dev eth0
10.101.0.108/22 dev eth0
}
}

10.101.1.186上的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
! Configuration File for keepalived

global_defs {
notification_email {
ops@yidian-inc.com
}
smtp_server 10.101.1.139
smtp_connect_timeout 30
router_id 101-1-186-lg-201-l10.yidian.com
}

vrrp_instance ha-internal-1 {
state BACKUP
interface eth0
virtual_router_id 1
priority 99
advert_int 1
authentication {
auth_type PASS
auth_pass 1-internal-ha
}
virtual_ipaddress {
10.101.0.101/22 dev eth0
10.101.0.102/22 dev eth0
}
}

vrrp_instance ha-internal-2 {
state MASTER
interface eth0
virtual_router_id 2
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 2-internal-ha
}
virtual_ipaddress {
10.101.0.107/22 dev eth0
10.101.0.108/22 dev eth0
}
}

配置文件搭建完毕后,通过sudo service keepalived start即可启动服务,执行ip addr命令即可看到vip。需要注意的是,通过ifconfig命令是看不到vip的。

有了vip,其他服务就可以利用该vip做一些绑定vip的端口来作为主辅热备模式了。

about vrrp

关于VRRP的详细说明可以查看RFC3768,我这里记录几点说明。

协议中的以太网Destination Address的值必须为多播地址224.0.0.18。

当前正在使用的VRRP版本为version 2,认证功能已经取消,但为了向下兼容,仍然可用。在抓取的包中,仍在使用认证信息

download

这里提供两个vrrp协议的pcap包

一直对堆排序算法用的不错,但是又是在排序中挺重要的算法,并且可以求解其他问题,比如top k问题。已经对堆排序学习过好多次了,无奈每次都记不太清楚具体的细节问题,本文对堆的问题进行整理。本文的排序例子来源于严蔚敏的《数据结构》,本文的知识点来自《算法导论》。

最大堆为堆中的最大元素位于根节点,顾名思义,最小堆的根节点为最小值。堆的性质决定了堆中节点一定大于等于其子节点。在堆排序算法中用到的是最大堆,最小堆用于构造优先队列,要是使用最小堆进行排序,得到的排序结果为倒序。

堆的结构为完全二叉树,因此可以用数组存储来代替树的链式存储结构。

建最大堆过程

我这里通过图表的形式对建大顶堆的过程进行了展示,不再对文字进行叙述。在堆排序的过程中会不断进行建最大堆过程的调用。

堆的初始化

堆排序的核心步骤

清楚了堆的初始化,再看一下下面的堆排序步骤就非常清楚了,不需要图片进行描述了。堆排序的步骤:

  1. 将待排序的数组初始化为大顶堆,该过程即建堆。
  2. 将堆顶元素与最后一个元素进行交换,除去最后一个元素外可以组建为一个新的大顶堆。
  3. 新建立的堆不是大顶堆,需要重新建立大顶堆。重复上面的处理流程,直到堆中仅剩下一个元素。

top k问题的堆解法

该问题最常规和通用的解决思路为使用快速排序,还可以在N的范围不大的情况下采用哈希(桶)的方式。

另外一种解法就是堆排序的思路来解决。这里用到的为最小堆,用最小堆来存储最大的k个数,其中堆顶元素为最大k个数种最小的数。这个解法初看有些别扭,求最大的k个数,居然会用到小顶堆。

该算法必然是一个个遍历N个数一次就够了,这点很好理解。每读取一个新的数x,如果x比堆顶的元素y小,则抛弃;如果x比y大,则用x替换y,并重新更新堆。

十一回农村老家,有件小事有些感悟。

只要家里有人,家里的大门白天一直都是敞着的,外面的人都可以直接走进来,这在农村是很正常的一件事情,门敞着才说明家里是有人的,农村的人没有城里人这么多的隔阂。

正巧地里的活都干得差不多了,母亲在院子里晒着太阳,我在屋子里收拾东西。从门口走到院里一个人来,只见一身道姑打扮,还带一顶帽子,嘴里振振有辞,说是泰山娘娘庙里的保佑家里平安之类的。母亲见到来者第一反应就是骗子,立马上前说是地里有活要干,马上要出去了。来者压根不理会母亲的话,依旧是保佑平安之类的,像极了大街上迎上前去得乞讨者。来者说道,要捐款之类的,并有个小本子,上面写着捐款者的名字。母亲看了眼捐款者的名单,很多都是邻居家的,说明来者刚从家里过来,迟疑了一会便签上了名字,回屋里去取钱。

这时还在屋子里的我才看到并明白过来是这么一回事,我一看母亲名字都签上了,钱肯定是要给的了,想逃掉避免少不了一番纠结。我便回屋子去取钱,在城市待习惯了,知道乞讨者五毛一块就能打发的很高兴,我便从钱包里去了两张一块的,其中一张还是备用的,我先给一块,要是嫌少再给两块。我先于母亲出了屋子,给到了来者一块钱,岂知来者说道别人都是给三十五十的,这些太少了我不要。靠!这年头乞讨还嫌钱少啊,还是我太out了?我直接说那你走吧,我没钱,并转头回屋子,可最烦的是她也跟着往屋里走。

就在这时母亲从屋子里走出来并拿着10块钱,迎上前去给了她。又是嫌少之类的话,最后也不情愿的收下了,并留下了一根红丝带,说些全家平安之类的话就走了,当然了骗子的目的已经完成了,只需匆匆收场就行了。

此事的经过到此结束,但却有个问题挺令我深思的,这也是为什么写下本文的原因。

父母并不富裕,家里一直也是过得比较平淡的生活,在我看来勤奋和勤俭节约一直是我们老家那块的美德。平常家里买个菜什么的都要为了几毛钱掂量半天,但却在面对乞讨施舍这种事情上扔掉了10块钱的巨款,而且母亲对于骗钱这件事从始至终都是知情的。

事后,我的同样上当受骗的邻居也来到了我家,通过邻居和我母亲的谈话我大体理解了他们在经历此事时的心理活动。由于每年都会有多次来到家里进行骗钱的,骗钱的方式是多种多样的,无非是找不到孩子,回不了家之类的,母亲一开始见到骗子就知道是个骗子,这第一印象的判断是过关的。

母亲对骗子的第二个行为是阻拦,母亲用了要去地里干活的信息来阻拦,但是阻拦不彻底,见阻拦不成功就放弃了。这一点上就显现出了的缺点,不知道用合理的手段来保护自己,并完全从主动状态变为了被动状态。之所以意志这么不坚定,其中有一个很大的因素就是母亲知道强加阻拦的后果就是骗子可能会爆粗口,完全不想听骗子絮絮叨叨个没完没了,撵都撵不走,还不如给点钱省事。另外得罪的骗子的后果可能会更麻烦,毕竟骗子往往都是一个团伙,且知道家庭住址,怕有什么报复行为。所以之所有给钱的原因就是花钱买个安宁,免得带来一身的麻烦,当然从这个出发点上给钱是对的。骗子也正是利用了这一点才在农村屡试不爽。

但给钱的数目跟平时的生活水平是完全不相符的,要知道在农村买上10块钱的菜可以吃上好几顿。我觉得之所以出现这个问题,根源在于对自己的不够重视。在中国这种权力的社会中,母亲一直觉得处于权力的最底层,事实也确实如此。即使在自己的家中也很容易变主动为被动,让骗子得手。

可能有人会说农村的法律意识淡薄,不知道合理的维权,完全可以打110来解决。我曾经打110处理过店铺扰民这种鸡毛小事,110给的处理时间为5天,这还是在城市里。我估计换做农村的派出所,这种小事估计110是请不动的。

我在此次事件中并没有第一时间作出应有的反应,这点我需要反省。首先,我见到母亲签字后没有跟骗子要一下工作证明,这样子至少让骗子没这么容易得逞,给我的反驳增加大大的筹码。其次,没有阻拦母亲给骗子送钱,按照我的意思是跟骗子死缠到底的,毕竟是在我家,给不给钱也是我的自由。但我怕在家里生活时间不长,骗子的规矩我不懂,还是顺从了母亲的行为。

之所以写这篇文章是想梳理下农民身上的共同缺点及我身上的缺点。我不是歧视农民,我是农村出来的,我知道农村人的辛苦,忙起来的时候他们的辛苦程度是我等码农不能企及的。

在这里为广大奋斗在田地里的农民致敬!

本文在学习saltstack的过程中编写,内容比较基础,方便使用时查阅命令。

安装

为了方便起见,直接采用yum的安装方式,centos源中并没有salt,需要手工添加一下。

CentOS 7

安装master

1
2
rpm -Uvh http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/7/x86_64/e/epel-release-7-5.noarch.rpm
yum install salt-master

修改/etc/salt/master配置文件,在其中指定salt文件根目录位置,默认路径为/srv/salt/。

1
2
3
file_roots:
base:
- /svr/salt/

salt在安装的时候已经创建了systemctl命令启动程序需要的service文件,位于/usr/lib/systemd/system/salt-master.service,重启systemctl restart salt-master.service生效。

CentOS 6.5

安装minion

1
2
rpm -Uvh http://ftp.linux.ncsu.edu/pub/epel/6/i386/epel-release-6-8.noarch.rpm
yum install salt-minion

修改/etc/salt/minion配置文件,在其中指定master主机的地址

1
master: 192.168.204.128

执行service salt-minion restart对服务进行重启。

连通性测试

执行salt-key -L命令可以看到已认证和未认证的minion,执行salt-key -a 192.168.204.149可接收minion。

在master主机中执行salt '*' test.ping可测试连接的minion主机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  stats  salt-key -L
Accepted Keys:
Denied Keys:
Unaccepted Keys:
192.168.204.149
Rejected Keys:

➜ stats salt-key -a 192.168.204.149
The following keys are going to be accepted:
Unaccepted Keys:
192.168.204.149
Proceed? [n/Y] y
Key for minion 192.168.204.149 accepted.

➜ stats salt '*' test.ping
192.168.204.149:
True

state

可以通过预先定义好的sls文件对被控主机进行管理,这里演示一个简单的文件复制的例子,该例子可以将master主机上的vimrc文件复制到目标主机上。

在master主机的/svr/salt/edit目录下新建vim.sls文件,文件内容如下:

1
2
3
4
5
6
/etc/vimrc:
file.managed:
- source: salt://edit/vimrc
- mode: 644
- user: root
- group: root

另外在edit目录下需要存在一个空的init.sls,以确保state.sls可以找到该目录下的sls文件。同时该目录下还需要存在要复制的vimrc文件。

执行salt '*' state.sls edit.vim即可以执行该命令。

如果将vim.sls更改为init.sls文件,执行salt '*' state.sls edit命令即可。

常用命令

salt ‘192.168.204.149’ cmd.run ‘free -m’

salt ‘192.168.204.149’ sys.list_modules 列出minion支持哪些模块,默认已经支持很多模块

salt ‘192.168.204.149’ cp.get_file salt://test_file /root/test_file 将master主机file_roots目录下的文件复制到minion任意目录下,该命令不可以将master主机任意目录下的文件进行复制

salt ‘192.168.204.149’ cp.get_dir salt://test_dir/ /root/ 实验未成功

salt ‘*’ file.mkdir dir_path=/root/test_dir user=root group=root mode=700 在minion主机上创建目录s

参考文章

*saltstack官方文档

*《python自动化运维技术与最佳实践》

前段时间我的工作有了一个比较大的变动,我的工作地点从济南到了北京,离开了待了9年的济南,离开了温馨的家,离开了我的亲人,独自一人开启了北漂模式。

本文不打算叙述面试细节问题,具体的面试细节我自己在印象笔记中整理过,但不打算放出来。本文仅选取有代表性的几家公司来叙述。

之所以有如此大的变动,最大的因素是我的个人技术的发展遇到了瓶颈。我近几年对个人发展的定位是在技术上能够更上一层楼,尽量不走管理路线,还是以踏踏实实学技术为主要任务。可是在济南工作已经慢慢被推上了管理的岗位,我怕会逐渐脱离技术,直到完全走上了管理的岗位,这跟我对技术非常感兴趣的初衷是有些违背的。

工作这几年,技术的进步主要来自于自己业余时间的学习,工作中越来越学不太着东西,不是因为工作中不需要牛逼的技术,而是没有人和精力去研究新技术,济南缺乏这个环境,而且是非常匮乏。我已经深深感觉到济南的IT行业未来会逐渐缩小,实际上目前济南的圈子就是很小的,稍微有点规模的公司也就那么几家而已。

我自从开始工作就以互联网公司为目标,可以济南没有一家真正意义上的互联网公司,用的技术也都不咋地,工作五年后,有了一定的技术积累,家庭也算稳定了,是时候出来闯闯了,不能在济南的安逸环境中,像水煮青蛙般等待着济南IT业的下滑。

这次来北京找工作的目标非常明确,互联网公司,最好是规模能够稍微大些的,BAT更好,不想加入A轮的公司,我需要的是成熟的互联网公司的环境和技术,来洗刷我已经在传统行业奋斗了多年的旧习。

我是裸辞的,因为毕竟面试是需要去北京的,在职请假面太麻烦,且不能够全身心的找工作。我给自己找工作的期限为一个月的时间,我最终上班的时间是在20天多点的时间。

在辞职后的第一周我在家里边休息边看了一遍《STL源码剖析》,之前一直觉得没必要看此书,结果看起来效果还不错,比我想象的要简单的多的多。另外,我制作了自己的简历,包括了pdf版本和markdown版本,并在拉勾网上投递了几份简历。

就这样第一周过去了,而我没有收到任何的面试通知,第二周我必须加紧开始找工作了。首先在100offer上申请拍卖了我的简历,之前一直关注100offer,微信公众账号和知乎上经常看到100offer的文章,感觉是个靠谱的平台,事实证明确实是一个靠谱的平台。另外,恰巧我在微信公共账号“余晟以为”的文章看到了怎样写简历的文章,就跟作者聊了一会,并且作者在twitter上推荐了我的简历,我的博客有史以来日pv达到了600多,这是我没有想到的,在这里非常感谢余晟的无私帮助。

100offer上拍卖后没多久就收到了二个面试通知,周三上午就赶到了北京参加面试。上午参加的一家游戏公司的面试,用C++做后台的业务逻辑处理,其实我并不喜欢游戏类工作,来面试也仅仅是为了了解北京面试的流畅增加一些资历而已。两天后收到了该公司的offer,而我理所当然是拒绝了。

周四上午参加了一个创业团队的面试,面试官年龄跟我差不多,问的问题非常多,非常杂,最后还有一个我最讨厌的逻辑题,一直面试到下午一点。这家公司的面试是通过twitter上看到我的简历联系我的,我来的目的其实也是出于学习和了解行业。面试完后跟团队成员一起吃了个饭,令我感动的是我面试完已经一点钟了,而大家都在等着我一起吃饭,团队的成员都是做技术的码农,还是非常好相处的。如果我已经有了几年的工作经历,或许我会选择这样一家公司。

下午参加了我目前所在公司一点资讯的面试,面试流程还是非常nice的,感觉跟我心目中的互联网公司基本吻合,虽然面试的时候问的问题过于简单些,令我有点怀疑公司的真实技术实力。另外还问了一堆我并不深入的web技术问题,令我非常捉急。

没有面试的时候,我就直接回济南了,毕竟在自己家里比北京不知舒服多少倍。

第三周的时候,我已经开始有些慌了。该用的能有面试机会的方式我都用了,而却收不到面试的邀请了。我用到了100offer、拉勾网、内推、jobdeer、同学内推的方式。难道是我简历写的太水了,可是我已经很难改进自己的简历了,我不想将简历写的夸张了。

我开始反思原因。我发现互联网用到的我擅长的C++技术的公司非常少,互联网追求的是短平快,C++并不具备开发速度快的特点,因此并不受互联网公司欢迎。像BAT类的公司用C++技术还是比较多的,因为做到一定程度会深究程序的性能,而这是C++擅长的。而BAT类的公司,我并没有任何优势,我虽有几年工作经验,但是都是在传统行业,互联网行业的经验却为0。很多大公司的hr在看到我的简历后直接就给pass掉了,压根没有面试的机会。

我找同学内推了百度的简历,另外在拉勾网上也投了一些百度的简历。在我入职之前的几天百度的hr妹子给我电话沟通说一周之内会给我面试通知,最终的结果是一周后百度hr妹子给我打电话让我去面试,看来大公司的流程真是复杂,连面试都得排队,而那天是我入职的第一天。没办法,只能委婉的拒绝了,hr倒是很爽快的挂掉了电话。

目前,我已经入职有三周的时间了,工作已经趋于稳定。我也搬到了公司附近,离公司就几分钟的路程,毕竟就自己一个人,没必要离公司太远,上下班太折腾。晚上一般会在公司待到十点以后,毕竟回去了也没太有什么事情。周末会抽时间回济南跟家人团聚,或者家人来北京,不期望因为工作的原因而对家庭有损伤。工作方面的内容还算满意,能涉及到很多新技术,对个人的成长还不错,只是组内的人员较少,沟通交流的机会不够多,通过招聘慢慢就会解决了。由于用到的很多技术都不够熟悉,自己俨然变成了一个菜鸟,有大量的技术需要学习。同事也还比较给力,大家对工作也很认真负责,团队的凝聚力也符合我的预期。

总结来看,这段找工作的经历虽有很多失误的地方,错误的对行业的需求进行了估计,以为C++程序员很抢手,事实并不是如此,但结果还算满意。现在自己一个人在北京奋斗,期望通过自己的努力能够有个好的收货。要想写的东西很多,很多都一笔带过了,非技术类的文章写起来还是挺费脑力的。

最近在开发程序的过程中遇到了一个getaddrinfo函数的问题,令我感到非常奇怪。

程序中调用了librdkafka库,当程序选择用-static方式链接所有库时程序会在librdkafka库中某个函数core dump,但是选择动态链接系统库(包括libpthread、libdl、libz、libm、libc等)时程序却能正常运行。

每次程序都回core dump在getaddrinfo函数中,经过搜索发现有人跟我遇到同样的问题,但是却没有解决方案。

我这里实验了文中提到了例子,在静态链接的时候确实会报错,动态链接却非常正常,编译选项为g++ -o test_getaddrinfo test_getaddrinfo.cpp -lpthread -Wall -static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
include <stdio.h>
#include <netdb.h>
#include <pthread.h>
#include <unistd.h>

void *test(void *)
{
struct addrinfo *res = NULL;
fprintf(stderr, "x=");
int ret = getaddrinfo("localhost", NULL, NULL, &res);
fprintf(stderr, "%d ", ret);
return NULL;
}

int main()
{
for (int i = 0; i < 512; i++)
{
pthread_t thr;
pthread_create(&thr, NULL, test, NULL);
}
sleep(5);
return 0;
}

发现程序在链接的时候会提示如下警告:

1
2
3
4
/tmp/cc0WILtn.o: In function `test(void*)':
test_getaddrinfo.cpp:(.text+0x49): warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/lib/gcc/x86_64-redhat-linux/4.8.3/../../../../lib64/libpthread.a(libpthread.o): In function `sem_open':
(.text+0x685b): warning: the use of `mktemp' is dangerous, better use `mkstemp'

从网上查看有该警告的人还是非常多的,都是在-static方式链接glibc库时遇到的,但是没有发现很好的解决方案。该问题的原因
产生估计是glibc在静态链接时调用libnss库存在问题,因此不提倡静态链接方式。

我看到了两种解决方案:

方案一:用newlib或uClibc来代替glibc来静态链接,这种方案我没有去尝试是否可行。

方案二:用--enable-static-nss重新编译glibc。我试了一下问题仍然存在。

我之所以采用静态链接的方式,是因为开发机器和运行机器的glibc版本不一致造成的。我尝试将libc.so相关文件复制运行机器上,并让程序链接我复制过去的文件,ldd查看可执行文件没有错误,但是当运行程序时会报如下错误:

1
./xxx: relocation error: /home/kuring/lib/libc.so.6: symbol _dl_starting_up, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference

最终,我放弃了静态链接的方式,采用了动态链接方式来暂时解决了问题。如果你知道解决方案,请告诉我。

参考文章

这是一篇拿来主义的文章,所有的安装步骤仅为互联网上查找,网络上的教程各种凌乱,这里根据我的实践情况进行了更改,本文仅记录了我的安装过程,由于不同环境可能导致安装步骤不甚相同。

MAC OS X 10.10

php

Mac OSX 10.10的系统自带了php、php-fpm,省去了安装的麻烦,可以执行php -v查看php的版本。这里需要简单地修改下php-fpm的配置,否则运行php-fpm会报错。

1
2
sudo cp /private/etc/php-fpm.conf.default /private/etc/php-fpm.conf
vim /private/etc/php-fpm.conf

修改php-fpm.conf文件中的error_log项,默认该项被注释掉,这里需要去注释并且修改为error_log = /usr/local/var/log/php-fpm.log。如果不修改该值,运行php-fpm的时候会提示log文件输出路径不存在的错误。

如果系统中存在多个php-fpm.conf,不知道需要编辑哪一个,可以执行php-fpm -t命令查看php-fpm要读取的配置文件。

通过php-fpm -D来启动php-fpm,可以通过lsof -Pni4 | grep LISTEN | grep php命令来查看php-fpm是否监听在9000端口。

nginx

这里为了简单,直接采用了brew的方式安装。执行

1
brew install nginx

nginx的配置文件位于/usr/local/etc/nginx/nginx.conf,默认只能解析html文件,需要配置后才能调用php-fpm解析php文件。下面内容为该修改后的文件全部有效内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
worker_processes  1;
events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
root /Users/kuring/www; // 页面存放路径
location / {
index index.html index.htm index.php;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
}
include servers/*;

然后执行nginx命令即可启动,默认监听的端口为8080,在浏览器中输入http://127.0.0.1:8080即可看到nginx的初始界面。nginx要想监听1024以下端口还需要进一步的配置,8080端口既能满足我需求,不再更改。

mysql

1
brew install mysql

在启动mysql之前可对mysql的配置文件进行更改,我这里需要更改mysql的编码方式,将所有的编码方式都更改为utf8,防止乱码问题的发生。mysql的配置文件为my.cnf,我的位于/usr/local/Cellar/mysql/5.6.25/my.cnf,对文件添加如下内容,有些选项不存在,可手动添加。

1
2
3
4
5
6
7
8
[mysqld]
character-set-server=utf8

[client]
default-character-set=utf8

[mysqld_safe]
default-character-set=utf8

输入mysqld命令即可启动mysql,启动mysql后输入mysql_secure_installation命令对mysql进行配置,可以设置root用户的密码。

通过mysql -uroot -p命令连接到mysql后,输入status命令可查看刚才更改的编码是否生效。

由于不需要长期使用mysql,这里不设置mysql自启动命令。

CentOS6

我首先采用的方案为完全用普通用户安装,尝试失败后采用root安装依赖库普通用户编译程序的方案。

普通用户安装依赖库

我首选选择在普通用户kuring下进行安装和运行整个web环境。因此不能使用yum安装方式,必须采用源码安装的方式。通过普通用户安装,最麻烦的地方就在于需要安装很多的依赖库,而依赖的库的安装可能又有需要的库,且库之间存在版本问题。

首先普及几个小知识:

bash查找命令的先后顺序为:

  1. alias别名
  2. shell中的关键字,如if等
  3. shell中的函数
  4. shell内置命令,如echo等
  5. $PATH环境变量,PATH中的匹配顺序为从前向后的。

程序查找lib库的先后顺序为:

  1. 编译程序时指定的链接库路径,g++编译器可以通过-Wl,-rpath,路径来指定链接库的路径。
  2. 环境变量LD_LIBRARY_PATH指定的搜索路径。
  3. /etc/ld.so.conf指定的路径。
  4. 默认的系统动态库搜索路径,如/usr/lib64、/usr/local/lib64等。

很多程序采用pkg-config程序来检查库的版本号,pkg-config命令依赖于动态链接库对应的.pc文件,这些.pc文件一般位于系统的/usr/local/lib/pkgconfig目录下。为了能够将安装完成的库通过pkg-config找到对应的.pc文件,需要将.pc文件所在的路径/home/kuring/local/lib/pkg-config设置到环境变量PKG_CONFIG_PATH中。

安装php依赖的libxml2库时提示找不到libtool、autoconf和automake,首先安装libtool。执行./configure --prefix=/home/kuring/local;make; make install将其安装到当前用户的local目录下。

用同样的步骤安装autoconf,执行./configure --prefix=/home/kuring/local;make; make install

为了能够将安装的程序起作用,需要将/home/kuring/local目录添加到PATH环境变量中,在.bash_profile文件中添加PATH=$PATH:$HOME/local/bin语句,并执行source ~/.bash_profile

安装之前需要先安装libxml2库,下载地址采用git clone git://git.gnome.org/libxml2的方式下载。在执行sh autogen产生configure配置文件的过程中,发现提示

1
2
./configure: line 13094: syntax error near unexpected token `LZMA,liblzma,'
./configure: line 13094: ` PKG_CHECK_MODULES(LZMA,liblzma,'

经过发现是由于找不到PKG_CHECK_MODULES造成的,正常情况下该函数定义在aclocal.m4文件,而该情况下aclocal.m4文件中并不存在该函数。之所以不存在是由于aclocal命令找不到pkg.m4文件造成的,可以通过aclocal --print命令查看查找的pkg.m4文件的路径。我这里的解决思路为直接从其他机器上复制一个pkg.m4文件过来。

在产生了configure命令后,执行./configure --prefix=/home/kuring/local命令后发现提示找不到Python.h命令的错误。

鉴于遇到了如此之多的错误,本着不浪费生命的原则还是采用yum来安装依赖库吧。

php

这里的mysql直接采用了yum命令安装的。

在执行php的./configure命令后提示libxml2找不到错误,直接yum install libxml-devel命令安装libxml-devel即可。然后执行./configure --enable-fpm --prefix=/home/kuring/php5.5 --with-mysqli=/usr/bin/mysql_config;make; make install;。在编译php的时候要加上php-fpm选项来安装php-fpm命令。

安装后配置~/.bash_profile文件的$PATH环境变量的值为:PATH=$HOME/bin:$HOME/php5.5/bin:$HOME/php5.5/sbin:$PATH。

此时即可通过php-fpm -D命令来启动php-fpm命令了。

安装完成后通过phpinfo()函数查看里面有MySQLi的选项,但是实际程序运行的时候居然不支持mysqli的一些力函数,说明mysqli的扩展安装不成功。在/home/kuring/php5.5/include/php/ext/mysqli目录中找到了对应的.h文件,却没有找到mysqli.so的动态链接库文件。大概是由于在编译php时mysql的路径配置有些问题造成的,因为mysql是通过yum安装的,路径比较乱一些。

为了能够产生mysqli.so文件,采用单独编译的方式,在php的源码目录中已经包含了mysqli的源码,进入mysqli源码目录下执行phpize;./configure --prefix=/home/kuring/php5.5/mysqli --with-php-config=/home/kuring/php5.5/bin/php-config --with-mysqli=/usr/bin/mysql_config;make;make install;。将mysqli.so文件安装到了/home/kuring/php5.5/lib/php/extensions/no-debug-non-zts-20121212目录下,不知道为什么目录末尾还要加个这么长的文件夹名,直接将文件复制到上一级目录下。

在/home/kuring/php5.5目录下没有找到php.ini文件,通过php --ini命令查看php的配置文件路径为/home/kuring/php5.5/lib,直接从php的源码文件中复制一个php.ini文件到该目录下。并将php.ini中的增加如下内容:

1
2
extension_dir = "/home/kuring/php5.5/lib/php/extensions"
extension=mysqli.so

再运行程序,发现mysqli的系列函数已经支持了,好一段折腾。

php-fpm

执行cp $HOME/php5.5/etc/php-fpm.conf.default $HOME/php5.5/etc/php-fpm.conf来增加配置文件。

nginx

首先安装pcre库,该库为正则表达式库。下载后通过

下载源码后执行./configure --prefix /home/kuring/nginx;make;make install;即可安装完成。

修改nginx的配置文件为如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
worker_processes  1;
events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
root /home/kuring/www;
location / {
index index.html index.htm index.php;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
}
include servers/*;

常见操作

nginx

  • nginx -s stop:关闭
  • nginx -t:检测nginx的配置是否正确

mysql

  • mysqld_safe:启动mysql
  • mysqladmin shutdown -u root -p:关闭mysql
  • create user kuring identified by ‘kuring_pass’:mysql创建用户(我尝试过几次,每次创建的用户密码都为空)
  • drop user kuring:删除一个用户
  • grant all privileges on . to ‘root‘@’%’ identified by ‘root’ with grant option :允许mysql的root用户通过远程登录

创建用户的操作

使用create user kuring identified by 'kuring_pass'命令创建用户kuring。默认创建完成的用户在本机无法登陆,但是远程却可以登陆。 这是因为mysql数据库中的user表中存在一条记录造成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use mysql;
select user,host,password from user;

// 在表中存在一条用户名为空的记录
+---------+-----------+-------------------------------------------+
| User | host | password |
+---------+-----------+-------------------------------------------+
| root | localhost | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
| root | 127.0.0.1 | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
| root | ::1 | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
| | localhost | |
| root | % | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
| report1 | % | *884CAA4D6FA1C3F7E4849C8DAF1B5B37FCB3EC0B |
+---------+-----------+-------------------------------------------+

// 将mysql中的为空的记录删除掉,这样就可以通过创建的用户连接了

在mysql命令行中执行grant all privileges on kuring_db.* to kuring identified by 'kuring_pass'命令可以给刚创建的用户对数据库的权限。

修改mysql用户密码

mysql将用户名和密码存放到了mysql数据库的user表中,在mysql命令行中执行use mysql;update user set password=password("new password") where user="username";flush privileges;即可更新相应用户的密码。

php-fpm

  • php-fpm -D:启动php-fpm,如果需要指定php.ini文件,可以使用-c参数
  • php-fpm -t:检查php-fpm的配置文件
  • kill -USR2: 重启php-fpm
  • kill -INT: 停止php-fpm

php

  • php –ini:显示php.ini文件路径

参考文章

  1. 运用Autoconf和Automake生成Makefile的学习之路

我在软件行业工作已经有五个年头了,在现在这家公司已经有两个年头了。虽然身为公司的研发部经理可以参与公司的一些决定,但是没有绝对的话语权,对于公司的很多决定我深知是错误的,虽然后来也证明是错误的,但是我仍然无能为力。这里总结一下在公司中遇到的问题。

宁可招聘一个技术水平高的也不愿招聘三个技术水平低的。在工作中能够非常有力的证明这一点,三个刚工作的技术人员,尤其对于C++这样门槛稍微高一些,需要工作经验来弥补C++中坑的语言,三个C++技术人员远没有一个高水平的工作效率高,因为三个菜鸟需要将大牛踩过的坑全部踩一遍,踩过多少坑就代表走了多少弯路。

兴趣是最大的老师。我带过不少人,很多都是新人,我给他们制定了学习计划,期望他们能够在业余时间多学习,但实际上哪有几个人能够充分利用业余时间的。我就非常怀疑他们对技术的兴趣问题,如果他们对技术不感兴趣那为什么要加入该行业,为没有兴趣的工作而工作就是自己对自己耍流氓。如果他们对技术感兴趣,那只能说明他们业余时间中有更大的诱惑。

在招聘中不要过于在意金钱,便宜无好货在招聘行业中仍然非常适用。在招聘中千万不要吝惜给员工的那点钱,因为一千块钱而错失一个好的员工是非常不值得的。

盈利模式决定了公司对产品的态度。我所在的软件行业属于传统的软件行业,传统软件行业的盈利模式为销售,由于软件具有可复制性的特点,因此只要一套产品卖的越多就赚的越多。对于传统软件行业的产品使用者很多情况下就是几个人,至少跟互联网产品的用户数量不在一个量级。使用的人数决定了传统软件行业的用户体验可以做的很烂,技术水平可以不用那么高,只要能用就行,慢点无所谓,只要能卖出去就行了。身为一个技术人员,一个对技术有追求的技术人员,这令我非常反感,我做技术我不能对技术无所谓,我讨厌听到无所谓这样的字眼。

一定要明确公司的定位,明白什么时候应该干什么,什么应该干,野心太大也是问题。公司处于成长阶段提出了今年营业额比去年增长10倍的目标,我听到之后就是嗤之以鼻,这压根就是不可能的任务,而事实证明这也根本不可能完成,实际上当年营业额仅比去年增长了一倍。

一家公司一定要有自己的明确产品线,要抵住外界的诱惑。公司的产品线本来是非常明确的,后来由于客户需求和各种方面的原因,开始考虑疯狂扩展产品,这就造成了本来人手就紧蹙的情况下,没有时间去改善现有的系统,不得不去研发新的产品。自己没有的产品甚至跟客户合作或者完全购买别人的产品,导致公司很多人都在考虑跟其他公司合作的事宜。结果可想而知,新产品的销售并不理想,旧有的产品升级维护的也开始变慢。ps:我是非常讨厌在技术上跟其他公司之间考虑合作的问题,因为这从本质上讲并没有产生任何的社会价值,技术上必然涉及到接口的问题,只要是接口必然会有很多细节问题,这些往往会出现技术人员扯皮的问题,一个问题你可以解决他也可以解决,但是谁都不愿意解决,你说烦不烦。

技术人员后来要么转行要么做管理了。在济南技术人员就这两种出路吧,没见过多少大龄的程序员,很多情况下写着写着程序突然发现自己转为公司的中层了,比如我,并逐渐参与公司的事务。很多对程序不感兴趣的,可能就直接换个行业或者转行做销售了。

有些人再怎么培养也成不了高手。在工作我发现,有些人即使有了几年的工作经验,对公司的产品也非常了解,但是在解决问题的时候总是找不到点子上,占了一大堆资源,最后解决起问题来即慢又绕弯路,还留下一堆bug。对于这部分人,我想说也许这个行业不适合你。

领导千万不可三天两头一个想法,这在员工看来就是一个不靠谱的领导。谁都不愿意追随一个拿着自己当猴耍的领导,一会一个想法只能说明领导不够成熟,不适合做领导。跟随杰出的人,为杰出的人工作。

搞公司最好不要搞施工太久的。公司很多做工程的都在为现场的情况忙碌,一个点架设完毕后往往还需要耗费大量的时间来维护,维护对于公司而言牵涉到精力太大,尽量避免需要整天跟客户打交道和整天维护的业务。

专科生是很难撑起一家科技企业。虽然我不完全认同学历就能决定能力,但学历跟能力之间是成正比关系的。我的朋友中有专科生在工作几年后可以做专业的视频教程,并且业余时间写过几部玄幻小说。由于学习经历的不同这就造就了科班出身的程度不同,自然能力之间是有差异的。虽然中国的大学教育跟工作很脱节,但是在工作中还是能够跟大学教育挂起钩来的。学历跟素质之间也是成正比关系的,这里的素质体现在工作中就包括了工作中的责任心,工作态度等方面,这里就不展开了,要展开的话我可以举出非常多活生生的例子。因此,我非常不提倡在公司招聘中招聘专科生。我发现在很多情况下,很多专科人员是连普通话都不会的,操着各种方言或各种被普通话的方言。基本上能不能说普通话也是一个断定人素质的标准,扩展到其他行业同样适用。

也许本文的观点有些偏激,没错我就是一个偏激的IT工程师,就酱。

在C++98中有左值和右值的概念,不过这两个概念对于很多程序员并不关心,因为不知道这两个概念照样可以写出好程序。在C++11中对右值的概念进行了增强,我个人理解这部分内容是C++11引入的特性中最难以理解的了。该特性的引入至少可以解决C++98中的移动语义和完美转发问题,若你还不清楚这两个问题是什么,请向下看。

温馨提示,由于内容比较难懂,请仔细看。C++已经够复杂了,C++11中引入的新特性令C++更加复杂了。在学习本文的时候一定要理解清楚左值、右值、左值引用和右值引用。

移动构造函数

首先看一个C++98中的关于函数返回类对象的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class MyString {
public:
MyString() {
_data = nullptr;
_len = 0;
printf("Constructor is called!\n");
}

MyString(const char* p) {
_len = strlen (p);
_init_data(p);
cout << "Constructor is called! this->_data: " << (long)_data << endl;
}

MyString(const MyString& str) {
_len = str._len;
_init_data(str._data);
cout << "Copy Constructor is called! src: " << (long)str._data << " dst: " << (long)_data << endl;
}

~MyString() {
if (_data)
{
cout << "DeConstructor is called! this->_data: " << (long)_data << endl;
free(_data);
}
else
{
std::cout << "DeConstructor is called!" << std::endl;
}
}

MyString& operator=(const MyString& str) {
if (this != &str) {
_len = str._len;
_init_data(str._data);
}
cout << "Copy Assignment is called! src: " << (long)str._data << " dst" << (long)_data << endl;
return *this;
}

operator const char *() const {
return _data;
}

private:
char *_data;
size_t _len;

void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
};

MyString foo()
{
MyString middle("123");
return middle;
}

int main() {
MyString a = foo();
return 1;
}

该例子在编译器没有进行优化的情况下会输出以下内容,我在输出的内容中做了注释处理,如果连这个例子的输出都看不懂,建议再看一下C++的语法了。我这里使用的编译器命令为g++ test.cpp -o main -g -fno-elide-constructors,之所以要加上-fno-elide-constructors选项时因为g++编译器默认情况下会对函数返回类对象的情况作返回值优化处理,这不是我们讨论的重点。

1
2
3
4
5
6
Constructor is called! this->_data: 29483024 // middle对象的构造函数
Copy Constructor is called! src: 29483024 dst: 29483056 // 临时对象的构造,通过middle对象调用复制构造函数
DeConstructor is called! this->_data: 29483024 // middle对象的析构
Copy Constructor is called! src: 29483056 dst: 29483024 // a对象构造,通过临时对象调用复制构造函数
DeConstructor is called! this->_data: 29483056 // 临时对象析构
DeConstructor is called! this->_data: 29483024 // a对象析构

在上述例子中,临时对象的构造、复制和析构操作所带来的效率影响一直是C++中为人诟病的问题,临时对象的构造和析构操作均对堆上的内存进行操作,而如果_data的内存过大,势必会非常影响效率。从程序员的角度而言,该临时对象是透明的。而这一问题正是C++11中需要解决的问题。

在C++11中解决该问题的思路为,引入了移动构造函数,移动构造函数的定义如下。

1
2
3
4
5
6
MyString(MyString &&str) {
cout << "Move Constructor is called! src: " << (long)str._data << endl;
_len = str._len;
_data = str._data;
str._data = nullptr;
}

在移动构造函数中我们窃取了str对象已经申请的内存,将其拿为己用,并将str申请的内存给赋值为nullptr。移动构造函数和复制构造函数的不同之处在于移动构造函数的参数使用*&&*,这就是下文要讲解的右值引用符号。参数不再是const,因为在移动构造函数需要修改右值str的内容。

移动构造函数的调用时机为用来构造临时变量和用临时变量来构造对象的时候移动语义会被调用。可以通过下面的输出结果看到,我们所使用的编译参数为g++ test.cpp -o main -g -fno-elide-constructors --std=c++11

1
2
3
4
5
6
Constructor is called! this->_data: 22872080 // middle对象构造
Move Constructor is called! src: 22872080 // 临时对象通过移动构造函数构造,将middle申请的内存窃取
DeConstructor is called! // middle对象析构
Move Constructor is called! src: 22872080 // 对象a通过移动构造函数构造,将临时对象的内存窃取
DeConstructor is called! // 临时对象析构
DeConstructor is called! this->_data: 22872080 // 对象a析构

通过输出结果可以看出,整个过程中仅申请了一块内存,这也正好符合我们的要求了。

C++98中的左值和右值

我们先来看下C++98中的左值和右值的概念。左值和右值最直观的理解就是一条语句等号左边的为左值,等号右边的为右值,而事实上该种理解是错误的。左值:可以取地址,有名字的值,是一个指向某内存空间的表达式,可以使用&操作符获取内存地址。右值:不能取地址,即非左值的都是右值,没有名字的值,是一个临时值,表达式结束后右值就没有意义了。我想通过下面的例子,读者可以清楚的理解左值和右值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lvalues:
//
int i = 42;
i = 43; // i是左值
int* p = &i; // i是左值
int& foo();
foo() = 42; // foo()返回引用类型是左值
int* p1 = &foo(); // foo()可以取地址是左值

// rvalues:
//
int foobar();
int j = 0;
j = foobar(); // foobar()是右值
int* p2 = &foobar(); // 编译错误,foobar()是右值不能取地址
j = 42; // 42是右值

C++11右值引用和移动语义

在C++98中有引用的概念,对于const int &m = 1,其中m为引用类型,可以对其取地址,故为左值。在C++11中,引入了右值引用的概念,使用*&&*来表示。在引入了右值引用后,在函数重载时可以根据是左值引用还是右值引用来区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void fun(MyString &str)
{
cout << "left reference" << endl;
}

void fun(MyString &&str)
{
cout << "right reference" << endl;
}

int main() {
MyString a("456");
fun(a); // 左值引用,调用void fun(MyString &str)
fun(foo()); // 右值引用,调用void fun(MyString &&str)
return 1;
}

在绝大多数情况下,这种通过左值引用和右值引用重载函数的方式仅会在类的构造函数和赋值操作符中出现,被例子仅是为了方便采用函数的形式,该种形式的函数用到的比较少。上述代码中所使用的将资源从一个对象到另外一个对象之间的转移就是移动语义。这里提到的资源是指类中的在堆上申请的内存、文件描述符等资源。

前面已经介绍过了移动构造函数的具体形式和使用情况,这里对移动赋值操作符的定义再说明一下,并将main函数的内容也一起更改,将得到如下输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
MyString& operator=(MyString&& str) { 
cout << "Move Operator= is called! src: " << (long)str._data << endl;
if (this != &str) {
if (_data != nullptr)
{
free(_data);
}
_len = str._len;
_data = str._data;
str._len = 0;
str._data = nullptr;
}
return *this;
}

int main() {
MyString b;
b = foo();
return 1;
}

// 输出结果,整个过程仅申请了一个内存地址
Constructor is called! // 对象b构造函数调用
Constructor is called! this->_data: 14835728 // middle对象构造
Move Constructor is called! src: 14835728 // 临时对象通过移动构造函数由middle对象构造
DeConstructor is called! // middle对象析构
Move Operator= is called! src: 14835728 // 对象b通过移动赋值操作符由临时对象赋值
DeConstructor is called! // 临时对象析构
DeConstructor is called! this->_data: 14835728 // 对象b析构函数调用

在C++中对一个变量可以通过const来修饰,而const和引用是对变量约束的两种方式,为并行存在,相互独立。因此,就可以划分为了const左值引用、非const左值引用、const右值引用和非const右值引用四种类型。其中左值引用的绑定规则和C++98中是一致的。

非const左值引用只能绑定到非const左值,不能绑定到const右值、非const右值和const左值。这一点可以通过const关键字的语义来判断。

const左值引用可以绑定到任何类型,包括const左值、非const左值、const右值和非const右值,属于万能引用类型。其中绑定const右值的规则比较少见,但是语法上是可行的,比如const int &a = 1,只是我们一般都会直接使用int &a = 1了。

非const右值引用不能绑定到任何左值和const右值,只能绑定非const右值。

const右值引用类型仅是为了语法的完整性而设计的, 比如可以使用const MyString &&right_ref = foo(),但是右值引用类型的引入主要是为了移动语义,而移动语义需要右值引用是可以被修改的,因此const右值引用类型没有实际意义。

我们通过表格的形式对上文中提到的四种引用类型可以绑定的类型进行总结。

引用类型/是否绑定 非const左值 const左值 非const右值 const右值 备注
非const左值引用
const左值引用 全能绑定类型,绑定到const右值的情况比较少见
非const右值引用 C++11中引入的特性,用于移动语义和完美转发
const值引用 没有实际意义,为了语法完整性而存在

下面针对上述例子,我们看一下foo函数绑定参数的情况。

如果只实现了void foo(MyString &str),而没有实现void fun(MyString &&str),则和之前一样foo函数的实参只能是非const左值。

如果只实现了void foo(const MyString &str),而没有实现void fun(MyString &&str),则和之前一样foo函数的参数即可以是左值又可以是右值,因为const左值引用是万能绑定类型。

如果只实现了void foo(MyString &&str),而没有实现void fun(MyString &str),则foo函数的参数只能是非const右值。

强制移动语义std::move()

前文中我们通过右值引用给类增加移动构造函数和移动赋值操作符已经解决了函数返回类对象效率低下的问题。那么还有什么问题没有解决呢?

在C++98中的swap函数的实现形式如下,在该函数中我们可以看到整个函数中的变量a、b、c均为左值,无法直接使用前面移动语义。

1
2
3
4
5
6
7
template <class T> 
void swap ( T& a, T& b )
{
T c(a);
a=b;
b=c;
}

但是如果该函数中能够使用移动语义是非常合适的,仅是为了交换两个变量,却要反复申请和释放资源。按照前面的知识变量c不可能为非const右值引用,因为变量a为非const左值,非const右值引用不能绑定到任何左值。

在C++11的标准库中引入了std::move()函数来解决该问题,该函数的作用为将其参数转换为右值。在C++11中的swap函数就可以更改为了:

1
2
3
4
5
6
7
template <class T> 
void swap (T& a, T& b)
{
T c(std::move(a));
a=std::move(b);
b=std::move(c);
}

在使用了move语义以后,swap函数的效率会大大提升,我们更改main函数后测试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() { 
// move函数
MyString d("123");
MyString e("456");
swap(d, e);
return 1;
}

// 输出结果,通过输出结果可以看出对象交换是成功的
Constructor is called! this->_data: 38469648 // 对象d构造
Constructor is called! this->_data: 38469680 // 对象e构造
Move Constructor is called! src: 38469648 // swap函数中的对象c通过移动构造函数构造
Move Operator= is called! src: 38469680 // swap函数中的对象a通过移动赋值操作符赋值
Move Operator= is called! src: 38469648 // swap函数中的对象b通过移动赋值操作符赋值
DeConstructor is called! // swap函数中的对象c析构
DeConstructor is called! this->_data: 38469648 // 对象e析构
DeConstructor is called! this->_data: 38469680 // 对象d析构

右值引用和右值的关系

这个问题就有点绕了,需要开动思考一下右值引用和右值是啥含义了。读者会凭空的认为右值引用肯定是右值,其实不然。我们在之前的例子中添加如下代码,并将main函数进行修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void test_rvalue_rref(MyString &&str)
{
cout << "tmp object construct start" << endl;
MyString tmp = str;
cout << "tmp object construct finish" << endl;
}

int main() {
test_rvalue_rref(foo());
return 1;
}

// 输出结果
Constructor is called! this->_data: 28913680
Move Constructor is called! src: 28913680
DeConstructor is called!
tmp object construct start
Copy Constructor is called! src: 28913680 dst: 28913712 // 可以看到这里调用的是复制构造函数而不是移动构造函数
tmp object construct finish
DeConstructor is called! this->_data: 28913712
DeConstructor is called! this->_data: 28913680

我想程序运行的结果肯定跟大多数人想到的不一样,“Are you kidding me?不是应该调用移动构造函数吗?为什么调用了复制构造函数?”。关于右值引用和左右值之间的规则是:

如果右值引用有名字则为左值,如果右值引用没有名字则为右值。

通过规则我们可以发现,在我们的例子中右值引用str是有名字的,因此为左值,tmp的构造会调用复制构造函数。之所以会这样,是因为如果tmp构造的时候调用了移动构造函数,则调用完成后str的申请的内存自己已经不可用了,如果在该函数中该语句的后面在调用str变量会出现我们意想不到的问题。鉴于此,我们也就能够理解为什么有名字的右值引用是左值了。如果已经确定在tmp构造语句的后面不需要使用str变量了,可以使用std::move()函数将str变量从左值转换为右值,这样tmp变量的构造就可以使用移动构造函数了。

而如果我们调用的是MyString b = foo()语句,由于foo()函数返回的是临时对象没有名字属于右值,因此b的构造会调用移动构造函数。

该规则非常的重要,要想能够正确使用右值引用,该规则必须要掌握,否则写出来的代码会有一个大坑。

完美转发

前面已经介绍了本文的两大主题之一的移动语义,还剩下完美转发机制。完美转发机制通常用于库函数中,至少在我的工作中还是很少使用的。如果实在不想理解该问题,可以不用向下看了。在泛型编程中,经常会遇到的一个问题是怎样将一组参数原封不动的转发给另外一个函数。这里的原封不动是指,如果函数是左值,那么转发给的那个函数也要接收一个左值;如果参数是右值,那么转发给的函数也要接收一个右值;如果参数是const的,转发给的函数也要接收一个const参数;如果参数是非const的,转发给的函数也要接收一个非const值。

该问题看上去非常简单,其实不然。看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

using namespace std;

void fun(int &) { cout << "lvalue ref" << endl; }
void fun(int &&) { cout << "rvalue ref" << endl; }
void fun(const int &) { cout << "const lvalue ref" << endl; }
void fun(const int &&) { cout << "const rvalue ref" << endl; }

template<typename T>
void PerfectForward(T t) { fun(t); }

int main()
{
PerfectForward(10); // rvalue ref

int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref

const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref

return 0;
}

在上述例子中,我们想达到的目的是PerfectForward模板函数能够完美转发参数t到fun函数中。上述例子中的PerfectForward函数必然不能够达到此目的,因为PerfectForward函数的参数为左值类型,调用的fun函数也必然为void fun(int &)。且调用PerfectForward之前就产生了一次参数的复制操作,因此这样的转发只能称之为正确转发,而不是完美转发。要想达到完美转发,需要做到像转发函数不存在一样的效率。

因此,我们考虑将PerfectForward函数的参数更改为引用类型,因为引用类型不会有额外的开销。另外,还需要考虑转发函数PerfectForward是否可以接收引用类型。如果转发函数PerfectForward仅能接收左值引用或右值引用的一种,那么也无法实现完美转发。

我们考虑使用const T &t类型的参数,因为我们在前文中提到过,const左值引用类型可以绑定到任何类型。但是这样目标函数就不一定能接收const左值引用类型的参数了。const左值引用属于左值,非const左值引用和非const右值引用是无法绑定到const左值的。

如果将参数t更改为非const右值引用、const右值也是不可以实现完美转发的。

在C++11中为了能够解决完美转发问题,引入了更为复杂的规则:引用折叠规则和特殊模板参数推导规则。

引用折叠推导规则

为了能够理解清楚引用折叠规则,还是通过以下例子来学习。

1
2
3
4
5
6
7
8
9
10
typedef int& TR;

int main()
{
int a = 1;
int &b = a;
int & &c = a; // 编译器报错,不可以对引用再显示添加引用
TR &d = a; // 通过typedef定义的类型隐式添加引用是可以的
return 1;
}

在C++中,不可以在程序中对引用再显示添加引用类型,对于int & &c的声明变量方式,编译器会提示错误。但是如果在上下文中(包括使用模板实例化、typedef、auto类型推断等)出现了对引用类型再添加引用的情况,编译器是可以编译通过的。具体的引用折叠规则如下,可以看出一旦引用中定义了左值类型,折叠规则总是将其折叠为左值引用。这就是引用折叠规则的全部内容了。另外折叠规则跟变量的const特性是没有关系的。

1
2
3
4
A& & => A&
A& && => A&
A&& & => A&
A&& && => A&&

特殊模板参数推导规则

下面我们再来学习特殊模板参数推导规则,考虑下面的模板函数,模板函数接收一个右值引用作为模板参数。

1
2
template<typename T>
void foo(T&&);

说白点,特殊模板参数推导规则其实就是引用折叠规则在模板参数为右值引用时模板情况下的应用,是引用折叠规则的一种情况。我们结合上文中的引用折叠规则,

  1. 如果foo的实参是上文中的A类型的左值时,T的类型就为A&。根据引用折叠规则,最后foo的参数类型为A&。
  2. 如果foo的实参是上文中的A类型的右值时,T的类型就为A&&。根据引用折叠规则,最后foo的参数类型为A&&。

解决完美转发问题

我们已经学习了模板参数为右值引用时的特殊模板参数推导规则,那么我们利用刚学习的知识来解决本文中待解决的完美转发的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

using namespace std;

void fun(int &) { cout << "lvalue ref" << endl; }
void fun(int &&) { cout << "rvalue ref" << endl; }
void fun(const int &) { cout << "const lvalue ref" << endl; }
void fun(const int &&) { cout << "const rvalue ref" << endl; }

//template<typename T>
//void PerfectForward(T t) { fun(t); }

// 利用引用折叠规则代替了原有的不完美转发机制
template<typename T>
void PerfectForward(T &&t) { fun(static_cast<T &&>(t)); }

int main()
{
PerfectForward(10); // rvalue ref,折叠后t类型仍然为T &&

int a;
PerfectForward(a); // lvalue ref,折叠后t类型为T &
PerfectForward(std::move(a)); // rvalue ref,折叠后t类型为T &&

const int b = 8;
PerfectForward(b); // const lvalue ref,折叠后t类型为const T &
PerfectForward(std::move(b)); // const rvalue ref,折叠后t类型为const T &&

return 0;
}

例子中已经对完美转发的各种情况进行了说明,这里需要对PerfectForward模板函数中的static_cast进行说明。static_cast仅是对传递右值时起作用。我们看一下当参数为右值时的情况,这里的右值包括了const右值和非const右值。

1
2
3
4
5
6
7
// 参数为右值,引用折叠规则引用前
template<int && &&T>
void PerfectForward(int && &&t) { fun(static_cast<int && &&>(t)); }

// 引用折叠规则应用后
template<int &&T>
void PerfectForward(int &&t) { fun(static_cast<int &&>(t)); }

可能读者仍然没有发现上述例子中的问题,“不用static_cast进行强制类型转换不是也可以吗?”。别忘记前文中仍然提到一个右值引用和右值之间关系的规则,如果右值引用有名字则为左值,如果右值引用没有名字则为右值。。这里的变量t虽然为右值引用,但是是左值。如果我们想继续向fun函数中传递右值,就需要使用static_cast进行强制类型转换了。

其实在C++11中已经为我们封装了std::forward函数来替代我们上文中使用的static_cast类型转换,该例子中使用std::forward函数的版本变为了:

1
2
template<typename T>
void PerfectForward(T &&t) { fun(std::forward<T>(t)); }

对于上文中std::move函数的实现也是使用了引用折叠规则,实现方式跟std::forward一致。

引用

  1. 《深入理解C++11-C++11新特性解析与应用》
  2. C++11 标准新特性: 右值引用与转移语义
  3. 如何评价 C++11 的右值引用(Rvalue reference)特性?
  4. C++11 完美转发
  5. C++ Rvalue References Explained
  6. 详解C++右值引用 (对C++ Rvalue References Explained的翻译)

最近粗读了一遍《大型网站技术架构-核心原理与案例分析》,并对其中的内容通过思维导图的形式进行了整理。本书的所讲解的内容均为大型网站中涉及到的问题及相关技术,但并未展开深入讨论相关技术的解决办法,非常适合入门。下面我将我的思维导图以图片的形式贴出来,并提供XMind编辑的.xmid格式的文件。

下载

大型网站技术架构读书笔记

0%