Fortran程序入门介绍及Makefile
一、脚本编译
Linux下,编译fortran脚本的代码如下
$ifort -o hello.exe hello.f90
这里我的 Linux 装的是 Intel 的 Fortran 编译器,如果你想在你的 Linux 上编译 hello.f90
,得看看你那的 Fortran 编译器是啥,然后把这里的 ifort 改成你的编译器的名称就行了。-o hello.exe
会指定可执行文件名为 hello.exe
,你也可以改成 -o myhello
,而后面跟的hello.f90
则是将要编译的源码文件。若是不指定-o
,写成ifort hello.f90
,就会得到名为 a.out
的可执行文件。或许你的编译器是 gfortran?那就输入:
$gfortran -o hello.exe hello.f90
除了 -o
选项,还有 -O
、-O2
、-O3
,这些“杠大欧”们会对你的程序进行优化。
二、加入函数库
为什么需要加入函数库呢?简而言之,外部函数库的功能就是帮助你完成一些 Fortran 本身无法完成的工作,比如读写 nc 文件、进行并行计算等等。通常下载安装完一个函数库以后,会得到一些库文件和头文件分别存放在 lib
和 include
文件夹下,以 netcdf 库(读写 nc 文件的库)为例:
$ ls /opt/netcdf/netcdf-latest/lib/
libnetcdf.a libnetcdff.so libnetcdf.la libnetcdf.so.11
libnetcdff.a libnetcdff.so.6 libnetcdf.settings libnetcdf.so.11.0.0
libnetcdff.la libnetcdff.so.6.0.1 libnetcdf.so pkgconfig
$ ls /opt/netcdf/netcdf-latest/include/
netcdf4_f03.mod netcdf.h netcdf_nc_data.mod
netcdf4_nc_interfaces.mod netcdf.inc netcdf_nc_interfaces.mod
netcdf4_nf_interfaces.mod netcdf_mem.h netcdf_nf_data.mod
netcdf_f03.mod netcdf_meta.h netcdf_nf_interfaces.mod
netcdf_fortv2_c_interfaces.mod netcdf.mod typesizes.mod
那么如何使用 netcdf 库呢?首先需要在源代码里面 use netcdf
,然后调用 netcdf 库函数进行读取。而在编译的时候则需要把库的名称和路径告诉编译器,即编译时加上几个参数。这里提供一个示例程序:
program main
use netcdf
implicit none
character(50) :: ncfile
integer :: fid, varid, stat, year, recnum, start_num
integer :: i
real(kind=8), dimension(200) :: ice
real(kind=8), dimension(180) :: lat
real(kind=8), dimension(360) :: lon
ncfile='HadISST_ice.nc'
year=1979
recnum=12
start_num=(year-1870)*12+1
!打开文件,读取数据
stat=nf90_open(ncfile, NF90_NOWRITE, fid)
stat=nf90_inq_varid(fid, 'sic', varid)
stat=nf90_get_var(fid, varid, ice, start=(/180,10,start_num/), count=(/1,1,recnum/))
stat=nf90_inq_varid(fid, 'latitude', varid)
stat=nf90_get_var(fid, varid, lat)
stat=nf90_inq_varid(fid, 'longitude', varid)
stat=nf90_get_var(fid, varid, lon)
stat=nf90_close(fid)
!输出到屏幕
write(*,*)' lat= ', lat(10), ' lon= ', lon(180)
do i=1,recnum
write(*,*)ice(i)
enddo
end program main
源码就不作过多说明了,我们来看看编译命令及运行结果:
$ ifort -o readnc readnc.f90 -lnetcdff -I/opt/netcdf/netcdf-latest/include -L/opt/netcdf/netcdf-latest/lib
$ ./readnc
lat= 80.5000000000000 lon= -0.500000000000000
0.920000016689301
0.959999978542328
0.850000023841858
0.959999978542328
0.870000004768372
0.910000026226044
0.680000007152557
0.589999973773956
0.259999990463257
0.560000002384186
0.720000028610229
0.750000000000000
编译的命令和之前相比多了三项。-lnetcdff
可以看成是 -l
和 netcdff
连在一起,其中的 -l
是 lib
的意思,netcdff 为库名,表示你将用到的库文件名为 libnetcdff.so
或 libnetcdff.a
,它们的位置可以再库文件之后也可以在之前。库文件的位置由 -L
后的路径给出,头文件则从 -I
后的路径里去找。使用其他库时,也是类似地加上 -l/xxx路径
-I/xxx路径
-L/xxx路径
,然后就能正确调用外部函数了。当然,你可以加上多个 -l、-I 以及 -L。
三、.so / .a / .o文件
1.什么是xxx.so和xxx.a文件
xxx.so
是动态函数库,xxx.a
是静态函数库。两者的区别之一在于,在运行执行文件时,是否需要从库文件中读取函数信息。对于静态函数库,它在编译的时候就直接整合到可执行文件中了,程序可以独立运行的,不需要再读 xxx.a
的内容。而动态函数库,在运行可执行文件时,还是得去xxx.so
里面读取函数。形象点描述的话,静态函数库是揣兜里的,随时可以使用,而动态函数是人家的,要用的时候还得找人家拿来看看。
也许你就要问了,前面使用 netcdf 库编译时加的是 -lnetcdff
,可我们并不知道它到底用的 libnetcdff.so
还是 libnetcdff.a
啊?要知道,在 /opt/netcdf/netcdf-latest/lib 目录下,这两个文件可都是存在的!一般情况下,编译脚本会优先调用动态函数库。此外,我们也可以不使用 -l 和 -L 的组合,而是这样写:
[N@Dell readnc]$ ifort -o readnc readnc.f90 /opt/netcdf/netcdf-latest/lib/libnetcdff.so -I/opt/netcdf/netcdf-latest/include
[N@Dell readnc]$ ifort -o readnc readnc.f90 /opt/netcdf/netcdf-latest/lib/libnetcdff.a -I/opt/netcdf/netcdf-latest/include
可以看到使用 libnetcdff.so
时,编译没有问题。而使用 libnetcdff.a
时则会出错,说明之前采用 -l 与 -L 组合时,实际是用了 libnetcdff.so
文件。至于为什么不能用静态库编译呢?大概是还需要其他函数库,而 libnetcdff.so
里有它们的信息但 libnetcdff.a
里面却没有吧。
那么我们来看另一个有关动态库的问题。还是以 netcdf 库为例,之前我们能够正常编译运行,但现在我把 .bashrc
文件里的某行注释掉(图中netcdf
部分的export LD_LIBRARY_PATH=…
那行):
那么当我重新登录的编译readnc.f90
,就会显示这样的结果:
$ ifort -o readnc readnc.f90 -lnetcdff -I/opt/netcdf/netcdf-latest/include -L/opt/netcdf/netcdf-latest/lib
$ ./readnc
./readnc: error while loading shared libraries: libnetcdff.so.6: cannot open shared object file: No such file or directory
为什么呢?编译没错,却不能运行?那是因为我们编译时用的动态库,运行时就得再次访问libnetcdff.so
文件,而程序需要由LD_LIBRARY_PATH
这个环境变量提供动态库文件所在的位置。这样就懂了吧?注释掉那行就意味着程序再也找不着 libnetcdff.so
了。
动态库和静态库有各自的优点,动态库用起来方便一点,静态库独立一些。前面说过用 libnetcdff.a
编译时,因为缺少其他函数库的信息失败了,而 libnetcdff.so
则含有那些函数库的信息,ldd libnetcdff.so
就能看到。另一方面,动态库升级后不需要重新编译可执行文件,而静态库升级后需要重新编译才能使用新函数。不过,使用静态库的好处在于,编译得到的程序可以独立执行,不再需要访问库文件了。
2.xxx.a和xxx.so怎么生成
这里我再稍微介绍下怎样生成 .a
及 .so
文件。不过先要提一下.o
文件,即目标文件,相信大家都见过。以hello world
为例,如果编译命令这样写,就会生成.o
文件:
[N@Dell hello]$ ifort -c hello.f90
[N@Dell hello]$ ifort -o hello hello.o
[N@Dell hello]$ ls
hello hello.f90 hello.o
反而多敲了一行,为什么一定要生成 .o
文件呢?其实静态库就是将很多 .o
和在一起形成的,动态库也差不多,有了库文件就能多次使用而不用每次都编译了。那如何形成库文件呢?这里我用另一个程序来演示一下,和 hello world
不同,它有三个源码文件。先不管我这个程序是做什么用的,只看看我是如何编译的吧:
[N@Dell test]$ ls
main.c main.h solve_x.c solve_x.h solve_y.c solve_y.h
[N@Dell test]$ mpicc -c solve_x.c
[N@Dell test]$ mpicc -c solve_y.c
[N@Dell test]$ ar -r libsolve.a solve_x.o solve_y.o
ar: creating libsolve.a
[N@Dell test]$ mpicc -o solve main.c libsolve.a
[N@Dell test]$ mpirun -n 2 ./solve
Proc 0: x=1.994085, ex=0.000035, step=11
Proc 1: y=1.999241, ey=0.000002, step=11
这个程序是用 c 语言写的,还用到了并行,因此编译器是 mpicc
,且运行时需要写成 mpirun -n 2 ./solve
,但它跟普通的串行程序看起来区别不大。可以看到我先编译了 solve_x.c
和 solve_y.c
,得到目标文件后把它们合成一个libsolve.a
,建立了自己的一个函数库。然后编译 main.c
时直接就用这个静态库了,最后执行 solve
。如果考虑制作动态函数库,可以这样:
[N@Dell test]$ ls
main.c main.h solve_x.c solve_x.h solve_y.c solve_y.h
[N@Dell test]$ mpicc -fPIC -shared -o libsolve.so solve_x.c solve_y.c
[N@Dell test]$ mpicc -o solve main.c libsolve.so
[N@Dell test]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
[N@Dell test]$ mpirun -n 2 ./solve
Proc 0: x=1.994085, ex=0.000035, step=11
Proc 1: y=1.999241, ey=0.000002, step=11
其过程和静态库生成差不太多,只是出现了 -fPIC
和 -shared
选项,同时还需要设置LD_LIBRARY_PATH
。
四、Makefile简介
Makefile 是啥?为什么要使用它呢?想象一下你有个程序是由100个源文件编译链接而成,那我岂不是要敲很长的命令?若要重新编译,我还得再敲一遍?当然不是,我们可以把编译信息写到一个文件里,即 Makefile,然后敲入命令 make
就行了!而且当你改动了一些源码文件后再次编译时,make
还能够自动识别修改了哪些文件并只编译那些文件,这样可以节省不少时间。还是以 solve
程序为例来演示下 Makefile 的基本规则吧,我写的 Makefile 文件内容如下:
CC = mpicc
solve: main.o solve_x.o solve_y.o
${CC} -o solve main.o solve_x.o solve_y.o
main.o: main.c
${CC} -c main.c
solve_x.o: solve_x.c
${CC} -c solve_x.c
solve_y.o: solve_y.c
${CC} -c solve_y.c
clean:
rm -f solve main.o solve_x.o solve_y.o
其中第一行为定义变量,这里我把编译器定为 mpicc
,如果想换另一个编译器,如ifort,gfortran,只需要修改第一行。后面的内容则以这样的形式书写:
target: object files
[tab] command
其中target
为目标文件或标签,后面的 object files
为完成该 target
所需要用到的文件,两者以冒号隔开。第二行以tab
键开头,后面紧跟完成该 target
的具体命令。这里共有5个 target
,前四个分别生成solve
、main.o
、solve_x.o
、solve_y.o
,最后一个是把这四个文件删除。如果你要执行某一条 target
,可以敲入make target
,比如这样的操作:
[N@Dell test]$ ls
libsolve.a main.c Makefile solve_x.h solve_y.h
libsolve.so main.h solve_x.c solve_y.c
[N@Dell test]$ make solve_x.o
mpicc -c solve_x.c
[N@Dell test]$ make solve
mpicc -c main.c
mpicc -c solve_y.c
mpicc -o solve main.o solve_x.o solve_y.o
[N@Dell test]$ make clean
rm -f solve main.o solve_x.o solve_y.o
当只需要某个target
,直接make
对应的target
就能实现,但是前提是object file
存在
参考
[1]https://www.dazhuanlan.com/2020/02/27/5e57075c96d35/