Makefile的编写总结

原文链接:Makefile的编写总结
发布时间:2015-09-01 22:10:24

在Linux做开发,工程的编译输出,需要自己编写Makefile来构建,下面笔者根据自己的实践,做一些总结,本文并不是什么教程,只是笔者觉得一些重要的东西在此记录下来。

一、基本格式

1
2
3
target : depend

command

target 是要生成的目标文件,可以是可执行文件、.o、.a、*.so等文件,当然也可以一个伪目标。

depend 是依赖文件,可以是源文件,头文件,*.o文件,当然也可以是一个伪依赖。

command 是执行命令,即如何编译依赖文件生成目标文件。值得注意的是command前面必须是一个tab键。

eg : main.c depend.c depend.h 生成可执行文件main

1
2
3
4
5
6
7
8
main:main.o depend.o
gcc main.o depend.o -o main

main.o : main.c
gcc -c -Wall main.c

depend.o:depend.c depend.h
gcc -c -Wall depend.c

二、变量

Makefile中可以自定义变量,有的地方也称之为宏,变量名尽量用数字、字母和下划线命名,并且对大小写敏感。

变量可以用来代表一个文件名列表、编译选项列表、程序运行的选项参数列表、搜索源文件的目录列表、编译输出的目录列表等。

eg:

1
2
CC = gcc
CFLAG = -c -Wall

引用一个变量是这样:$(VarName),例如:$(CC) , $(CFLAG)

笔者的使用习惯,定义变量全都是大写,显然这和C语言的宏定义一样,称之为宏也不为怪。

这里的赋值号前后可以有空格,这与Shell脚本不同,Shell中定义变量,赋值号前后不能有空格。

改写上面的Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
CC = gcc
CFLAG = -c -Wall
TARGET = main
OBJ = main.o depend.o

$(TARGET):$(OBJ)
$(CC) $(CFLAG) $(OBJ) -o $(TARGET)

main.o : main.c
$(CC) $(CFLAG) main.c

depend.o:depend.c depend.h
$(CC) $(CFLAG) depend.c

三、特殊变量

  • $@ 表示目标对象
  • $^ 表示所有依赖对象
  • $< 表示第一个依赖对象
  • VPATH 指定源文件的搜索路径,它和linux下环境变量类似
  • vpath 和VPATH类似,但是比VPATH更灵活

继续改写上面的Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
CC = gcc
CFLAG = -c -Wall
TARGET = main
OBJ = main.o depend.o

$(TARGET):$(OBJ)
$(CC) $(CFLAG) $^ -o $@

main.o : main.c
$(CC) $(CFLAG) $<

depend.o:depend.c depend.h
$(CC) $(CFLAG) $<

四、项目案例

1
2
3
4
5
6
7
8
Project
├── Bin
├── Build
├── Lib --liblog.a libsqlite.a
└── Source
├── Src1 --Demo1.c Demo1.h
├── Src2 --Demo2.c Demo2.h
└── Src3 --Demo3.c Demo3.h

如上图所示,工程项目Project下有四个目录–Bin,Buld,Lib,Source

  • Source 是源代码目录,其下又有三个子目录Src1,Src2,Src3,在实际项目中,源文件一般按功能模块放在不同的目录下,便于管理。
  • Lib 目录是库目录,根据笔者的经验,这样的目录放第三方的库,如果是开源的库,源代码可以放入其中,编译成一个库文件,链接程序时用到。因为第三方的开源库很少需要修改,编译成库文件后,就可以一直使用了,所以要把他们单独放在Lib文件目录里。
  • Build 目录是编译构建目录,Makefile放在里面,编译时生成的中间文件*.gch,*.o都会在这里,不会散落在源代码目录里,这个目录很有必要。
  • Bin 是生成的二进制程序目录,当然这个目录其实软件运行环境目录,这个目录是要部署到机器上的,一个可执行程序的运行,其实是有很多依赖的,比如可能要读取xml配置文件,可能要依赖某个文件创建进程通信的Key值,在这里我简化了它,Bin目录下没有其他子目录,只存放可执行文件。

假设本工程最终生成可执行文件main,依赖Source下的所有源文件,并且依赖Lib下的liblog.a libsqlite.a

Makefile1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC = gcc
CFLAG = -c -Wall
BIN = ../Bin
LIB = ../Lib
SOURCE = ../Source
LINKLIB = -L$(LIB) -lmylog -lsqlite
VPATH = $(SOURCE)/Src1:$(SOURCE)/Src2:$(SOURCE)/Src3
TARGET = main
OBJ = Demo1.o \
Demo2.o \
Demo3.o

all:$(TARGET)
cp $(TARGET) $(BIN)

$(TARGET):$(OBJ)
$(CC) $(CFLAG) $^ $(LINKLIB) -o $@

%.o:%.c
$(CC) $(CFLAG) $<

clean:
rm *.o main

说明:

  1. Makefile文件是在Build目录中,因此Makefile的工作路径就是这个路径,定义其他路径时都是相对于Makefile的工作路径,当然执行Makefile时也必须在这个路径
  2. 开头定义了几个变量,这样做的好处是使Makefile更容易修改维护
  3. VPATH定义了源文件寻找目录,以冒号间隔,Make会自动到这几个目录寻找源文件
  4. 定义OBJ用的字符 \ 表示行连接符
  5. all 是一个伪目标,执行命令仅仅是把生成的可执行程序main拷贝到Bin目录,和下面的clean类似
  6. %.o:%.c 是Make的一种自动推导方式,表示一个.o文件会依赖一个对应的.c文件,本例中Make会自动到当前目录或者VPATH中寻找依赖Demo1.c Demo2.c Demo3.c

五、常用函数

Makefile里也有函数的,有函数定义和调用的语法格式,但是笔者只使用过内置函数,没有使用过自定义函数。

1、wildcard

通配符函数,按照匹配模式生成一个以空格间隔的列表。

语法格式:$(wildcard pattern1 pattern2 ...) ,参数以空格隔开。

例如:SRC = $(wildcard *.c ./foo/*.c) 表示把当前目录下的.c文件和子目录foo的.c文件以列表赋值给SRC。

2、patsubst

模式替换,以特定模式替换字符串。

语法格式:$(patsubst from, to, str)

1
2
SRC = Demo1.c Demo2.c Demo3.c
OBJ = $(patsubst %.c, %.o, $(SRC)) # 把.c替换成对应的.o

此时OBJ展开:OBJ = Demo1.o Demo2.o Demo3.o

3、subst

字符串替换,暴力的把一个字符串替换成另一个字符串。

语法格式:$(subst from, to, str)

4、filter

过滤函数,保留符合模式的字符串。

语法格式:$(filter pattern1 pattern2 ..., text)

1
2
src = a.c b.c a.h c.sh d.o
csrc = $(filter %.c %.h, $(src)) # 保留.c .h文件

此时csrc展开:csrc = a.c b.c a.h

5、filter-out

过滤函数,保留不符合模式的,与filter功能相反,语法一样。

Makefile2(用函数改写):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CC = gcc
CFLAG = -c -Wall
BIN = ../Bin
LIB = ../Lib
SOURCE = ../Source
LINKLIB = -L$(LIB) -lmylog -lsqlite
SRC = $(wildcard $(SOURCE)/Src1/*.c $(SOURCE)/Src2/*.c $(SOURCE)/Src3/*.c)
TARGET = main
OBJ = $(patsubst %.c, %.o, $(SRC))

all:$(TARGET)
cp $(TARGET) $(BIN)

$(TARGET):$(OBJ)
$(CC) $(CFLAG) $^ $(LINKLIB) -o $@

%.o:%.c
$(CC) $(CFLAG) $<

clean:
rm *.o main

Makefile2 比 Makefile1 更简洁通用,但当工程需要生成多个可执行程序时,Makefile1 中 OBJ 变量的定义更灵活清晰。

由于笔者的水平有限,出错在所难免,恳请读者拍砖指正,谢谢阅读

Linux下使用gdb调试C/C++程序

原文链接:Linux下使用gdb调试C/C++程序
发布时间:2015-08-27 14:55:29

在笔者工作实践中,使用gdb调试C++程序和调试C程序,他们稍有不同,下面是使用总结。

一、编译选项

1
2
3
4
5
-g         生成调试信息

-gstabs+ 以stabs格式生成调试信息,并且包含仅供gdb使用的额外调试信息生成调试信息,是阻止编译器将局部变量优化的寄存器里,

笔者在调试C++程序时,不加这个选项,所有的变量都打印不出来,gdb提示说没有这个符号

二、打断点

对于C程序,代码都是函数封装,打断点很容易,但是C++代码都是类封装,而且函数还可以重载前2种打断点的方式主要用于C程序,后3种多用于C++程序

1
2
3
4
5
6
7
8
9
1、b funcName 即在函数入口处打断点

2、b lineNum 在某一行打断点

3、b className::funcName或者 b className::funcName(type,type) 在某个类的某个函数入口打断点,由于类的函数有重载,可以在函数名内加上参数类型

4、b fileName:lineNum 在某个源文件的某一行打断点

5、b fileName:funcName 在某个源文件的某个函数入口打断点

三、常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1、finish            一直执行到当前函数返回,即跳出当前函数

2、r 运行程序(run)

3、n 执行下一条语句(next 不会进入函数内部)

4、s 执行下一条语句(step 会进入函数内部)

5、回车键 重复执行上一条命令

6、c 继续执行直到下一个断点(continue)

7、p 打印变量的值(print)

8、bt 查看函数堆栈(backtrce)

9、q 退出gdb调试(quit)

10、info break 查看断点信息

11、delete breakNum 删除某个断点

四、bt命令调试段错误

在Linux下编程,一不下心就会出现段错误(Segmentation fault),关于段错误的产生,笔者目前只是大概知道是内存的非法访问造成的,比如访问空指针,指针越界访问都会造成段错误,关于本质原因,还需要研究Linux内存管理、进程空间等知识,这方面有待后续深入研究。下面演示如何利用 core文件定位程序段错误。

测试代码:segfault.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*************************************************************************
> File Name: segfault.c
> Author: KentZhang
> Mail: zhchk2010@qq.com
> Created Time: Thu 27 Aug 2015 02:48:37 PM CST
************************************************************************/
#include<stdio.h>

void test_seg_fault()
{
printf("test_seg_fault() start.\n");
char *p=NULL;
*p = 'a';
printf("test_seg_fault() end.\n");
}

void main()
{
test_seg_fault();
}

1、编译程序生成可执行文件

1
gcc -g segfault.c -o segfault

2、生成Core文件

1
ulimit  -c  unlimited

执行程序:./segfault

1
2
3
[root@localhost MyDir]# ls

core.8450 segfault segfault.c

3、启动gdb,打印栈信息

1
gdb  ./segfault  ./core.8450

然后执行:bt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Loaded symbols for /lib64/ld-linux-x86-64.so.2

Core was generated by `./segfault'.

Program terminated with signal 11, Segmentation fault.

#0 0x00000000004004e2 in test_seg_fault () at segfault.c:13

13 *p = 'a';

Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.x86_64

(gdb) bt

#0 0x00000000004004e2 in test_seg_fault () at segfault.c:13

#1 0x00000000004004ff in main () at segfault.c:19

(gdb)

4、分析结果

          本实例中,程序崩溃后,保留了2层栈的信息,#0 栈,是程序发生崩溃时所在的栈,即是在test_seg_fault函数内发生崩溃,在segfault.c文件的第13行,这一句发生段错误,到此定位完成。其实函数每调用一层,就会有一层栈的信息,最上面的总是 #0 号栈,即程序当前所在的栈,这很符合压栈操作。

由于笔者的水平有限,出错在所难免,恳请读者拍砖指正,谢谢阅读

再谈Linux下的动态库

原文链接:再谈Linux下的动态库
发布时间:2015-08-26 18:35:55

为了解决上一篇的博客《Linux下静态库、动态库的创建和使用》最后留下的问题,今天总结一下Linux下动态库版本号的控制。

一、动态库版本号的组成

对于上一篇博客中提到的库文件libcurl.4.3.0,其中4代表主版本号,3代表次版本号,0代表发行版本号,因此动态库的命名形式为:libname.x.y.z

x – 主版本号(不兼容):重大升级,不同主版本的库之间的库是不兼容的。所以如果要保证向后兼容就不能删除旧的动态库的版本。

y – 次版本号(向下兼容): 增量升级,增加一些新的接口但保留原有接口。高次版本号的库向后兼容低次版本号的库。

z – 发布版本号(相互兼容):库的一些诸如错误修改、性能改进等,不添加新接口,也不更改接口。主版本号和次版本号相同的前提下,不同发布版本之间完全兼容。

二、动态库的3个名字

1、real name

  动态库的real name即动态库本身的文件名,例如libcurl.so.4.3.0

2、soname (short for shared object name)

  动态库的soname即是libname.so.x,显然是保留了主版本号,例如libcurl.so.4。

3、linker name

  动态库的linker name即链接名,libname.so,去掉了后面的版本号,例如libcurl.so。

三、创建带有soname的动态库

在我的上一篇博客中创建的动态库是没有版本号的,也没有soname的。

假如我要开发并维护一个动态库,库的版本控制是必要的,就要按照上述版本号命名的方法。

1
2
gcc -c -fpic mylib.c -o mylib.o
gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 mylib.o

生成了库文件libmylib.so.1.0.0,然后执行命令:

1
readelf -d libmylib.so.1.0.0

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Dynamic section at offset 0x668 contains 21 entries:

Tag Type Name/Value

0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

0x000000000000000e (SONAME) Library soname: [libmylib.so.1]

0x000000000000000c (INIT) 0x468

0x000000000000000d (FINI) 0x5c8
...

由上面的结果可以看到,有一个SONAME字段,Library soname:libmylib.so.1,由此得出结论,在编译生成动态库时,由于我指定了库文件的soname为libmylib.so.1,编译器会这个soname写入库文件的头部。还有一个NEEDED字段表示该动态库所依赖的其他动态库,在本例中是依赖标C库。

四、使用带有SONAME的动态库

测试环境:CentOS6.5 x86_64 GCC 4.4.7

1
2
pwd  # /home/user/lib
ls # libmylib.so.1.0.0 main.c mylib.c mylib.h mylib.o

编译:

1
gcc main.c -o main -L. -lmylib

编译报错:

1
2
/usr/bin/ld: cannot find -lmylib
collect2: ld returned 1 exit status

这样编译无法通过,上篇博客已经说明原因。

这样编译:

1
gcc main.c -o main libmylib.so.1.0.0  -Wl,-rpath=/home/user/lib

可以通过,这样是直接把libmylib.so.1.0.0当成目标文件,完成链接过程,生成可执行程序main

然后执行命令:

1
readelf -d main
1
2
3
4
5
6
7
8
9
10
Dynamic section at offset 0x7a8 contains 22 entries:

Tag Type Name/Value

0x0000000000000001 (NEEDED) Shared library: [libmylib.so.1]

0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

0x000000000000000f (RPATH) Library rpath: [/home/user/lib]
...

由以上执行结果可知,main依赖2个动态库,分别是libmylib.so.1,libc.so.6

显然libmylib.so.1是libmylib.so.1.0.0的SONAME,由此得出结论:

编译器在链接动态库时,会读取库的SONAME,并且写在可执行文件的头部,那么程序在运行时, 会根据自己的头部记录的SONAME,去搜索对应的动态库。其中RPATH字段是动态库搜索路径,程序运行时,会首先去这个路径寻找所依赖的动态库。

但是此时,生成的main并不能执行,报错如下:

1
2
./main: error while loading shared libraries: libmylib.so.1: cannot open shared object
file: No such file or directory

为何不能执行呢?执行:

1
ldd main

结果如下:

1
2
3
4
5
6
7
8
9
[root@localhost lib]# ldd main

linux-vdso.so.1 => (0x00007fff7c3ff000)

libmylib.so.1 => not found

libc.so.6 => /lib64/libc.so.6 (0x00000034b0800000)

/lib64/ld-linux-x86-64.so.2 (0x00000034b0000000)

原来libmylib.so.1这个文件 not found,这个文件确实没有,只有libmylib.so.1.0.0,由此得出结论:

程序运行会很据自己头部信息去寻找这些以SONAME命名的库文件。

上一篇博客提到,使用动态库有4种方法,上面我们使用的是最后一种方法,那么再试试其他方法吧!!!

1
2
3
gcc main.c -o main libmylib.so.1.0.0
# 然后把/home/user/lib路径加入/etc/ld.so.conf文件中
ldconfig

查看当前目录有何变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@localhost lib]# ll

total 32

lrwxrwxrwx. 1 user user 17 Aug 26 15:32 libmylib.so.1 -> libmylib.so.1.0.0

-rwxrwxr-x. 1 user user 5900 Aug 26 14:37 libmylib.so.1.0.0

-rwxrwxr-x. 1 user user 6731 Aug 26 15:13 main

-rw-r--r--. 1 user user 345 Aug 26 14:52 main.c

-rw-rw-r--. 1 user user 340 Aug 26 14:22 mylib.c

-rw-rw-r--. 1 user user 304 Aug 26 14:21 mylib.h

-rw-rw-r--. 1 user user 1544 Aug 26 14:37 mylib.o

由上面结果可以看到,当前目录生成了一个名为libmylib.so.1软链接,而且指向libmylib.so.1.0.0

此时再执行:

1
2
3
4
5
6
7
8
9
[root@localhost lib]# ldd main

linux-vdso.so.1 => (0x00007fff8ebff000)

libmylib.so.1 => /home/user/lib/libmylib.so.1 (0x00007fac4c78a000)

libc.so.6 => /lib64/libc.so.6 (0x00000034b0800000)

/lib64/ld-linux-x86-64.so.2 (0x00000034b0000000)

可以看出,main头部信息里记录是:libmylib.so.1,因此运行时就去找这个文件,于是找到了这个文件/home/user/lib/libmylib.so.1,而它是个软链接指向libmylib.so.1.0.0,于是就可以加载这个库文件了,程序可以正确运行了。

五、ldconfig有何用处?

由上面可以看出,ldconfig作用就是生成了一个以SONAME命名并且指向库文件的软链接。

如果现在我的库文件更新了,增加一些接口,依然兼容原来的版本,那么库文件名应该为:libmylib.so.1.1.0,SONAME仍然是libmylib.so.1

1
2
gcc -c -fpic mylib.c -o mylib.o
gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.1.0 mylib.o
1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost lib]# ll

total 40

lrwxrwxrwx. 1 root root 17 Aug 26 15:47 libmylib.so.1 -> libmylib.so.1.0.0

-rwxrwxr-x. 1 user user 5900 Aug 26 14:37 libmylib.so.1.0.0

-rwxr-xr-x. 1 root root 5900 Aug 26 15:59 libmylib.so.1.1.0

-rwxrwxr-x. 1 user user 6731 Aug 26 15:13 main
...

显然多了一个库文件libmylib.so.1.1.0,然后再执行:

1
ldconfig
1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost lib]# ll

total 40

lrwxrwxrwx. 1 root root 17 Aug 26 16:28 libmylib.so.1 -> libmylib.so.1.1.0

-rwxrwxr-x. 1 user user 5900 Aug 26 14:37 libmylib.so.1.0.0

-rwxr-xr-x. 1 root root 5900 Aug 26 15:59 libmylib.so.1.1.0

-rwxrwxr-x. 1 user user 6731 Aug 26 15:13 main
...

结果:软链接libmylib.so.1不再指向libmylib.so.1.0.0,而是指向libmylib.so.1.1.0,这是为什么呢?

显然软链接会指向一个最新的版本,程序就会加载到这个最新的库,由于相同SONAME的库高版本是向下兼容的,因此可以最大程度的兼容程序。

如果我的动态库又更新了,这次变化很大,删除了一些以前的接口,那么这次主版本号要变化了,库文件名应该为 libmylib.so.2.0.0,SONAME也变化为libmylib.so.2,而且不兼容以前的版本libmylib.so.1.x.y.

1
2
3
gcc -c -fpic mylib.c -o mylib.o
gcc -shared -Wl,-soname,libmylib.so.2 -o libmylib.so.2.0.0 mylib.o
ldconfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost lib]# ll

total 48

lrwxrwxrwx. 1 root root 17 Aug 26 16:28 libmylib.so.1 -> libmylib.so.1.1.0

-rwxrwxr-x. 1 user user 5900 Aug 26 14:37 libmylib.so.1.0.0

-rwxr-xr-x. 1 root root 5900 Aug 26 15:59 libmylib.so.1.1.0

lrwxrwxrwx. 1 root root 17 Aug 26 16:41 libmylib.so.2 -> libmylib.so.2.0.0

-rwxr-xr-x. 1 root root 5900 Aug 26 16:41 libmylib.so.2.0.0

-rwxrwxr-x. 1 user user 6731 Aug 26 15:13 main
...

于是乎,又生成一个库文件libmylib.so.2.0.0 和一个软链接 libmylib.so.2,显然多个不同SONAME的库文件可以同时工作了,互不影响。

结论:

  1. ldconfig会遍历/etc/ld.so.conf 中的目录(也包括/lib,/usr/lib)下的库文件,根据读取库文件的SONAME,会在库文件所在目录,生成一个以SONAME命名的软链接并且指向这个库文件。
  2. 如果有多个SONAME相同的库文件,那么这个软链接只会指向一个最新版本的库文件,即次版本号最大的库,若次版本号也相同,会指向发布版本号最大的库文件。
  3. ldconfig还会生成缓存记录在/etc/ld.so.cache中,程序寻找库,会首先找这个cache文件。

关于linker name:

我们在链接库的时候,比如 gcc main.c -o main -L. -lmylib,只需要使用库的名字即可,如何才能做到这样子呢?

那就必须手动创建软链接:

1
ln -s libmylib.so.1 libmylib.so

生成软链接libmylib.so指向libmylib.so.1,libmylib.so.1 也是一个软链接。

编译参数 -L. -lmylib 表示在当前目录寻找libmylib.so文件,libmylib.so -> libmylib.so.1 -> libmylib.so.1.1.0 最终会链接到具体的库文件。当然 libmylib.so 也可以指向 libmylib.so.1,这个需要根据实际情况,选择库文件的版本。

六、解决上篇博客的留下的问题

通过以上的分析,已经清楚了动态库的加载细节,可以解决哪个问题了。

问题:

1
gcc main.c -o main /home/user/lib/libcurl.so.4.3.0  -Wl,-rpath=/home/user/lib

这种可以编译通过,但是程序无法运行。

解决方法:

方法一、在进入/home/user/lib目录下,建立SONAME软链接

1
ln -s libcurl.so.4.3.0 libcurl.so.4

方法二、把libcurl.so.4.3.0文件改名为libcurl.so.4,如果使用的库不更新的话,可以使用这种方法。

上面讲了那么多,下面做一个要点总结:

1、要解决不同版本库依赖问题,要遵守上述的库版本号命名约定。

2、如何创建带有SONAME的动态库。

3、编译器在编译链接动态库时,会先读取动态库的SONAME,并把SONAME记录在可执行文件的头部(还有一种特殊情况,假入动态库没有SONAME,例如libmylib.so,那么编译器会把动态库的文件名添加到可执行那个文件的头部,其实就是real name, 其实种情况,可以理解为real name,Soname,linker name 三者相同)

4、程序运行时,会读取自己头部记录的所依赖的库文件名字,会根据这个名字去寻找同名的文件(去哪里寻找,前面已经总结),找到该文件后加载它,显然大多时候,是找到一个软链接,该软连接指向具体的库文件,这样便可以加载成功。

5、关于ldconfig的作用,这里不再陈述。

由于笔者的水平有限,出错在所难免,恳请读者拍砖指正,谢谢阅读。

一个守护vsftp进程的脚本

原文链接:一个守护vsftp进程的脚本
发布时间:2015-08-25 21:56:38

最近一个朋友遇到了如下需求任务,是要求用shell实现一个常驻程序,让我帮助他完成。需求如下:

1、该程序每5秒钟检查vsftp进程运行状态

2、如果vsftp进程正在运行,检查当前目录下的vsftpd.pid文件是否存在:如果不存在则自动创建文件,并在文件中 写入进程号;如果pid文件存在,则检查当前进程号与pid文件中的进程号是否一致,如果不一致,则自动更新pid 文件为当前进程号;

3、如果vsftp进程不存在,则:如果shell首次启动运行,则主动拉起vsftp进程,并把进程号写入到vsftpd.pid文件 中;

4、如果上一次检查时程序正在运行,这次检查程序已经不存在,说明程序在这段时间异常退出,则主动拉起vsftp进 程,更新vsftpd.pid文件,同时写出告警文件,告警文件的格式是 Warn_YYYY_MM_DD_SN.txt(YYYY 年 MM 月 份 DD 日期 SN 自增序列号)

代码如下:DaemonVsftp.sh

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
#!/bin/bash

IsFirstStart=0
ProcessName=vsftpd #vsftp服务的进程名
FileName=vstfpd.pid
SNFileName=SNFile.txt #记录自增序列号SN,最开始为1,每次读取作为SN,
#然后将该值加1在写入文件,下次再读取作为SN

#如果vsftp进程存在,要执行的任务
FuncExistProcess(){
PID=$(pidof ${ProcessName}) #获得vsftp进程ID
if [ -e $FileName ];then
OldPID=$(cat $FileName)
if [ ! $PID -eq $OldPID ];then
echo $PID > $FileName
fi
else
touch $FileName
echo $PID > $FileName
fi
}

#如果vsftp进程不存在,要执行的任务
FuncNoProcess(){
if [ $IsFirstStart -eq 0 ];then
echo "start vsftp!"
service vsftpd start #启动vsftp
IsFirstStart=1
else
echo "start vsftp!"
service vsftpd start
SN=$(cat $SNFileName)
NewSN=$[$SN+1]
echo $NewSN > $SNFileName #将加1后的SN写入文件
WarnFileName="Warn_"$(date +%Y_%m_%d)"_"${SN}".txt" #拼接警告文件名
touch $WarnFileName
fi
}

if [ ! -e $SNFileName ];then #第一次执行该脚本时,创建SNFile.txt文件,并将1写入
touch $SNFileName
echo 1 > $SNFileName
fi

while true #死循环,每5秒循环一次
do
if [ $(pgrep $ProcessName|wc -l) -gt 0 ];then #判断vstfp进程是否存在
FuncExistProcess
else
FuncNoProcess
fi
sleep 5
done

由于笔者的水平有限,出错在所难免,恳请读者拍砖指正,谢谢阅读。

Linux下静态库、动态库的创建和使用

原文链接:Linux下静态库、动态库的创建和使用
发布时间:2015-08-25 20:26:24

Linux下静态库、动态库的创建和使用

Linux库文件名由:前缀lib、库名和后缀3部分组成,静态库通常以.a作为后缀,动态库以.so作为后缀,

Linux下把动态库叫做共享库,so即shared object的缩写。

静态库是程序编译链接时使用,动态库是程序运行时使用

预备知识:

a) 2个重要的环境变量:

LIBRARY_PATH : 库文件(静态库或者动态库)搜索路径,编译器链接时,需要去这些路径搜索相应的库,当然系统默认的库文件搜索路径是/usr/lib和/lib。

LD_LIBRARY_PATH :动态库文件搜索路径,程序运行时,需要去这些路径搜索相应的动态库,程序运行时,默认的搜索动态库的路径也是/usr/lib和/lib。

b) 2个重要的系统文件:

/etc/ld.so.conf :里面可以加上动态库的搜索路径,绝对路径,(/usr/lib和/lib默认包含其中)

/etc/ld.so.cache :保存/etc/ld.so.conf里所有路径的所有的动态库文件的缓存,保存了已排好序的动态链接库名字列表,有一个命令:ldconfig 会根据/etc/ld.so.conf生成或者更新/etc/ld.so.cache,执行这个命令需要root权限。(关于这个cache文件里面具体实现哪些东西,笔者也不是很清楚,待后续研究

程序在运行时,如果都要搜索/etc/ld.so.conf里目录下库文件,效率会很低,因此Linux采用了类似高速缓存制,程序先去查找缓存文件/etc/ld.so.cache ,因此当/usr/lib或者/lib下放入新的动态库后,也要执行sudo ldconfig 重新生成缓存文件,否则程序无法运行,报错提示,找不到共享库。当往/etc/ld.so.conf里添加目录时,或者后来在这些目录下添加新的库,都需要执行 sudo ldconfig 生成新的缓存文件。

==================================================================================

假设有如下文件:mylib.h mylib.c depend.h depend.c

一、创建静态库

1
2
3
gcc -c mylib.c -o mylib.o
gcc -c depend.c -o depend.o
ar rcs libmylib.a mylib.o depend.o

ar 其实就是将*.o文件打包归档而已,主要参数说明:

  r --- 在库中加入成员文件,若存在,则替换

  c --- 创建一个静态库

  s --- 无论ar命令是否修改了库的内容,都强制重新生成库符号表

  d --- 从库中删除成员文件

二、使用静态库(3种方法)

1、把库文件放入/usr/lib或者/lib目录下

1
gcc main.c -o main -lmylib

2、链接时指定库的目录,假设库文件放在/home/user/lib/下

1
gcc main.c -o main -L/home/user/lib -lmylib

3、修改环境变量,将库文件所在目录/home/user/lib/加入到LIBRARY_PATH

1
2
export  LIBRARY_PATH=/home/user/lib/:$LIBRARY_PATH
gcc main.c -o main -lmylib

三、创建动态库(2种方法)

1
2
3
4
5
6
7
# 方法1
gcc -fpic -c mylib.c -o mylib.o
gcc -fpic -c depend.c -o depend.o
gcc -shared -o libmylib.so mylib.o depend.o

# 方法2
gcc -fpic -shared mylib.c depend.c -o libmylib.so

四、使用动态库(4种方法)

假设libmylib.so库文件在/home/user/lib/下,以下4种方法:

1、把动态库放入 /usr/lib或者/lib下,然后执行sudo ldconfig

1
gcc main.c -o main -lmylib

2、修改环境变量

1
export LD_LIBRARY_PATH=/home/user/lib/:$LD_LIBRARY_PATH

注意这种方式,不能编译通过,因为LD_LIBRARY_PATH只和运行时有关,所以正确的做法是:

1
2
# 编译 + 运行时路径
gcc main.c -o main -L/home/user/lib -lmylib

或者将/home/user/lib加入到LIBRARY_PATH中:

1
2
export LIBRARY_PATH=/home/user/lib/:$LIBRARY_PATH
gcc main.c -o main -lmylib

3、将/home/user/lib/加入到/etc/ld.so.conf中,执行sudo ldconfig

1
gcc main.c -o main -lmylib

4、在编译时,设置动态库运行时寻找路径

1
gcc main.c -o main -L/home/user/lib -lmylib -Wl,-rpath=/home/user/lib

-Wl,-rpath= 指定程序在运行时,搜索动态库会第一个搜索这个指定目录,然后是LD_LIBRARY_PATH,再是/etc/ld.so.conf中的路径。

结论:程序运行时,动态库搜索路径顺序:

  1. 编译时 -Wl,-rpath= 指定的路径
  2. LD_LIBRARY_PATH
  3. /etc/ld.so.cache,即/etc/ld.so.conf中的路径。

根据笔者的实践经验,笔者推荐使用第4种方法,原因如下:

1、对于方法1,诚然简单方便,易于批量部署,但是假如我使用动态库在目标机器也存在,比如 libcurl.so, boost库,如果我使用的libcurl.so比目标机器上的libcurl.so库版本高,那么就等于升级了目标机器的libcurl.so库文件,虽然Linux约定了动态库版本兼容的规则,但是对于随意升级目标机器上的库文件,有时候会造成一些问题,显然污染了系统文件的纯洁。

2、对方法2和方法3都需要修改配置文件,比较麻烦,不易于批量部署,而且也容易污染系统文件的纯洁。

3、方法4的库文件路径可以自己定义,与系统库文件隔离了,解决了方法1,2,3存在的问题。

然而在实践过程中,使用方法4会出现一些莫名其妙的问题。

比如我的程序依赖这个库libcurl.so.4.3.0(一个有名的开源库),并且放在/home/user/lib下,

1
2
3
4
5
# 无法编译 —— libcurl.so 不存在,实际文件是 libcurl.so.4.3.0
gcc main.c -o main -L/home/user/lib -lcurl -Wl,-rpath=/home/user/lib

# 可以编译,但无法运行
gcc main.c -o main /home/user/lib/libcurl.so.4.3.0 -Wl,-rpath=/home/user/lib

为什么会出现这种问题呢?该如何解决呢?请参见下一篇博客《再谈Linux下的动态库》。

以上内容是笔者在工作中,使用动态库、静态库的个人总结,今作记录于此,由于笔者的水平有限,出错在所难免,恳请读者拍砖指正,谢谢阅读。

Vim命令使用总结

原文链接:Vim命令使用总结
发布时间:2015-08-20 23:39:57

vim的使用命令非常繁多,要想全部掌握,那是不现实的,也没有必要,只需要掌握常用的命令,便可在工作中使用地得心应手。

注意下面括号中内容为笔者的注释。

一、基本命令(命令模式)

:help command —命令帮助手册(例如执行:help w 将给出w命令的解释)

:w —写入文件

:q —退出vim

:q! —不保存强制退出vim

shift + zz —退出vim并保存(这个是在正常模式执行)

:x —退出vim并且保存文件(同shift + zz)

:X —给文件加密(执行该命令会让你输入加密密码,然后执行:w 写入命令,文件经加密了,当再次打开此文件时,需要这个密码。那么如何去掉加密设置呢?执行命令:set key= 意思就是设置密码为空,然后再执行:w 写入命令,此时文件便去掉加密设置)

:%!xxd —将文本转化为16进制数字

:%!xxd -r —-将16进制数字转化为文本

二、光标移动 (正常模式)

$ —将光标移动到行尾(在正则表达式中,’$’符号可以匹配行尾)

0 —将光标移动到行首(0是数字零)

^ —将光标移动到改行的第一个非空格字符(在正则表达式中,’^’符号可以匹配行首)

gg —将光标移动到文件的开始位置(g == goto)

G  —将光标移到到文件的结束位置(G == goto)

H  —将光标移到屏幕上第一行 (H == head)

L  —将光标移到屏幕上最后一行(L == low)

% —使光标在花括号的左侧,按下 shift + %,可以使光标跳到与之配对的花括号上,当代码块比较大的时候,这个命令很有用

Ctrl + b  —向后翻一页(b == backward)

Ctrl + f  —向前翻一页(f == forward)

w —光标跳到下个word的开头(w == word)

e —光标跳到下个word的字尾(e == end) 

b —光标回到上个word的开头(b == backward)

K —跳到光标所在关键字的Man手册上

三、文本编辑(正常模式)

dd —删除一行 (d == delete)

ndd —向下删除n行,包括光标所在行(n为一个整数)

x —删除光标后面字符,与退格键功能相反

yy —复制光标所在行(y == yank)

nyy —复制n行,包括光标所在行

p —粘贴(p == put)

np —粘贴n次

u —撤销上一次操作(u == undo)

d0 —删除至行首

d$ —删除至行尾

四、代码缩进(正常模式)

有时候代码经过别的编辑器修改或者生成,再次用Vim打开发现代码格式混乱,那么Vim可以帮我们完成代码缩进。

在正常模式下,按下 v 进入可视模式,选中要格式的代码,然后按下 = ,便会自动递归缩进代码。那么如何缩进整个文本呢?vim下是没有Ctrl + a全选这个操作。

解决方法:在正常模式下分别按下 ggvG=  便可全部选中,并递归缩进整个文本的代码,由此可见ggvG相当于ctrl + a ,gg 表示光标跳到文件首,v表示进入可视模式,G表示光标跳到文件尾。

五、查找替换(命令模式)

/pattern —从光标开始处向文件尾搜索pattern(正常模式,下面3个也是) 

?pattern —从光标开始处向文件首搜索pattern 

n —在同一方向重复上一次搜索命令 

N —在反方向上重复上一次搜索命令 

:s/from/to/g —将当前行中所有from均用to替代 (命令模式,其中from可以是正则表达式,下同)

:n1,n2s/from/to/g —将第n1至n2行中所有from均用to替代 

:%s/from/to/g —将文件中所有from均用to替换 

六、多文件编辑

1.多文件操作

例如: vim file1 file2 file3

此时vim打开了3个文件,file2占满vim窗口,那么各个文件如何切换呢?

:n —切换到下一个文件

:N —切换到前一个文件

:rew —切换到第一个文件

2、分屏操作

vim -on file1 file2 file3 

上下分屏打开3个文件

vim -On file1 file2 file3 

左右分屏打开3个文件

如果vim已经启动,下面命令将使vim再次上下分屏或者左右分屏

:sp file4 file5 (上下分屏,sp == split)

:vsp file6 file7 (左右分屏,vsp == vertical split)

那么各个窗口之间如何切换呢?

Ctrl + ww 后一个(w == window,下同)

Ctrl + wp 前一个

Ctrl + wh 四个方向

Ctrl + wi

Ctrl + wk

Ctrl + wl

如果只想保留光标所在的屏,关掉其他所有可以执行

Ctrl + wo

以上为笔者在工作常用到的vim命令,今作总结于此,由于笔者的水平有限,出错在所难免,恳请读者拍砖指正,谢谢阅读。