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 文件、进行并行计算等等。通常下载安装完一个函数库以后,会得到一些库文件和头文件分别存放在 libinclude 文件夹下,以 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可以看成是 -lnetcdff 连在一起,其中的 -llib 的意思,netcdff 为库名,表示你将用到的库文件名为 libnetcdff.solibnetcdff.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.csolve_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,前四个分别生成solvemain.osolve_x.osolve_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/

  • 6
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值