Chinaunix论坛“Linux/UNIX系统编程,系统程序员成长的基石?”话题讨论的回复

以下是我参与Chinaunix论坛关于“Linux/UNIX系统编程,系统程序员成长的基石?”(http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=4118545&fromuid=28864581)话题讨论的回复,整理出来发到这里,给需要的人做个参考:

(1) 您在Linux/UNIX系统编程行业的经历及经验
我是2006年研究生一年级时开始接触Linux系统编程的,当时是因为我实习的公司用到Linux。后来自己在业余时间看了一些系统编程方面的书,还实现了一个简单的网络协议。通过研究生这两年的经历,算是对Linux系统编程有了一个入门。2008年研究生毕业后,并没有从事Linux/UNIX系统编程的相关工作,而是做手机MMI程序的开发。2010年来到现在这家公司,才又重新开始Linux/UNIX系统编程的工作,目前我主要在Solaris/Linux系统上从事通信网关和服务器程序的开发。通过这些年的经历,我觉得从事Linux/UNIX系统编程最大的益处就是可以获得很多计算机底层系统的知识。举几个例子来说:通常我们都使用gcc编译器编译程序,我们需要了解一些常用编译选项的含义,因为有可能一个编译选项就会导致程序不按你的想法运行;程序不可能没有bug,也许我们需要gdb帮助我们详细了解程序的内存布局,去找到原因;有时程序在一个处理器上运行的好好的,而在另一个上面就不行,此时我们可能又要去了解处理器结构和汇编语言。总之,作为一个底层软件工程师,需要不断地学习计算机最底层最核心的技术,这是件很快乐的事情。

(2) Linux/UNIX系统编程职业生涯的发展探讨
最近几年,随着移动互联网的兴起,前端技术越来越受到大家的重视,而底层技术似乎现在的关注度并不高。我个人觉得Linux/UNIX系统编程知识是程序员的重要基础,无论你从事什么方面的开发,了解系统编程知识和原理,对你是有百益而无一害的,Linux/UNIX系统编程也永远不会被淘汰。所以,我们这些Linux/UNIX系统编程工程师需要做的就是不断学习,提高自己的技术能力。

(3)对于新入门的Linux/UNIX系统编程人员来说,您有什么话对他们说?
我并不是一个资深的工程师,以下是我个人的一点经验和体会,希望给大家一点启示吧:
首先要调整好心态,现在是个浮躁的社会,所以要尽量着让自己静下心来,踏踏实实地去学习技术。
第二就是多实践,经典的技术书籍很多,可是光是阅读,效果并不好。要试着自己写一些代码,这样会加深印象。
第三就是多读好的开源代码。现在github上有很多的优秀代码值得我们学习。拿著名的NoSQL数据库Redis来说,看完Redis的main函数,我们就可以了解到如何创建一个deamon进程了,此外我们也会从中学到如何写个好的高并发服务器程序,如何访问文件系统等等。

以上是我的一孔之见,希望能给大家一点帮助吧。

libumem使用和源码分析系列文章(一)

libumem是2001年由SUN的工程师Jeff Bonwick和Jonathan Adams开发的一个对应用程序进行内存管理的动态链接库。简单地讲,libumem通过提供一组替换C标准库的内存分配函数(malloc,free,realloc等等),从而可以使用户更容易发现和解决关于内存管理的bug。从Solaris 9(Update 3)开始,libumem作为Solaris操作系统的一个标准库提供给应用程序使用。现在libumem已经被移植到其它的平台上,这个网站(https://labs.omniti.com/labs/portableumem)不仅介绍了关于libumem的移植,并且还提供了源代码。从今天起,我打算写一组系列文章,通过对libumem的源代码分析,介绍如何使用libumem以及它的工作原理。

首先,我介绍一下如何使用libumem。这里有两个方法:
(1)通过使用LD_PRELOAD环境变量:
LD_PRELOAD=libumem.so;export LD_PRELOAD
这样就使用户程序使用的是libumem中提供的内存管理函数,而不是C标准库的函数。关于LD_PRELOAD,感兴趣的读者可以参考stackoverflow上的这篇帖子:http://stackoverflow.com/questions/426230/what-is-the-ld-preload-trick
(2)在链接程序时加上 -lumem选项。

当应用程序运行后,我们可通过以下简单的Dtrace脚本去检查程序是否使用的是libumem:

#!/usr/sbin/dtrace -qs

pid$1::malloc:entry
{
    trace(probemod);
}

这个脚本会输出使用哪个模块的malloc函数,如果输出的模块名是类似于“libumem.so.1”,那么就证明程序已经使用libumem了,否则就需要检查一下,看看问题在哪里。

2013年终总结

2013年转眼就要走完了,这是我第一次真正地感觉到时间流逝如此之快。回想这一年,似乎又是碌碌无为,什么也没做。不管怎么样,还是写一篇文章,纪念一下吧。

这一年生活上过得像个机器人:周一至周五上班,周六上午去超市买东西,下午看书或者运动,周日上午打扫卫生,下午休息。每一周几乎都是这样的日子,千篇一律。

工作上表现中规中矩,所有的项目都按时完成了,也没出什么问题,就不细说了。

英语学习也断断续续地坚持下来了,虽然自己并没有觉得有什么质的飞跃,还是会继续坚持下去的。因为英语水平已经到谷底了,坚持下去肯定只会一天比一天好吧。

技术方面上半年主要学习Lua,也就刚入门。下半年开始学习Golang,目前能写简单的程序了,但是要达到自己C语言的水平,还需多下功夫。另外最近一个月也开始看Dtrace,目前已经利用Dtrace解决了两个工作中的问题了,这也给了自己一点小小的成就感和坚持下去的信心。

展望2014年,还是希望自己能够在技术方面能更上一层楼吧,同时自己也已经30岁了,古人云:“三十而立”,自己也在考虑新的职业发展方向。好了,就写到这里吧。

使用Dtrace检查recv()的返回值

这周在新的产品版本上线后,发现监控日志总是会报recv()返回error,并且errno是131(ECONNRESET)。查了一下man手册,发现并没有说recv()会返回ECONNRESET,于是自己便打算一探究竟。想到最近正在学习Dtrace,于是便写了下面这个简单脚本(check_recv.d):

#!/usr/sbin/dtrace -qs

syscall::recv:return
/(int)arg0 <=0 && pid == $1/
{
    printf("recv return: tid=%d, arg0=%d, errno=%d\n", tid, arg0, errno);
}

第四行是触发探针条件:当recv返回0或者-1并且进程号等于输入监控的进程号。
第六行是输出:线程ID,recv()返回值,errno。
使用方法 :check_recv.d 19771(监控进程号)
通过运行脚本,我发现其实recv()返回的是0,而errno也是0,那么为什么监控日志会输出errno是131呢?我又查了一下这个版本新加的代码,发现了下面的逻辑:

if (recv() <= 0)
{
    log(errno)
}

原来在recv()返回0时,也会输出errno。而在recv()返回0时,是不会更新errno的值,只有在recv()返回-1时,才会更新errno的值。所以现在监控日志里的errno其实是以前某个系统调用错误时设置的errno。所以代码应该改为:

if (recv() < 0)
{
    log(errno)
}

问题解决!

客户端—服务器通信模型浅析

客户端—服务器(Client-Server)是我们平时最常见的通信方式,本文就对这一通信模型做个简单介绍。

(一)TCP连接方式:短连接方式?长连接方式

目前,多数的客户端—服务器选择TCP做为传输层协议,也有少数选择UDP或SCTP协议的。而TCP连接有两种工作方式:短连接方式(Short-Live Connection)和长连接方式(Long-Live Connection)。

(1)短连接方式:

当客户端有请求时,会建立一个TCP连接,接收到服务器响应后,就断开连接。下次有请求时,再建立连接,收到响应后,再断开。如此循环。这种方式主要有两个缺点:

a)建立TCP连接需要3次“握手”,拆除TCP连接需要4次“挥手”,这就需要7个数据包。如果请求和响应各占1个数据包,那么一次短连接的交互过程,有效的传输仅占2/9,这个利用率太低了。

b)主动断开TCP连接的一端,TCP状态机会进入TIME_WAIT状态。如果频繁地使用短连接方式,就有可能使客户端的机器产生大量的处于TIME_WAIT状态TCP连接(UNIX系统下,可以使用netstat命令来查看)。

(2)长连接方式:

客户端和服务器建立TCP连接后,会一直使用这条连接进行数据交互,直到没有数据传输或异常断开。在空闲期间,通常会使用心跳数据包(Keep-Alive)保持链路不断开。目前长连接方式应用范围比较广泛。

(二)消息交互方式

(1)一个请求,一个响应

这种消息交互方式如下图所示:


当客户端发出请求后,程序就阻塞在那里,直到收到服务器的响应或者超时。很多对数据库的访问方式都是这样的(像Redis的C程序客户端hiredis)。

(2)多个请求,多个响应

这种消息交互方式如下图所示:


这种方式下,客户端一次可以发送多个请求,而每个请求会带有一个消息标示(Message ID)。这样在客户端收到响应后,就可以根据响应的消息标示,和请求对应起来。

如何理解CPU利用率(译文)

以下这段文字翻译自技术大牛Brendan Gregg和Jim Mauro 2011年的著作:《DTrace: Dynamic Tracing in Oracle Solaris, Mac OS X and FreeBSD》第三章《System View》里关于CPU利用率的一个介绍(56页)。个人觉得写得很清晰,就把它翻译出来,希望能让更多人对CPU利用率有正确的理解。翻译有错之处,还希望大家批评指正。

在信息技术领域,CPU利用率作为一个重要能力度量指标已经有了很多年的历史了,它可以通过很多传统的“stat”工具得到(像Solaris的vmstat(1M)命令,等等)。随着处理器技术的演进,CPU利用率作为一个度量指标的实际意义和满意度正在减弱。拥有多个处理器的系统,拥有多核的处理器,拥有多个线程的处理器核,拥有多个执行单元(整数,浮点数)并允许多个线程并发地执行指令的处理器核等等这些因素,都会使我们对CPU利用率真正含义产生曲解,而不管CPU运行何种操作系统。

一个具体的例子是内存总线访问。当等待内存总线完成数据传输时,CPU的“装载”和“存储”指令也许会使CPU停下来。而这些停下来时耗费时钟周期发生在执行一条CPU指令期间,所以会被算作利用率,虽然可能和人们期待的不一样(虽然在使用却是在等待!)。

应用程序的并行化程度也是一个影响因素;一个单线程程序可以让一个CPU的利用率达到百分之百,却让其它CPU处于空闲状态。在一个拥有多个CPU的系统上,这种情况将使统计工具显示很低的系统级别的CPU利用率,而这很可能使你不会去深入了解CPU状态。一个充满竞争锁的多线程程序也许会使多个CPU的用率达到百分之百,而其中很多线程都在竞争锁,而不是真正地处理它们应该完成的任务。因此仅仅通过CPU利用率,并不足以让我们确定CPU本身是不是真正的性能问题。

清楚地了解CPU正在做什么(处理指令或是等待内存访问完成,正在执行用户应用程序还是操作系统内核程序)对我们将是很有益的。

“北漂”小记

2008年1月,我的学生时代画上了句号。离开校园,我成了“北漂”大军中的一员。

进入社会,第一个面临的事情就是租房。由于以前没有租房经验,所以第一次我就租了个一楼北向的一间卧室。开始有暖气还好,屋里不是很冷。后来等停了供暖,才感觉不妙。因为是一楼,还是朝北的,所以一天根本进不了多少阳光,屋里又潮湿又阴冷。没过多长时间,我就生了病。所以住了不到两个月,我就搬家了。

我的第二个住处搬到了南沙滩,这里介于北京四环和五环之间。据说在北京的中轴线上,风水很好。我在这里住了两年,也的确住的很舒服。这里离我的母校科技大学很近,夏天的时候吃完晚饭,我经常一个人走回校园,漫步在操场上,回忆着学生时代的往事。冬天的时候,我一边听着袁阔成先生的<<三国演义>>评书,一边在社区里散步。这些情景至今我还历历在目,对我来说是很美好的回忆。

2010年我换了工作,新公司的办公地点在西单。每天下班后,我就穿梭在西单附近的小胡同里,寻找晚饭的落脚点。现在回想起来,印象比较深的有一家清真饭馆,他们家做的拉条子味道不错。老板可能是青海人,因为餐厅的电视总是锁定在青海卫视频道。我经常是烤着火炉,喝着茶水,吃着拉条子,看着电视。这个场景现在回忆起来也是别有一番情调。

2011年2月18日,我和女友正式登记结婚了。我们在北三环租了一间一居室的房子。尽管房子不是自己的,我们收入也不高,但是漂泊了这么长时间,我终于有了家的感觉。每天下班回来,两个人围在一起吃晚饭的感觉很是让我温馨,也很让我满足。

一眨眼,我成为“北漂”已经快5年了。尽管每天生活有太多的不如意,我还是尝试着让自己内心平和地去接受发生的一切。因为无论发生什么,生活总要继续,我还要向前走。就像人们常说的:做好自己的份内事,然后去等待,看看会发生什么……

unixODBC使用经验分享

unixODBC(http://www.unixodbc.org)和iODBC(http://www.iodbc.org)是UNIX系统上两个开源的ODBC实现。关于二者的比较,有兴趣的朋友可以参考stackoverflow上的这篇帖子。我没有用过iODBC,所以不好评论孰优孰劣。但是从stackoverflow上关于两个项目的标签统计来看,应该是unixODBC用的更广泛一些。在距离上一版本发布两年以后,今年10月,unixODBC发布了2.3.2版本,基本上就是修正了一些bugs(其中有3个bug是我发现并提出修改意见的),没有新的feature引入。我从2011年开始在项目中使用unixODBC,2012年中旬项目正式上线,到现在已经运行了1年半时间。总的来说unixODBC还是很稳定的,基本连个错误都没有。在这篇文章中,我把我使用unixODBC的一点经验分享出来,希望能给需要的朋友一些帮助。

(1)源码,编译和安装
在unixODBC的网站上提供了最新版本的源码下载,如果想下载之前的发布版本,可以访问这个网站:ftp://ftp.unixodbc.org/pub/unixODBC/。个人感觉,unixODBC的代码风格还是很清晰的,前后也比较一致(unixODBC里面也包含了一些其它开源的代码,像libltdl,而这些开源代码的风格和unixODBC的风格是不一致的)。只是有些函数有些太长了(像SQLConnect函数就有600多行),似乎不符合KISS(Keep It Small and Simple)原则。
编译unixODBC很简单:

./configure;
make;
make install

我的很多同事都说在Solaris环境下编译源代码是很痛苦的事情,但是编译unixODBC却总是一气呵成,连个错误都没有。
此外,由于默认编译unixODBC是优化的,所以我在实验室里使用时一般都是禁止优化的(./configure CFLAGS=’-g -O0 ‘),这样debug时也比较方便,不会出现代码和函数对不上的问题。只有到了生产环境,才把优化打开。

(2)使用连接池
从2.0.0版本起,uinixODBC引入了连接池。unixODBC的项目负责人Nick Gorham在这篇文章里详细介绍了连接池。引入连接池,可以使对数据库连接的使用更高效。下面我就对连接池的原理做个分析:
首先,uinixODBC引入两个全局变量:

/*
 * connection pooling stuff
 */

CPOOL *pool_head = NULL;
int pooling_enabled = 0;

其中pool_head指向连接池链表的首节点,而pooling_enabled则表示是否使用连接池。
其次,在SQLDisconnect函数中,可以看到:

/*
 * is it a pooled connection, or can it go back 
 */

if ( connection -> pooled_connection )
{
    __clean_stmt_from_dbc( connection );
    __clean_desc_from_dbc( connection );

    return_to_pool( connection );

    ......

    return function_return( SQL_HANDLE_DBC, connection, SQL_SUCCESS );
}
else if ( pooling_enabled && connection -> pooling_timeout > 0 ) 
{
    __clean_stmt_from_dbc( connection );
    __clean_desc_from_dbc( connection );

    return_to_pool( connection );

    ......

    return function_return( SQL_HANDLE_DBC, connection, SQL_SUCCESS );
}

如果使用了连接池的话,当断开连接时,连接是回到了连接池里,也就是return_to_pool这个函数。
最后,在SQLConnect函数中,可以看到:

if ( pooling_enabled && search_for_pool( connection, 
                                            server_name, name_length1,
                                            user_name, name_length2,
                                            authentication, name_length3,
                                            NULL, 0 ))
{
    ret_from_connect = SQL_SUCCESS;

    .....

    connection -> state = STATE_C4;

    return function_return( SQL_HANDLE_DBC, connection, ret_from_connect );
}

如果连接数据库时可以在连接池中找到可用的连接(search_for_pool),函数就直接返回,不用再做连接的操作了。

(3)在日志中打印线程
unixODBC的日志输出是通过dm_log_write函数:

if ( !log_info.program_name )
{
    uo_fprintf( fp, "[ODBC][%s]%s[%s][%d]%s\n", __get_pid((SQLCHAR*) tmp ), 
            tstamp_str,
            function_name, line, message );
}
else
{
    uo_fprintf( fp, "[%s][%s]%s[%s][%d]%s\n", log_info.program_name,
        __get_pid((SQLCHAR*) tmp ), 
        tstamp_str,
        function_name, line, message );
}

可以看到,日志输出包含进程名,时间戳,函数名,行号,和消息。但是现在的程序基本是多线程的,所以当有多个线程同时访问数据库时,从日志就很难区分开哪个线程到底做了什么,因此通常我会在日志中增加对线程的打印。因为我们的程序运行在Solaris系统,而在Solaris上,pthread_t类型是个整数,标示当前的线程号,所以下面的改动就可以增加对线程号的输出:

if ( !log_info.program_name )
{
    uo_fprintf( fp, "[ODBC][%s][%d]%s[%s][%d]%s\n", __get_pid((SQLCHAR*) tmp ), pthread_self(),
            tstamp_str,
            function_name, line, message );
}
else
{
    uo_fprintf( fp, "[%s][%s][%d]%s[%s][%d]%s\n", log_info.program_name,
        __get_pid((SQLCHAR*) tmp ), 
        pthread_self(),
        tstamp_str,
        function_name, line, message );
}

由于pthread_t类型在不同的系统定义不一样,所以如果同一份unixODBC代码需要运行在不同系统上,就需要实现定制化:

#ifdef __linux__
......
#endif

#ifdef __sun
......
#endif

关于pthread_t的打印,可以参考stackoverflow上的这篇帖子
以上就是我使用unixODBC的一点经验分享,希望能给需要的朋友一点帮助。如果大家有其它好的经验,也希望能分享出来。

Solaris操作系统网络编程经验分享

自从2010年我开始在Solaris操作系统进行应用程序开发算起,到现在已经超过3年的时间了。在这里我想把在Solaris操作系统上做网络编程开发的一些特有的经验分享出来,希望给别人有所帮助。

经验1:链接动态库选项:-lresolv -lnsl -lsocket。
也许在其它的UNIX系统上,一个”-lsocket”链接选项就能解决所有问题。但是在Solaris系统上,要链接这三个库。

经验2:不支持SO_SNDTIMEO和SO_RCVTIMEO socket选项。
在我用过的Solaris 9和Solaris 10上,这两个socket选项都不支持(我没用过Open Solaris,所以不确定Open Solaris是否支持)。所以尽管这两个宏定义在sys/socket.h,但是却是不起作用的。如果在程序中用到这两个选项,一定要注意这一点。我在使用MySQL,hiredis这些开源代码时都遇到过这个问题。

经验3:使用sctp_bindx之前,一定要先调用bind。
这个可能也是Solaris系统特有的了。我曾经遇到一个使用SCTP的应用程序,在其它系统上跑的好好的,到Solaris上就不行。后来查了一下man手册,才发现需要在调用sctp_bindx之前,要先调用bind。

经验4:shutdown()函数作用于一个listen socket时,会产生ENOTCONN错误
我曾经写过下面的程序:
第一个线程:

{  
    ......  
    FD_ZERO(&fd_sets);  
    FD_SET(sock_fd, &fd_sets);  

    ret_val = select(sock_fd + 1, &fd_sets, NULL, NULL, NULL);  

    if (ret_val > 0)  
    {  
         accept(sock_fd, NULL, NULL);  
         ......  
    }  
    else  
    {  
        ......  
    }  
}  

第二个线程:

{  
    ......  
    shutdown(sock_fd, SHUT_RD);  
    ......  
} 

其中第一个线程阻塞在select函数,sock_fd是一个listen socket。我本意是在第二个线程中调用shutdown函数,使第一个线程的select返回,结果却发现没有生效,后来才发现shutdown返回了一个ENOTCONN错误。感兴趣的朋友也可以参考这里

以上就是我在Solaris上进行网络程序开发的一点心得,希望分享出来给需要的朋友一点帮助。如果大家有更好的经验,也欢迎分享出来。