Linux下的程序编译和运行 曙光信息产业股份有限公司
提纲 Linux下常用编译器介绍 串行程序编译和执行 OpenMP多线程并行程序 MPI并行程序 Make工具介绍
GNU编译器 GCC(GNU Compiler Collection,GNU编译器套件),是一套由GNU开发的编程语言编译器。它是一套以GPL及LGPL许可证所发行的自由软件,也是GNU计划的关键部分,亦是自由的类Unix及苹果电脑Mac OS X 操作系统的标准编译器。GCC(特别是其中的C语言编译器)也常被认为是跨平台编译器的事实标准。 GCC可处理C、C++、Fortran、Pascal、Objective-C、Java,以及Ada与其他语言。 编程语言 编译器调用名称 C gcc C++ g++ Fortran77 gfortran Fortran90/95
Intel编译器 Intel编译器是Intel公司发布的一款x86平台(i386/x86_64)编译器产品,支持C/C++/Fortran编程语言。 Intel编译器针对Intel处理器进行了专门优化,性能优异,在AMD处理器平台上表现同样出色。 编程语言 编译器调用名称 C icc C++ icpc Fortran77 ifort Fortran90/95
PGI编译器 PGI编译器是The Portland Group推出的一款编译器产品,支持C、C++和Fortran,在AMD处理器平台上 性能较好。 此外,PGI编译器还提供对HPF(High Performance Fortran,Fortran90的扩展)编程语言的支持 编程语言 编译器调用名称 C pgcc C++ pgCC Fortran77 pgf77 Fortran90/95 pgf90/pgf95 HPF pghpf
其它x86编译器 Open64(C、C++、Fortran77/90/95) PathScale (C、C++、Fortran77/90/95) Absoft Fortran Compiler g95 Fortran Compiler Lahey Fortran Compiler ...
提纲 Linux下常用编译器介绍 串行程序 OpenMP多线程并行程序 MPI并行程序 Make工具介绍
程序编译流程 源文件 source 编译 目标文件 object 连接 可执行文件 exe 执行
源代码后缀规范 在Linux系统中,可执行文件没有统一的后缀,系统从文件的属性来区分可执行文件和不可执行文件。 而源代码、目标文件等后缀名有统一的规范 文件类型 后缀名 C source .c C++ source .C, .cc, .cpp, .cxx, .c++ Fortran77 source .f, .for Fortran90/95 source .f90 汇编source .s 目标文件 .o 头文件 .h Fortran90/95模块文件 .mod 动态链接库 .so 静态链接库 .a
最简单的例子 hello.c源文件(可用vim等文本编辑器编辑) #include <stdio.h> void main() { printf("Hello world.\n"); } 注1:本文都以C语言示例,C++、Fortran等的编译运行流程与C语言类似 注2:示例使用的编译器为gcc,其它编译器的使用方法类似,请参见相关文档或man手册
最简单的例子(续1) 调用gcc编译源代码,默认在当前目录下生成可执行文件a.out 运行可执行文件 $>gcc hello.c #如果hello.c不在当前目录,需要输入其路径 $>file a.out a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), for GNU/Linux 2.6.4, dynamically linked (uses shared libs), not stripped 运行可执行文件 可以在终端中输入可执行文件的相对或绝对路径: $>./a.out Hello world. $>/home/test/a.out 如果可执行文件所在目录加入了PATH环境变量,可以直接使用可执行文件名 $>export PATH=$PATH:/home/test $>a.out
最简单的例子(续2) 编译时,指定生成可执行文件的路径或文件名(-o参数) $>gcc -o hello hello.c $>file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), for GNU/Linux 2.6.4, dynamically linked (uses shared libs), not stripped $>gcc -o /home/test/hello hello.c
最简单的例子(续3) 前面的例子中,gcc自动执行了编译和连接操作,这两步可以分开进行: 只执行编译,不执行连接(-c参数) $>gcc -c hello.c 生成目标文件hello.o $>file hello.o hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped 连接目标文件,生成可执行文件(编译器实际是调用系统的ld连接器) $>gcc -o hello hello.o $>file hello hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), for GNU/Linux 2.6.4, dynamically linked (uses shared libs), not stripped
多个源文件的例子 主程序源文件 main.c #include <stdio.h> int main() { int sum=0,r,i; for(i=1;i<=10;i++) r=fx(i); sum=sum+r; } printf("sum is %d\n",sum); 子函数源文件 fx.c int fx(int x) { int result; result=x*x; return(result); }
多个源文件的例子(续) 多个源文件同时编译 源文件分别编译,再将目标文件连接成可执行文件 $>gcc -o sum main.c fx.c 生成可执行文件sum $>./sum sum is 385 源文件分别编译,再将目标文件连接成可执行文件 $>gcc -c main.c $>gcc -c fx.c $>gcc -o sum main.o fx.o
指定头文件 源文件中如果包含了头文件,编译器会在自动在一些头文件目录中搜索 编译器默认搜索的头文件目录一般包括(优先级由高到低): 源文件所在目录(要求源文件中用#include "..."格式指定) INCLUDE之类环境变量指定的目录 编译器自己的头文件目录 /usr/include系统头文件目录 如果想自定义头文件搜索路径,可以使用 -I<path> 参数,用 -I 指定的目录优先级高于默认搜索路径 -I参数也可以指定多个:-I<path1> -I<path2> ...
指定头文件(举例) 编译时使用 -I 参数指定头文件搜索路径 $>gcc -c -I/home/test/include main.c 源文件 main.c #include <stdio.h> #include "myhead.h" int main() { printf(STRING); } 头文件 /home/test/include/myhead.h #define STRING "Hello World.\n" 编译时使用 -I 参数指定头文件搜索路径 $>gcc -c -I/home/test/include main.c $>gcc -o program main.o 或者 $>gcc -o program -I/home/test/include main.c
Linux下的函数库文件 Linux下的函数库分为静态库和动态库: 静态库 命名规范为libXXX.a 编译后库函数会被连接进可执行程序,可执行文件体积较大 可执行文件运行时,不需要从磁盘载入库函数,执行效率较高 库函数更新后,需要重新编译程序 动态库 命名规范为libXXX.so 编译后库函数不会被连接进可执行程序,可执行文件体积较小 可执行文件运行时,库函数动态载入 使用灵活,库函数更新后,不需要重新编译程序
静态库的生成 编译子函数源代码 使用ar命令将目标文件打包成静态库.a $>gcc -c fun1.c int fun1(int i) { return(i+i); } 子函数 fun2.c int fun2(int i) { return(i*i); } 编译子函数源代码 $>gcc -c fun1.c $>gcc -c fun2.c 使用ar命令将目标文件打包成静态库.a $>ar cr libtest.a fun1.o fun2.o
动态库的生成 编译子函数源代码,必须要使用 -fPIC 参数 使用编译器-shared参数将目标文件连接成动态库.so 子函数 fun1.c int fun1(int i) { return(i+i); } 子函数 fun2.c int fun2(int i) { return(i*i); } 编译子函数源代码,必须要使用 -fPIC 参数 $>gcc -c -fPIC fun1.c $>gcc -c -fPIC fun2.c 使用编译器-shared参数将目标文件连接成动态库.so $>gcc -o libtest.so -shared fun1.o fun2.o
库函数的使用 生成的库函数可以直接使用,连接时提供即可,可以通过两种方式: 方式一:连接时,直接使用库函数路径 主函数 main.c,会调用库函数中的子程序 #include <stdio.h> int main() { int i=10, sum, product; sum=fun1(i); product=fun2(i); printf("the sum is %d, the product is %d\n",sum,product); } 生成的库函数可以直接使用,连接时提供即可,可以通过两种方式: 方式一:连接时,直接使用库函数路径 $>gcc -c main.c $>gcc -o program main.o /home/test/lib64/libtest.a 或者 $>gcc -o program main.o /home/test/lib64/libtest.so
库函数的使用(续) 方式二:使用编译器的 -L<path> -lXXX 参数,表示在指定库函数路径下搜索名为libXXX.so或libXXX.a的库文件 如果在库函数路径下同时有静态库和动态库,会选择动态库 -L可以指定多次,-L<path1> -L<path2> -L指定的搜索路径优先级最高 如果在-L指定的搜索路径中没有找到库函数,或者没有指定-L,编译器还会按优先级从高到低搜索以下路径 LIBRARY_PATH(静态库)、LD_LIBRARY_PATH(动态库)环境变量指定路径 系统配置文件/etc/ld.so.conf中指定的动态库搜索路径 系统的/lib(64)、/usr/lib(64)等库文件目录 $>gcc -c main.c $>gcc -o program main.o -L/home/test/lib64 -ltest
程序运行时动态库的搜索路径 可执行程序运行时,动态链接的函数库需要从磁盘载入内存,动态库同样有搜索路径 搜索路径优先级从高到低: LD_LIBRARY_PATH 环境变量指定的路径 系统配置文件/etc/ld.so.conf中指定的动态库搜索路径 系统的/lib(64)、/usr/lib(64)等库文件目录 $>gcc -c main.c $>gcc -o program main.o -L/home/test/lib64 -ltest $>export LD_LIBRARY_PATH=$ LD_LIBRARY_PATH:/home/test/lib64 $>ldd ./program linux-vdso.so.1 => (0x00007fffd3bff000) libtest.so => /public/users/libin/test/libtest.so (0x00002b0f21f37000) libc.so.6 => /lib64/libc.so.6 (0x00002b0f2216b000) /lib64/ld-linux-x86-64.so.2 (0x00002b0f21d16000)
编译优化选项 常用的编译优化选项(以GNU编译器为例) -O1、-O2、-O3,不同优化级别,一般情况下选项 -O2 比较合理 -Os,针对减小执行文件体积优化 -funroll-loops,循环展开优化 -march=native、-msse3等,针对CPU指令集优化 ... Intel、PGI等编译器的优化选项请参考相关文档或man手册 关于编译优化 编译优化需适度,过多的优化参数反而可能会降低运行速度 过于激进的优化,会降低程序数值精度,导致计算结果不正确 对于编写不规范的代码,过高的优化级别,可能导致程序运行错误
编译调试选项 常用的一些编译调试选项(以GNU编译器为例) -Wall,打开编译告警,有助于发现代码不规范的地方和潜在错误 -g,用于产生调试信息,供gdb等调试器使用,-g会自动禁止部分编译优化 -pg,用于产生profile信息,供gprof等profile工具使用
提纲 Linux下常用编译器介绍 串行程序 OpenMP多线程并行程序 MPI并行程序 Make工具介绍
OpenMP简介 针对共享式内存的多线程并行编程标准 支持C、C++、Fortran等编程语言 编译制导( Compiler Directive )型,通常由编译器提供支持 主线程 并行执行区域
OpenMP程序示例 OpenMP示例程序hello.c #include <omp.h> #include <stdio.h> int main (int argc, char *argv[]) { #pragma omp parallel printf("Hello World!"); }
OpenMP程序的编译 OpenMP通过编译器支持,编译时打开OpenMP编译选项即可 常用编译器的OpenMP编译选项: GNU Intel PGI OpenMP编译选项 -fopenmp -openmp -mp 以GNU编译器为例: $>gcc -o hello -fopenmp hello.c
OpenMP程序的运行 编译 通过环境变量指定线程数运行 $>gcc -o hello -fopenmp hello.c $>ldd hello linux-vdso.so.1 => (0x00007fff789ff000) libgomp.so.1 => /usr/lib64/libgomp.so.1 (0x00002b3e5ab8e000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00002b3e5ad97000) libc.so.6 => /lib64/libc.so.6 (0x00002b3e5afb4000) librt.so.1 => /lib64/librt.so.1 (0x00002b3e5b313000) /lib64/ld-linux-x86-64.so.2 (0x00002b3e5a96d000) 通过环境变量指定线程数运行 $>export OMP_NUM_THREADS=2 $>./hello 或者 $> OMP_NUM_THREADS=2 ./hello Hello World!
提纲 Linux下常用编译器介绍 串行程序 OpenMP多线程并行程序 MPI并行程序 Make工具介绍
MPI简介 Message Passing Interface(MPI)是消息传递函数库的标准规范,支持Fortran和C、C++
常见MPI实现 注:请区分 OpenMPI 与 OpenMP MPI实现 支持的网络种类 兼容的MPI规范 MPICH Ethernet Ethernet、InfiniBand MVAPICH MPI-1.1,部分MPI-2 MVAPICH2 Intel MPI HP-MPI MPI-1.1,MPI-2 注:请区分 OpenMPI 与 OpenMP
MPI 程序示例 MPI示例程序 hello.c #include <stdio.h> #include <mpi.h> int main (argc, argv) int argc; char *argv[]; { int rank, size; MPI_Init(&argc, &argv); /* start MPI */ MPI_Comm_rank(MPI_COMM_WORLD, &rank); /* get process id */ MPI_Comm_size(MPI_COMM_WORLD, &size); /* get NO. of processes */ printf("Hello world from process %d of %d\n", rank, size); MPI_Finalize(); /* stop MPI */ return 0; }
MPI程序的编译 常见MPI实现都会提供相应的编译命令: 编程语言 C C++ Fortran77 Fortran90/95 编译器调用 mpicc mpicxx mpif77 mpif90 这些编译命令只是一层封装,实际会调用系统编译器,编译时自动添加MPI头文件搜索路径,连接时自动连接MPI库文件,方便使用 以 OpenMPI 的 mpicc为例: $>mpicc -show gcc -I/public/software/openmpi-gnu/include -pthread -L/public/software/openmpi-gnu/lib -lmpi -lopen-rte -lopen-pal -ldl -Wl,--export-dynamic -lnsl -lutil -lm -ldl
MPI程序的编译(举例) 使用 OpenMPI 的 mpicc 编译示例程序hello.c $>mpicc -o hello hello.c $>ldd hello linux-vdso.so.1 => (0x00007fff20dff000) libmpi.so.0 => /public/software/openmpi-gnu/lib/libmpi.so.0 (0x00002b7397ef7000) libopen-rte.so.0 => /public/software/openmpi-gnu/lib/libopen-rte.so.0 (0x00002b739819c000) libopen-pal.so.0 => /public/software/openmpi-gnu/lib/libopen-pal.so.0 (0x00002b73983e8000) libdl.so.2 => /lib64/libdl.so.2 (0x00002b739868f000) libnsl.so.1 => /lib64/libnsl.so.1 (0x00002b7398893000) libutil.so.1 => /lib64/libutil.so.1 (0x00002b7398aab000) libm.so.6 => /lib64/libm.so.6 (0x00002b7398caf000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00002b7398f05000) libc.so.6 => /lib64/libc.so.6 (0x00002b7399122000) /lib64/ld-linux-x86-64.so.2 (0x00002b7397cd6000)
MPI程序的运行 常见MPI实现都会提供MPI程序的启动器,不同MPI实现的启动器名称和语法略有不同: MPI实现 启动器名 指定进程数 指定运行节点 MPICH mpirun -np -machinefile OpenMPI -hostfile MVAPICH mpirun_rsh MPICH2 MVAPICH2 Intel MPI mpiexec.hydra -n -f
MPI程序的运行(举例) 以OpenMPI为例,在单节点运行示例MPI程序: $>mpirun -np 4 ./hello 多节点运行 Hello world from process 2 of 4 Hello world from process 0 of 4 Hello world from process 1 of 4 Hello world from process 3 of 4 多节点运行 $>mpirun -np 4 -machinefile ./hosts.list ./hello $>cat hosts.list node1 node2 (本例中,启动4个进程运行hello程序,每个节点2个进程)
提纲 Linux下常用编译器介绍 串行程序 OpenMP多线程并行程序 MPI并行程序 Make工具介绍
Make简介 在开发大系统时,经常要将程序划分为许多模块。各个模块之间存在着各种各样的依赖关系,在Linux中通常使用 Makefile来管理。 由于各个模块间不可避免存在关联,所以当一个模块改动后,其他模块也许会有所更新,对小系统来说,手工编译连接是没问题,但是如果是一个大系统,存在很多个模块,那么手工编译的方法就不适用了 为此,在Linux系统中,专门提供了make工具来自动维护目标文件。 与手工编译和连接相比,make命令的优点在于他只更新修改过的文件,而对没修改的文件则置之不理,并且make命令不会漏掉一个需要更新的文件。
Makefile文件 Makefile由规则组成,每一条规则都有三部分组成: 目标(object) 依赖(dependency) 命令(commands) object : dependency \t commands #注意:行首这里是一个tab,不是空格! 依赖可以是另一条规则的目标,也可以是文件 如果目标是一个文件,当它的依赖比目标新时,则运行规则所包含的命令来更新目标
一个简单的例子 a.c和b.c两个源文件 Makefile文件内容 extern void p(char *); main() { p(“hello world!"); } b.c #include <stdio.h> void p(char *str) { printf(“%s\n",str); } Makefile文件内容 hello: a.c b.c gcc a.c b.c -o hello 执行make,会查找Makefile,执行gcc a.c b.c -o hello $>make
一个简单的例子(续1) 将上例中的Makefile改造: hello: a.o b.o gcc a.o b.o -o hello a.o: a.c gcc –c a.c b.o: b.c gcc -c b.c 运行make时,可以接一目标名(eg. make hello)作为参数,表示要处理改目标。如没有参数,则处理第一个目标 对上述例子执行make,处理的是hello这个目标 编译完成后,如果更新b.c源文件,执行make,只重新编译b.c,不会重新编译a.c
一个简单的例子(续2) Makefile还可以处理其它的非编译任务 我们继续更新上例的Makefile文件: hello: a.o b.o gcc a.o b.o -o hello a.o: a.c gcc –c a.c b.o: b.c gcc -c b.c clean: rm -rf *.o hello 执行make clean,将删除目标文件和可执行文件 $>make clean
Makefile中的宏变量 Makefile中可以定义宏变量,定义后别处可引用。 宏的定义格式为: 宏名 = 宏的值 (宏名一般习惯用大写字母) 宏的引用格式为: $(宏名) 仍然用前面的例子说明,定义了宏的Makefile: EXE = hello OBJS = a.o b.o CC = gcc CFLAGS = -O2 $(EXE): $(OBJS) $(CC) $(OBJS) -o $(EXE) a.o: a.c $(CC) $(CFLAGS) –c a.c b.o: b.c $(CC) $(CFLAGS) -c b.c clean: rm -rf *.o $(EXE)
Makefile中的宏变量(续) make还有一些内部宏变量,它们根据每一个规则内容定义。 对我们的Makefile继续改造: $@ 当前规则的目的文件名 $< 依赖列表中的第一个依赖 $^ 整个依赖列表(除掉了里面的所有重复内容)。 $? 依赖列表中新于目标的 对我们的Makefile继续改造: EXE = hello OBJS = a.o b.o CC = gcc CFLAGS = -O2 $(EXE): $(OBJS) $(CC) $^ -o $@ a.o: a.c $(CC) $(CFLAGS) –c $< b.o: b.c $(CC) $(CFLAGS) -c $< clean: rm -rf *.o $(EXE)
Makefile中的隐含规则 上面例子中,从.c产生.o的步骤是完全一样的,Makefile能否简化? 用make的隐含规则就可以处理! EXE = hello OBJS = a.o b.o CC = gcc CFLAGS = -O2 $(EXE): $(OBJS) $(CC) $^ -o $@ .c.o: $(CC) $(CFLAGS) –c $< clean: rm -rf *.o $(EXE)
Make进阶 关于make的其它语法和高级功能,可以参考其它资料,或者查阅GNU Make的在线手册: $>man make $>man makefile
谢谢!