Unix/Linux下C/C++开发技术概览
来源:互联网 发布:淘宝品牌授权书 编辑:程序博客网 时间:2024/05/16 13:40
转自:http://blog.sina.com.cn/s/blog_50b8427f0101nc3l.html
1. 平台差异简介
Windows和Unix是当前两大主流操作系统平台,基于C/C++的开发人员经常会面临这两个平台之间的移植的问题。Unix作为一个开发式的系统,其下有出现了很多个分支,包括Sun的Solaris、IBM的AIX、HP Unix、SCO Unix、Free BSD、苹果的MAC OS以及开源的Linux等。对于这些Unix的分支操作系统,其实现又有很大的差别,因此开发人员又要针对这些不同的系统进行移植。本文的目的就是介绍一下Windows平台和Unix平台之间的差别,并简单介绍一下不同Unix分支操作系统之间的差别,在移植开发过程中的一些注意事项,同时简要介绍一下Unix下开发的一般流程和常用的开发调试工具。
关于平台之间的差异,主要是Windows平台和Unix平台之间的差异,这里着重介绍一下这两个平台在C/C++开发中存在的差异,其间会穿插介绍一些Unix不同分支之间的差异。
1.1语言特性的差异
1.1.1字节顺序的差异
十六进制表示
0x00004E20
Windows内存表示
20 4E 00 00
Unix内存表示
00 00 4E 20
如表中所示,Windows中存储方式和该整数的16进制表示是相反,是一种低位在前高位在后的存储顺序。而Unix下的存储顺序和正常的16进制表示的顺序相同,称为高位在前低位在后的顺序。这种差异带来的问题,主要体现在以下几个方面:
Ø
当Windows和Unix之间发生网络数据传输,传输一个整型数据(如一个数据包的长度)的时候,如果不经处理直接把内存中的数据传输过去,那么在对方看来完全是另一个数据,这样就会造成问题。如Windows下面发送过去一个20000(0x00004E20),在Unix下面收到的数据就会被理解成541982720(0x204E0000),这简直是天壤之别。
Ø
跟网络传输类似,如果在Windows下面把某个整数写到了文件中,然后在Unix下面打开这个文件读取该数据,就会出现跟上面类似的问题。
1.
2.
3.
跟这个问题类似,32位系统和64位系统的差异也会出现这样的问题,解决方法跟这个问题的解决方法相同。在32位系统和64位系统中,长整型(long)分别用32位和64位表示,这样,在不同系统之间交互的时候必然会出现整型数据表示方式不同的问题。目前大多数Windows系统都是32位的系统,而Unix中很多都是64位的,尤其是大型的服务器,所以这个问题必须引起重视。
1.1.2变量的作用域差异
在不同的系统下,由于编译器的不同,对变量作用域的实现机制也有所不同,这里以Windows下的VC和Solaris下的CC这两个编译器为例做一个简单的比较说明。
在C++的开发过程中,我们经常会有这样的用法:
这是一种最常用的for循环的用法,因为其中i主要使用来控制循环,所以一般没有必要拿出来单独进行声明,只是放在for语句中一起声明。这里i、j等简单的变量就成了我们常用的变量,一般不按照编程规范那样为他们命名。就是这种声明方法,在Windows下和Solaris下有了不同的理解,i的作用域不同。我们先把作用域进行划分,如下:
II
I
我们划分出I和II两个作用域,其中作用域II包含在作用域I当中。在Windows下,变量i的作用域是I的整个范围,而Solaris下的i的作用域只是II的范围。其实标准的C++语法应该是Solaris的做法,但是微软在实现的时候没有按照这个标准实现,这就引发了我们讨论的这个问题。由于这个差异,就引发了一些微妙而隐蔽的问题。先看一下下面两端代码。
A:
B:
代码A在Windows下面可以正常编译,而在Solaris下面确编不过去,提示第二个for循环中变量i没有定义。相反代码B在Solaris下可以正常编译,而在Windows下面编不过去,提示第二个for循环中变量i重复定义。
在通常的情况下,我们会按照B的方法书写代码,而在Windows编译是出现错误,然后改成A的那种形式。这样,在Windows下就没有问题了,程序也可以编译过去了,但是到Solaris下时,有会出现问题,这是就不得不把i的声明拿到所有for循环的外面。当i的声明拿到for循环的外面时,真正的问题来了。首先提示一下,这样的一段代码是没有问题的:
C:
int i = 0;
if(cond)
{
}
I
III
II
在上述代码,为了分析方便,我们把整段代码分成I、II和III三个作用域。其中作用域II就是整个if语句,实现的相当于一个strcpy函数的功能。II中的内容就好是我们上面说的后期维护中加入的,当然,实际情况并不像我们例子中这么明前,i的声明可能离我们的if语句很远,所以加入这段代码是不知道上面是否声明了i变量。而且,这段代码编译的时候也不回出错,不管是Windows还是Solaris(单独的一段II中的代码在Solaris下面编不过去)。在Windows下面,这段代码可以正常的运行,不回出现任何问题,因为II中的代码完全是根据Windows下的习惯编写的。但是在Solaris下面,这段代码就会出现内存越界的错误,虽然编译可以正常通过,但是实现的却不是程序员预期的目的。在执行memcpy的时候,那个i其实是外层声明的那个i,值是20,而str2和str1的大小之后10,所以就发生了读写内存越界。而程序员预想的,这个i是for循环算出来的str1字符串的长度,应该是5。
要解决这类问题,就得加强编程规范,杜绝这种错误代码的生成。从开始的时候就要意识到可能产生的问题,从而避免问题的发生。
1.1.3全局对象的初始化
1.1.4 语法检查的差异
不同操作系统下面有不同的编译器的实现,不同的编译器对语法要求的程度不同。在Windows下可以正常编译的代码,在Unix下就可能出现语法错误。1.1.2中就是一个典型的例子。另外,还有一些其他方面的语法检查的差异。
C是一中很灵活的语言,语法很自由,但是不同的平台下这中自由的程度也不同。Windows VC、Solaris CC和Linux gcc实现的都不错,但是有些其他的系统实现的就不是这么灵活,很多写法在他们下面都行不通。具体的记不太清了,在AIX和SCO Unix下面碰到很多这种情况。所以只能在移植的过程中逐渐的发现和改正。但是只要保证采用标准的书写规范,应该可以更少的产生这种错误。
有这样一段代码:
这是在大多数平台下面很好的一种习惯,可以避免哪种把“==”写成“=”的错误,在编译期间就能发现。但是在SCO Unix下面,这种写法就会引发编译器的一个警告。这个例子能简单的说明一下不同编译器之间存在的差别。
1.2 操作系统特性的差异
不同的操作系统中都存在一些系统的限制,如打开文件句柄数的限制、Socket等待队列的限制、进程和线程堆栈大小的限制等,因此在开发的过程中,必须考虑到这些限制因素对程序的影响。当然,有些限制参数可以适当的调整,这就需要在发布程序的时候加以声明。另外,操作系统的容错性也对程序有影响。下面分别进行讨论。
1.2.1 文件描述符的限制
文件描述符最初是Unix下的一个概念,在Unix系统中,用文件描述符来表示文件、打开的socket连接等,跟Windows下HANDLE的概念类似。文件描述符是一种系统资源,系统对每个进程可以分配的文件描述符数量都有限制。以Solaris为例,默认情况下每个进程可以打开的文件描述符为1024个,系统的硬限制是8192(具体的值跟版本有关),也就是说可以调整到8192。在Unix系统下使用ulimit命令来获得系统的这些限制参数。一般情况下,这都是够用的,但是有一个例外,在32为的Solaris程序中,使用标准输入输出函数(stdio)进行文件的操作,最大的文件描述符不能超过256。比如说用fopen打开文件,出去系统占用的3个文件描述符(stdin、stdout和stderr),程序中只能再同时打开253个文件。如果使用open函数来打开文件,就没有这个限制,但是就不能够使用stdio中的那些函数进行操作了,是程序的通用性和灵活性有所降低。这是因为在stdio的FILE结构中,用一个unsigned char来表示文件描述符,所以只能表示0~255。
在网络程序的开发中,每一个网络连接也都占用一个文件描述符,如果程序打开了很多Socket连接(典型的例子就是使用了连接池技术),那么程序运行的时候可能用fopen打不开文件。
解决这个问题,可以采用一下几种方法:
1.
2.
3.
至于采用哪种方法或者是否考虑系统中处理这个问题,就要视具体的情况而定了,那些不受这个限制影响的程序,可以不考虑这个问题。
1.2.2 进程和线程的限制
一般的操作系统对每个进程和线程可以使用的资源数都有限制,比如一个进程可以创建的线程数,一个进程可以打开的文件描述符的数量,进程和线程栈大小的限制和默认值等。
针对这些问题,首先要分析和考虑你的系统是一个什么样的规模,会不会收到这些限制的影响,如果需求大于系统的限制,可以通过适当的调整系统参数来解决,如果还不能解决,就得考虑采用多进程的方式来解决。
对于进程和线程的栈空间大小的限制,主要是线程栈空间的问题。一般的系统都有默认的线程栈空间大小,而且不同超作系统的默认值可能不同。在通常情况下,这些对程序没有影响,但是当程序的层次结构比较复杂,使用了过多的本地变量,这个限制可能就会对程序产生影响,导致栈空间溢出,这是一个比较严重的问题。不能通过调整系统参数来解决这个问题,但是可以通过相应的函数,在程序里面指定创建线程的栈空间的大小。但是具体该调整的数值应该适可而止,而不是越大越好。因为线程的栈空间过大的时候,就会影响到可创建线程的数量,虽然远没有达到系统多线程数的限制,但却可能因为系统资源占用过多导致分配内存失败。
Linux的线程是通过进程实现的,实际上是假的线程。如果程序只在Linux下运行,就可以考虑直接使用多进程技术来代替多线程,因为在Linux下多线程并不能带来多线程相对于多进程的优势。
1.2.3 网络通信能力的限制
对于网络编程来说,性能是最主要的因素。系统为了提高网络通信的性能,提供了很多辅助的技术,其中等待队列就是其中之一。
对于程序来说,在一个时间点只能处理一个网络连接请求,而如果同时来了多个网络连接请求的话,就会有很多请求失败。为了解决这个问题,操作系统提供了等待队列技术,就是处理不上的连接请求先放到系统的等待队列中,这样就可以提高网络连接的成功率。等待队列的创建也需要消耗系统资源,因此系统对等待队列的大小都有限制,程序中可以通过函数设定等待队列的大小,但是不能超过系统的硬性限制。下面列出了几个操作系统的最大等待队列的大小:
操作系统
最大等待队列
Windows 2000 Server
200
Windows XP Home
5
Solaris E250
128
上表中只简单列出了几个操作系统的等待队列参数,其他系统暂位列出,如果有兴趣可以自己作个简单的程序测试一下。所以这个问题就跟具体的系统环境有关了,不过我们可以在系统连接池的基础上再做一些工作,采用连接缓冲池技术。就是接受到网络连接请求以后,提交给出去线程处理的之前,先放到一个缓冲池中。这样可以接受更多的连接请求等待处理,能一定程度的提高系统的连接成功率。不过,跟系统的等待队列不同,这是通过软件方式实现的等待队列,而系统提供的连接池是从操作系统级来解决问题的,更接近硬件层次,所以效率肯定会不同。面对这类问题,首先还是得以调整系统连接池的大小,然后再采用其他辅助手段。
1.2.4 容错性的影响
采用C/C++开发程序,缓冲区溢出的错误非常普遍,但是系统运行程序的时候,对待运行期出现的这些错误的处理能力都不相同。总的来说,Windows系统的容错性最强,尤其是Debug版的程序,系统都加入了一些保护机制,能够保证出现一些小的错误以后,程序仍能够正常运行。Unix平台下面要求的就严格一些,有些系统更是容不得一点沙子,有一点错误就会出现宕机现象,这些跟操作系统的内存分配机制有关。Windows平台的程序分配内存的时候,一般都会多分出一些字节用于对齐,如果缓冲区溢出的不是太多,就不回对内存中其他变量的值造成影响,因此程序也能够正常运行。但是这种保护机制会带来更多的系统开销。这就是Windows程序移植到Unix下面稳定性降低的主要原因之一,也是为什么Windows系统会消耗那么多系统资源的原因。
要解决这类问题,就要进行更严格的测试和代码检查。同时,借助相关的测试工具,找出系统中隐藏的潜在的问题,不能放过任何一个可能产生的错误,尤其是编译过程中发现的警告信息。当然,这些工作都应该再移植前做的很充分,在移植后更应该加大测试的力度。
1.3 图形用户界面
Windows
1.4 并发处理
并发处理包括多进程和多线程的概念,Windows和Unix的并发处理差别也比较大,但是基本上都能找到一组对应的函数来实现类似的功能。
在Windows下,创建进程和线程可以通过调用Windows的API来完成,或者通过调用MFC提供的并发处理类库来实现。在Unix下面创建进程通常使用fork函数,这跟Windows下面的多进程概念有所不同,相当于在当前位置给当前进程创建一个副本;而Windows下的创建进程大都是创建一个新的进程。Unix下的多线程操作,通过一组线程函数来完成,通常我们使用POSIX
在有些Unix系统下,没有实现线程库,如SCO Unix,系统只提供多进程的开发方式。但是,如果为了实现程序代码的统一性,我们可以采用第三方提供的线程库。这里有一个叫FSU-threads的线程库供我们选择。这个线程库中实现了POSIX中定义的线程函数,而且是开源的,可以支持SunOS 4.1.x, Solaris 2.x, SCO UNIX, FreeBSD, Linux等系统。除此之外,还有ZThreads线程库等。
在Windows的线程库中,实现了互斥(Mutex)、事件(Event)、信号量(Semaphore)等同步对象,用于实现线程之间的同步。在Unix下面,线程同步主要使用互斥(mutex)和条件变量(cond),其中条件变量可以实现事件和信号量的功能。另外,
进程
POSIX线程库
pthread_create
pthread_attr_init
pthread_attr_destroy
pthread_exit
pthread_join
pthread_setschedparam
pthread_getschedparam
pthread_sigmask
pthread_kill
pthread_self
pthead_mutex_init
pthread_mutexattr_init
pthread_mutex_lock
pthread_mutex_unlock
ptherad_mutex_destroy
pthread_cond_init
pthread_condattr_init
pthread_cond_wait
pthread_cond_signal
pthread_cond_boradcast
pthread_cond_destroy
pthread_cancel
Solaris本地线程库
thr_create
thr_exit
thr_join
thr_yield
thr_suspend
thr_continue
thr_setprio
thr_getprio
thr_sigsetmask
thr_kill
thr_self
thr_main
thr_mutex_init
thr_mutex_lock
thr_mutex_unlock
thr_mutex_destroy
thr_cond_init
thr_cond_wait
thr_cond_signal
thr_cond_boradcast
thr_cond_destroy
rwlock_init
rw_rdlock
rw_wrlock
rw_unlock
POSIX信号量
sem_init
sem_destroy
sem_wait
sem_trywait
sem_post
sem_getvalue
semctl
semget
semop
sema_init
sema_destroy
sema_p
sema_p_sig
sema_tryp
sema_v
1.5 网络通信
Socket(中文译名:套接字)最初在Unix上出现,并很快成为Unix上最流行的网络编程接口之一。后来,微软将它引入到Windows中并得到实现,于是从Windows 95、WinNT4开始,系统就内置了Winsock1.1,后来到了Windows98、Windows2000,它内置的Winsock DLL更新为Winsock2.2。
Windows下的Socket函数大体上和Unix下的Socket函数差不多,函数名称很参数用法都类似,只有一些细微的差别,某些参数的意义不同,而且对于Socket的属性控制也不太一样。Windows下面还对Socket函数进行了封装,有一系列相关类可用使用,简化网络编程的复杂性。Unix本身没有这些类库,但是我们也已经积累了很多这方面的经验和资源。我们有一组现成的类对Windows和Unix下的Socket函数进行了封装,上层只需要简单的调用即可,不用关心底层的差别。而且,这套类库也可以同时支持多种平台,可移植性非常好。
关于Socket开发就简单说这些,想深入了解的话请参考介绍Socket编程的一些相关资料。
2. 基本开发流程
在Unix下开发程序,不同于在Windows下开发,除了上面介绍的程序级的差别外,开发环境也有很大的差别。在Windows下面,大都使用集成开发环境进行开发,如MS Visual C++、Borland C++ Builder等。在Unix下面也有集成开发环境,如Sun
2.1 代码的编写
对于移植工程来讲,基本代码都在Windows下完成,只需要吧代码传到Unix下,然后在Unix下面组织源码目录即可。对于传到Unix代码进行编辑,可以使用Unix下的vi工具来完成,也可以通过UltraEdit以Ftp方式打开Unix下的文件进行编辑。vi是Unix下面最常用的一个文本编辑器,后面将会介绍vi的一些基本用法。
有一点值得注意,Windows下文本里面的回车符包含两个字符‘/n’和‘/r’,而Unix下的文本里面的回车符只包含一个字符‘/n’。这样,如果上传问文件的时候没有选择正确的方式,应该使用文本方式上传的使用了二进制方式,或者应该使用二进制方式上传的使用了文本方式,那么在unix下都会出现问题,打开的文本当中每一行的行尾就会出现一个‘^M’字符。可以通过vi的匹配替换功能(稍后会做介绍)或者重新按照正确的方式上传来解决。
2.2 编译
2.2.1 简单编译
对于简单的程序,如只有几个源文件,可以直接使用编译器进行编译,或者把几条编译命令写在一个脚本文件里面,通过执行脚本文件实现工程的编译和连接。比如只有一个hello.cpp文件的工程,可以通过如下命令编译:
CC –o hello hello.cpp
其中CC是编译器,不同的系统下面可能有不同的编译器。一般来说,大多数Unix系统下的C编译器都叫cc,而C++编译器叫CC。Linux下面带的C编译器为gcc,C++编译器为g++。-o参数用来指定输出的目标的名称,也就是编译后执行程序的名称。这种情况下编译和连接一步完成。
对于稍微负责一些的程序,包含多个源文件的,可以编写一个编译脚本,相当于windows下的批处理。如下:
工程中包含hello.cpp、func.cpp、other.cpp,我们可以用如下脚本来实现工程的编译。
多个文件情况下,把编译和连接分开执行,先逐个编译源文件,然后再进行链接,形成最终的可执行程序。参数-c就是声明只进行编译操作。
2.2.2 使用Makefile
当工程达到一定的规模的时候,2.2.1中的做法显然是不能满足要求的,如果非要那样做,将会带来很大的工作量,而且还非常容易出错。这是我们就要使用Makefile来帮助我们完成工程的编译工作。
Makefile文件相当于一个工程文件,文件中描述了工程中的源代码、额外需要的库文件及其路径、额外需要的头文件路径已经编译器类型、编译参数等。通过make命令来调入Makefile进行工程的编译。当执行make命令是,会在当前目录下搜索名称为“Makefile”或者“makefile”的文件,作为当前编译的工程文件,也可一指定其他的工程文件,如make –f MyMakefile。
一个简单的Makefile文件内容如下:
#Makefile for Linux(RedHat)
宏定义
BIN_NAME=demo
#编译器及编译参数
CC = gcc
CXX = g++
CXXFLAGS = -g
文件列表
SRCS=/
#目标文件列表,通过源代码列表生成
OBJS=${SRCS:.cpp=.o}
#依赖关系
#depends
all:${BIN_NAME}
.depends : ${SRCS}
依赖关系
-include .depends
#%.o:%.cpp
#
#
${BIN_NAME}:${OBJS}
clean:
在Makefile文件中,大体来说分为“宏定义”、“文件列表”和“依赖关系”三个部分。宏定义中定义Makefile中可以使用的一些宏,定义好一个宏以后,在Makefile的后面部分可以通过宏引用来获得宏的值。像编译器、编译参数等通常都会定义成宏,然后通过修改宏的值就可以修改编译器类型及编译参数。在更复杂的Makefile中,宏定义中还会包含依赖的库文件、需要增加的库文件路径及头文件路径、程序中预定义的宏等。
其实文件列表也是一个宏,只是考虑到他的重要性,所以单独作为一个部分来介绍。在后面的依赖关系中,就会用到这个宏。文件列表中定义了工程包含的所有源文件和这些源文件编译以后生成的目标文件。目标文件可以通过源文件列表生成。上面我们使用${SRCS:.cpp=.o}来生成目标文件列表,意思就是说把源文件中所有扩展名为“.cpp”换成“.o”。
依赖关系部分是Makefile文件的关键,其中定义了多个依赖关系。每一个依赖关系又包括一个依赖规则和一组执行的命令。依赖规则由目标和依赖对象构成,也就是说这个目标依赖于指定的依赖对象。当执行make的时候,如果发现目标不存在或者目标的依赖对象的更新时间比目标还新,就会执行依赖关系中的命令,来完成对目标的编译。Makefile中可以定义多个目标,每个目标对应一个依赖关系和一组命令。执行make的时候,可以执行要编译的目标。如make clean表示检查执行目标clean。all是一个默认的目标,即执行make不加参数的时候,就会默认为要检查执行all这个目标。Makefile中可以为all目标指定依赖对象和要执行的命令。Makefile中,也可以只指定依赖关系,不指定要执行的命令,这种情况大多用于依赖对象也是一个定义的目标。Make在检查依赖关系的时候,如果发现依赖对象也是一个目标,就会先去检查这个目标的依赖关系,看是否需要重新编译这个目标,然后才会执行本依赖关系下面的命令。
为了实现源文件的编译,我们在依赖关系中需要定义源文件和目标文件之间的依赖关系,然后执行程序和这些目标文件之间建立一个依赖关系。这样就可以实现整个工程的编译了。上面的这个Makefile中,包含了两种定义这种依赖关系的方法,一种是通过编译器的“-M”命令来形成依赖关系文件,然后把这个文件的内容include到当前的依赖关系中,另一种是通过匹配替换的做法,直接通过源文件和目标文件的映射关系形成依赖关系。红色字体部分和蓝色字体部分分别代表这两种做法。“-M”参数是gcc/g++编译器特有的,其他编译器一般没有这个参数,应该使用第二种做法。
在不同的系统中,Makefile的语法会有一些细微的差别,在进行多平台移植的时候应该注意这个问题,分别对不同的系统编写不同的Makefile。
2.2.3 automake和autoconf
Makefile
程序设计者只需要写一些预先定义好的宏
2.3 运行和调试
跟Windows不统,Unix下面的文件只有有可执行的权限就可以执行,不像Windows那样依赖于扩展名。所以Unix下的执行程序大都没有扩展名,任何文件只要给了一个执行权限,就可以直接运行。可以通过chmod +x来给一个文件增加可执行的权限。编译后形成的目标文件,通常都已经有了执行的权限,所以可以直接执行。
2.3.1 设置运行环境
对于简单的程序来说,不用设置什么运行环境,直接运行即可。对于负责的程序来讲,可能执行程序和需要的共享库文件不在同一个目录下,就需要设置一些运行环境了。设置运行环境主要就是设置共享库的路径。Unix下的共享库,相当于Windows下面的动态库(dll文件),是一种以.so为扩展名的文件。系统的共享库存放在/usr/lib下面,程序运行的时候会自动去那个目录下面寻找需要的共享库。对于用户自己开发的共享库,一般不能放在/usr/lib目录下面,而是放在用户的程序目录下面。一个
mainpath
mainpath/bin
mainpath/lib
这种情况下,就需要设置程序的运行环境,指定库文件目录。设置运行环境其实就是在运行程序前设置若干个环境变量,其中共享库的路径就是其中之一,也可一设置一些程序需要的其他的环境变量。Unix下面用LD_LIBRARY_PATH来表示用户共享库的路径。设置完环境变量,就可以运行程序了。
有一种更方便的方法,就是编写执行脚本。Unix下面的脚本相当于Windows下的批处理文件,就是执行一系列的命令。本质上来说,脚本文件就是一个文本文件,编辑完成后增加一个可执行权限即可。在Unix下面的终端窗口,都是基于外壳(shell)的。在UNIX中大家最常使用Bourne
一个典型的程序目录中,一般存在3个脚本:执行脚本(run.sh)、停止进程脚本(kill.sh)和设置环境脚本(setenv.sh)。脚本文件一般都放在程序的主目录下面,进入主目录可以直接通过这些脚本来执行程序。下面简单列出了这几个脚本的内容。
setenv.sh:
path=`pwd`
LD_LIBRARY_PATH=$path/lib:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
run.sh:
path=`pwd`
LD_LIBRARY_PATH=$path/lib:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
cd bin
./demo
cd ..
#!/usr/bin/sh
for sid in `ps -e |grep demo |awk {'print $1'}`
do
kill $sid
done
2.3.2 使用调试工具进行调试
程序的调试,是程序开发中最重要的一部分,通过调试来找到程序中的bug。Unix下面的程序调试工具有很多,包括IDE环境下的调试工具和字符界面的调试工具。这里我们主要介绍字符界面的调试工具。常用的调是工具是dbx,大部分Unix系统下都有。在linux下面,调试工具为gdb,功能跟dbx类似。
跟Windows类似,要以调试方式运行程序,就必须按照debug版来编译程序。Unix下面的编译一一般通过-g参数来声明使用debug方式进行编译,这样编译后的目标文件中就包含了源代码的信息。下面以gdb为例,介绍一下Unix下面程序调试的基本方法。
1)
在执行程序所在的目录下,执行gdb,以程序名称为参数即可进入gdb调试界面。如:gdb demo
2)
在gdb下,使用list命令可以查看主函数所在的文件的内容。默认情况下,一次显示10行内容,再次执行list可以显示后面的10行。
确定要在哪行上设置断点,然后用break命令进行断点设置。如:
break 10
break命令还可以指定要设置断点的文件(如果不是主函数所在的文件),如:
break func.cpp:10
除了在具体的行上设置断点外,还可以在某个函数上设置断点,只要把行号换成函数名即可。
clear命令用来清楚当前行的断点,delete命令可以删除所有设置的断点。
3)
设置完断点后,可以在gdb下通过run命令运行程序,如果程序需要什么参数,直接放在run命令后即可,如:
run –start
其中-start是程序允许需要的一个参数。
4)
当程序执行到断点的时候,就会定下来等待用户进行处理,这是可以通过一些命令来查看程序当前的运行状态,这些命令包括:
print
where
set variable
5)
在断点处处理完成后,可以继续往下执行程序,next命令用来执行一行代码,step命令用来跟踪到即将执行的函数中。continue命令可以继续运行当前程序,直到遇到下一个断点。
2.3.3 core dump
对于Unix开发人员来说,core dump是再熟悉不过的了。再Unix下面,core dump就是程序bug的最严重的体现。core dump又叫核心转储,当程序运行过程中发生异常,程序以外退出的时候,系统会把程序当前的内存状况存储在一个core文件中,就叫core dump。
既然core文件记录了程序运行时的情况,就可以通过core文件来分析错误的原因。使用gdb/dbx
值得注意的是,在linux下面有一个系统限制,就是core文件的大小。如果这个限制设成0,则程序发生异常以后不回产生core文件。如果想得到core文件,请使用ulimit –c命令来调整这个参数。ulimit –a命令可以查看到系统当前的限制参数情况。
3. 常用命令介绍
Unix下面的常用命令,跟Windows下的控制台命令有很大差别。Unix下的命令,可能是一个执行程序,或者一个可以执行的脚本,系统的命令一般都可以在/usr/bin/下面找到。另外需要说明的一点,Unix下面严格区分文件名的大小写,包括命令的大小写。下面简单介绍一下Unix系统下常用的几个命令。
3.1 常用命令列表
ls
cd
mkdir
rm
mv
cp
ps
man
find
grep
vi
tar
gzip
3.2 命令的组合
在Unix下面,可以把多个命令组合使用,可以把一个命令的执行结果作为另一个命令的输入。其中,最常跟别的命令组合使用的命令就是grep。在2.3.1中的那个kill.sh脚本中,我们就使用了这样一个组合命令“ps -e |grep demo |awk {'print $1'}”,用来列出当前系统中所有名称为demo的进程的进程号。这种用法是我们开发过程中最常用的方法。另外如果我们想知道我们的程序的网络情况,可以通过netstat命令和grep命令来组合使用,获得指定端口的状态情况,比如netstat –a | grep 43001,这个命令将列出当前系统中所有在端口43001上的连接情况。
通常的grep命令,只能查找当前目录下的文件,比如:grep
grep 'OPEN_MAX' `find /usr/include -name '*.h'`
这样可以查找所有在/usr/include及其子目录下的头文件,看是否包含“OPEN_MAX”。执行的时候,先执行find命令,形成一个文件列表,然后把这个文件列表作为grep的一个参数来执行grep命令。这是命令组合的另一种方式。
3.3 vi简介
vi是Unix下最常用的一个文本编辑器,小巧而且功能强大,一次我们把它单独拿出来做一下介绍。如果想了解更详细的信息,请执行man vi查看其联机帮助文档。
3.3.1 命令模式和编辑模式
vi的工作模式包括命令模式和编辑模式两种。命令模式下可以执行vi中定义的一些命令,这些命令跟一些特定的键相对应,命令模式下所有的键盘相应将会作为命令来解释。编辑模式就是编辑文档的模式,在编辑模式下所有键盘的相应都为作为文档输入的内容。通过ESC键可以从编辑模式切换到命令模式。通过一些编辑命名可以从命令模式进入编辑模式。
3.3.2 基本命令
基本命令指的是在命令模式下,通过敲键执行的命令,这里我们介绍几个常用的命令:
i
I
a
A
x
X
D
dd
dw
u
G
此外,在命令模式下,通过输入‘/’可以进行查询,通过输入‘:’可以输入一些其他命令。输入‘:’可以输入的命令包括:
q
w
wq
q!
w!
set number
数字
3.3.3 查找和匹配
vi的查找功能也非常强大,命令模式下通过输入‘/’就可以进入查找模式,可以输入要查找的关键字。然后可以通过‘n’来查找下一处。
在输入‘:’后,还可以输入一些匹配替换的命令——“%s”。命令的格式为“:%s /str1/str2”,执行之后将把当前文件中所有str1替换成str2。举一个典型的例子,前面我们说过,Windows下的文本文件如果以二进制的方式传到了Unix下,那么vi打开的时候每行的行尾就会出现一个“^M”,我们可以用vi打开这个文件,然后通过vi的匹配替换功能去掉这些“^M”。命令格式如下:
其中“^M”通过CTRL+V
- Unix/Linux下C/C++开发技术概览
- Unix/Linux下C/C++开发技术概览
- Unix/Linux下C/C++开发技术概览
- Unix/Linux下C/C++开发技术概览
- Unix/Linux下C/C++开发技术概览
- Unix/Linux下C/C++开发技术概览
- Unix/Linux下C/C++开发技术概览
- Unix/Liunx下C/C++开发技术概览
- Unix/Linux下C/C++开发技术概览-平台差异简介
- Unix/Linux下C/C++开发技术
- unix/linux下c学习指南
- Linux下开发C
- linux下c开发
- 转载 Unix/Linux下C语言学习指南
- Unix/Linux下C语言学习指南
- Unix和Linux下C语言学习指南
- Unix和Linux下C语言学习指南
- linux下C开发中几点技术小总结
- TCP协议与UDP协议的区别
- JSON反序列化解析
- C# 中的委托和事件
- Android:interpolator用法
- 计算机视觉机器学习资源总结
- Unix/Linux下C/C++开发技术概览
- css样式选择器
- spring注解注入单例
- 动态规划经典问题
- poj 1287 Networking ( 最小生成树--prim算法)
- android动画坐标定义
- C#中的委托和事件(续)
- CentOS下查看以及修改文件权限
- 编程之美 - 计算字符串相似度