GCC 编程入门
一、 gcc 基础
①简介
• gcc(GNU C Compiler),是 GNU 项目的编译器套件。
• gcc 对源码进行预处理、编译、汇编和连接,生成可执行文件。
• gcc 调用 cc1 进行编译,生成汇编语言程序。但若非指定则立
即调用 as 进行汇编生成具有.o 扩展名的目标文件(所以这两步
也常被一起统称为编译)。中间的汇编语言输出不保存在文件
中,除非使用-S 参数指定。
(-S 参数用于输出以.s 作扩展名的 Linux 汇编语言文件)
• gcc 常用参数:
-c 只编译生成目标文件,不连接
-o 指定输出文件名。若不指定默认的可执行文件名是 a.out。
-g 产生用于 GDB 调试和排错的扩展符号表(毋与优化同用)
-O 代码的长度和速度优化
-O2 更好的速度优化
②编译示例
• 第一个示例程序 hello.c,如下:
#include <stdio.h>
int main(void)
{
printf(“Hello,world!\n”);
exit(0);
} 编译并连接:gcc –o hello hello.c
执行:./hello
显示:Hello,world!
(注:在 Linux 中执行当前目录中的文件须指定“./”,除非修
改主目录下的.bash_profile 文件,在 PATH 语句末尾加上“:.”,
但基于安全考虑不提倡这种做法。)
• 第二个程序示例多模块文件编译,拆 hello.c 为两个文件:
//helloa.c 第一个文件
exitern void disp(char *);
int main(void)
{
disp_str(“Hello, world!/n”);
exit(0);
}
//hellob.c 第二个文件
#include <stdio.h>
void disp_str(char *str)
{ printf(“%s”,str); }
编译:gcc –c helloa.c hellob.c (或 gcc –c hello?.c)
这一步生成 helloa.o 和 hellob.o 文件
连接:gcc –o hello helloa.o hellob.o (或 gcc –o hello?.o) 执行:./hello
③Makefile 文件
• Makefile 文件可有效地控制多模块文件的项目编译。
• 使用 make 命令启动应用了 Makefile 文件的项目编译过程。
• 若非使用-f 选项指定 make 所用的 makefile,make 将在当前
目录下以 GNUmakefile、makefile、Makefile 的顺序寻找。
资料中一般都建议使用 Makefile 文件名。
• Makefile 的编写规则:
暂且理解为:自顶向下分步求精地结构化表述,句法是:
目标:与此目标直接相关的依赖文件列表
如何利用这些依赖文件生成目标
例:第二个示例程序的 Makefile 文件,可以如下编写:
hello: helloa.o hellob.o
gcc –o hello helloa.o hellob.o
helloa.o: helloa.c
gcc –c helloa.c
hellob.o: hellob.c
gcc –c hellob.c
执行 make 命令后即可生成 hello 可执行文件。
(如果使用了用户头文件,应排在与目标有关的文件列表行
中。如:helloa.o: helloa.c helloa.h)
• Makefile 文件的简化:
Makefile 有几个很有用的变量,其中常用的三个意义分别是:
$@ --目标文件,
$^ --所有的依赖文件,
$< --第一个依赖文件.
利用这三个变量可以简化我们的 Makefile 文件为:
hello: helloa.o hellob.o
gcc -o $@ $^
helloa.o: helloa.c
gcc –c $<
hellob.o: hellob.c
gcc –c $<
利用 Makefile 的缺省规则 .c.o: gcc -c $<(这个规则表示所有
的 .o 文件都是依赖与相应的.c 文件) 还可以进一步简化:
hello: helloa.o hellob.o
gcc -o $@ $^
.c.o: gcc -c $<
(只要修改文件中目标名和目标文件名即可应用于其它项目)。
• 在 Makefile 文件中还可以增加其它目标,比如追加两行:
clean:
rm –f hello *.o
对编译无影响,但执行 make clean 命令可删除所有目标模块。
(回想一下编译内核章节,是否见过这种用法?) • 也可以利用 automake 和 autoconf 生成 Makefile,但笔者不
认为就能很轻松。其详细用法请参见相关资料,这里从略。
二、 进程创建与守护程序编程
①本次用于第七章第九节讲解前后台作业管理的道具程序:
//beep1.c 每 2 秒响一声,15 声后结束。
#include <stdio.h>
main(void)
{
int i;
for(i=0; i<15; i++) {putc(7,stdout); fflush(stdout): sleep(2);}
}
说明:⑴ASC 码 7 表示响铃,fflush()用于泻缓冲。
⑵编译:gcc –o beep1 beep1.c
⑶前台运行:./beep1 后台运行:./beep1&
②子进程和守护程序的概念
• fork()函数用于创建子进程,其原型在 unistd.h和 stdlib.h,
函数格式:int fork(void)
• 子进程是父进程的拷贝,fork()是分支点。分支前的代码只
执行一次
• fork()函数的最特殊点是每调用一次返回两次:在父进程中
返回所创建的子进程的 ID 值;在子进程中的返回值是 0。因此可以从返回值来判断这两次返回都是在哪个进程完成的。
(若出错返回-1)。
• PID(Process Identify Number),进程标识号。
• Linux 中实现守护程序的一般做法是:父进程在成功 fork()
后结束,子进程便成为守护进程(daemon)。
③daemon 化改造后的 beep 程序
//beep2.c 每 2 秒响一声,15 声后结束。守护程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
main(void)
{
pid_t pid; int i;
pid = fork();
if(pid<0) {printf(“Can’t fork!\n”); exit(-1);}
if(pid>0){printf(“Daemon (pid=%d) on duty!\n”, pid); exit(0);}
printf(“I am daemon beep2 (pid=%d) !\n”, getpid());
for(i=0; i<15; i++) {putc(7,stdout); fflush(stdout): sleep(2);}
exit(0);
}
编译:gcc –o beep2 beep2.c 执行:./beep2
验证:jobs 命令只能显示后台执行的作业,看不到守护进程。
查看守护进程要用 ps。(通常的守护程序并没有类似此
例结束处理,杀死常驻的守护进程要用 kill 命令。
三、 时间编程
①获取当前的日期与时间
• #include <time.h>
time_t time(time_t *tloc);
char *ctime(const time_t *clock);
time 函数返回从 1970 年 1 月 1 日 0 点以来的秒数,存储在
time_t 结构之中。ctime 函数可将上述秒数转化为象 Thu Dec 7
14:58:59 2000 这样长度固定为 26 的字符串。
//示例 now_tm1.c:
#include <time.h>
#include <stdio.h>
main()
{ time_t the_time;
time(&the_time);
printf("The date and time is %s",ctime(&the_time));
exit(0);
}
• 还可用到时间函数 localtime(),显示本地时间的函数:
//示例 now_tm2.c:
#include<time.h>
#include<stdio.h>
main()
{ struct tm *tm_ptr; time_t the_time;
(void) time(&the_time);
tm_ptr=localtime(&the_time);
printf("Raw time is %ld\n",the_time);
printf("Mytime show:\n");
printf("Date:%02d - %02d - %02d\n",
tm_ptr->tm_year,tm_ptr->tm_mon+1,tm_ptr->tm_mday);
printf("Time:%02d:%02d:%02d\n",
tm_ptr->tm_hour,tm_ptr->tm_min,tm_ptr->tm_sec);
exit(0);
}
显示结果:
Raw time is 1107433723
Mytime show:
Date: 105-02-03
Time: 07:28:43
结果的第一行显示的数字是 linux 的原始时间表示。还有一个时间函数 gmtime() ,显示的是格林尼治时间,将例子程序
的 localtime() 换成 gmtime(),会与北京时间相差 8 小时。
②计时
• 若需测量某个程序执行所花费的时间时,可用下面这个函数:
#include <sys/time.h>
int gettimeofday(struct timeval *tv,struct timezone *tz);
strut timeval { long tv_sec; /* 秒数 */
long tv_usec; /* 微秒数 */
};
gettimeofday 将时间保存在结构 tv 中。tz 一般可置为 NULL。
• 示例
//delta_tm.c 测试 function()函数的运行时间
#include <sys/time.h>
#include <stdio.h>
#include <math.h>
void function()
{
unsigned int i,j; double y;
for(i=0;i<1000;i++)
for(j=0;j<1000;j++) y=sin((double)i);
} main()
{
struct timeval tpstart,tpend; float timeuse;
gettimeofday(&tpstart,NULL);
function();
gettimeofday(&tpend,NULL);
timeuse=1000000*(tpend.tv_sec-tpstart.tv_sec)+
tpend.tv_usec-tpstart.tv_usec;
timeuse /= 1000000;
printf("Used Time:%f\n",timeuse);
exit(0);
}
编译:gcc -o delta_tm delta_tm.c –lm
执行:./delta_tm 结果显示:Used Time: 0.226031
③系统计时器
• Linux 为每个进程提供了 3 个内部间隔计时器:
• ITIMER_REAL:实时时钟,到时产生 SIGALRM 信号;
• ITIMER_VIRTUAL:虚拟时钟,只在进程运行时才运行,到时
产生 SIGVTALRM 信号;
• ITIMER_PROF:形象时钟,在进程运行时和在系统代表进程运
行时都运行,到时产生 SIGPROF 信号。 • 具体的操作函数是:
#include <sys/time.h>
int getitimer(int which, struct itimerval *value);
int setitimer(int which, struct itimerval *newval, struct itimerval *oldval);
struct itimerval { struct timeval it_interval;
struct timeval it_value; }
getitimer 函数得到间隔计时器的时间值,保存在 value 中。
setitimer 函数设置间隔计时器的时间值为 newval,并将旧值
保存在 oldval 中。 which 表示使用三个计时器中的哪一个。
itimerval 结构中的 it_value 是减少的时间,当这个值为 0 的
时候就发出相应的信号了. 然后设置为 it_interval 值。
• ITIMER_PROF 计时器的初始化示例:
void init_time()
{ struct itimerval value;
value.it_value.tv_sec=2;
value.it_value.tv_usec=0;
value.it_interval=value.it_value;
setitimer(ITIMER_PROF,&value,NULL);
}
四、 信号编程
• 信号机制是在软件层次上对中断机制的模拟。从概念上讲,
一个进程收到一个信号和一个处理器接受到一个中断请求其处理过程是类似的(中断当前操作、转入信号处理程序)。
• 信号可以来自其它进程、外部事件、进程自身、或 kill 命令。
• 信号的最多种类数与处理器字长有关,i386 是 32 位,所以定
义了 32 种信号。
• POSIX 下,每个进程有一个信号掩码(Signal Mask)。简单地
说,信号掩码是一个“位图”,其中每一位都对应着一种信号。
为 1 表示在执行当前信号的处理程序期间相应的信号被暂时
屏蔽,以避免嵌套响应。
• sigaction 函数为 signum 指定的信号设置对应的处理程序:
#include<signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
sigaction 结构也在<signal.h>中定义:
struct sigaction{
void (*sa_handler)(int); //信号处理程序
sigset_t sa_mask; //处理期间应阻塞的信号集掩码
int sa_flags; //修正 sa_handler 行为的掩码
void (*sa_restorer)(void); //这行现已废弃不用
};
• 所以,对 SIGPROF 信号的服务和初始化代码如下:
void prompt_info(int signo) //信号服务程序
{ char *mes = "The time has lost 2 second.\n\a";
write(STDERR_FILENO, mes, strlen(mes)); }
void init_sigaction(void) //信号机制初始化
{ struct sigaction act;
act.sa_handler=prompt_info;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGPROF,&act,NULL);
}
• sigemptyset/sigfillset/sigaddset/sigdelset 用于初始化信号集。
关于信号集处理可参见:《GNU/Linux 编程指南(第二版)》
(张辉译 清华大学出版社 2002年 6月第 1版¥68.00)13.5节。
• 完整的 ITIMER_PROF 计时器应用程序示例:
//tmr.c
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
void prompt_info(int signo)
{ char *mes = "The time has lost 2 second.\a\n"; write(STDERR_FILENO, mes, strlen(mes));
}
void init_sigaction(void)
{ struct sigaction act;
act.sa_handler=prompt_info;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGPROF,&act,NULL);
}
void init_time()
{ struct itimerval value;
value.it_value.tv_sec=2;
value.it_value.tv_usec=0;
value.it_interval=value.it_value;
setitimer(ITIMER_PROF,&value,NULL);
}
int main()
{ init_sigaction(); init_time();
while(1);
}
编译:gcc –o tmr tmr.c
执行:./tmr & 显示:[1] 2471
表示:程序 tmr 在以作业号 1、进程号 2471 运行。
结果:该程序每隔两秒钟输出一个提示。
• 实验:
注释掉主函数 main()中用于初始化 ITIMER_PROF 计时器的语
句 init_time(); 用 kill 命令代其向进程发信号:
kill –27 2471 (27 是 SIGPROF 信号的值 )
结果也会出现报警提示。
(以此加深对 kill 命令的理解)
五、 驱动模块编程示例(示例程序 drvmod/hello.c)
安装模块:insmod hello.o [--force]
模块列表:lsmod
挟载模块:rmmod hello
六、 Linux 用户界面编程简介
• 终端控制:<termios.h> POSIX.1 定义的标准终端接口
• 屏幕控制:ncurses 经典的 Unix 屏幕控制库
• 底层图形:SVGAlib、framebuffer、MiniGUI
• X-Window 编程:Xlib API、X toolkit API 等
• gnome 编程:GTK、GTK+/gnome、Glade
• KDE 编程: QT、QT Designer、KDevelop
• 3D 图形编程:OpenGL、Mesa