发信人: soochowtang(SoochowTang) 
整理人: blainelinar(2001-08-25 00:27:44), 站内信件
 | 
 
 
作者:Hoyt   Email:[email protected]
 
 前言: 
     这篇文章介绍在LINUX下进行C语言编程所需要的基础知识.在这篇文章当中,我们将会学到以下内容: 
 源程序编译 
 Makefile的编写 
 程序库的链接 
 程序的调试 
 头文件和系统求助 
 
 --------------------------------------------------------------------------------
 1.源程序的编译
     在Linux下面,如果要编译一个C语言源程序,我们要使用GNU的gcc编译器. 下面我们以一个实例来说明如何使用gcc编译器. 
 假设我们有下面一个非常简单的源程序(hello.c): 
  int main(int argc,char **argv)
   {
 	printf("Hello Linux\n");
   }
 
 要编译这个程序,我们只要在命令行下执行: 
 gcc -o hello hello.c 
 gcc 编译器就会为我们生成一个hello的可执行文件.执行./hello就可以看到程序的输出结果了.命令行中 gcc表示我们是用gcc来编译我们的源程序,-o 选项表示我们要求编译器给我们输出的可执行文件名为hello 而hello.c是我们的源程序文件. 
 gcc编译器有许多选项,一般来说我们只要知道其中的几个就够了. -o选项我们已经知道了,表示我们要求输出的可执行文件名. -c选项表示我们只要求编译器输出目标代码,而不必要输出可执行文件. -g选项表示我们要求编译器在编译的时候提供我们以后对程序进行调试的信息. 
 知道了这三个选项,我们就可以编译我们自己所写的简单的源程序了,如果你想要知道更多的选项,可以查看gcc的帮助文档,那里有着许多对其它选项的详细说明. 
 2.Makefile的编写
     假设我们有下面这样的一个程序,源代码如下: 
 
 /*  main.c */
  #include "mytool1.h"
  #include "mytool2.h"
 
  int main(int argc,char **argv)
  {
   mytool1_print("hello");
   mytool2_print("hello");
  }
 
 /*  mytool1.h  */
 #ifndef _MYTOOL_1_H
 #define _MYTOOL_1_H
 
  void mytool1_print(char *print_str);
 
 #endif
 
 /*  mytool1.c  */
  #include "mytool1.h"
  void mytool1_print(char *print_str)
  {
    printf("This is mytool1 print %s\n",print_str);
  }
 
 /* mytool2.h */
 #ifndef _MYTOOL_2_H
 #define _MYTOOL_2_H
 
  void mytool2_print(char *print_str);
 
 #endif
   
 /*  mytool2.c  */
  #include "mytool2.h"
  void mytool2_print(char *print_str)
  {
    printf("This is mytool2 print %s\n",print_str);
  }
 
 
 当然由于这个程序是很短的我们可以这样来编译 
 gcc -c main.c 
 gcc -c mytool1.c 
 gcc -c mytool2.c 
 gcc -o main main.o mytool1.o mytool2.o 
 这样的话我们也可以产生main程序,而且也不时很麻烦.但是如果我们考虑一下如果有一天我们修改了其中的一个文件(比如说mytool1.c)那么我们难道还要重新输入上面的命令?也许你会说,这个很容易解决啊,我写一个SHELL脚本,让她帮我去完成不就可以了.是的对于这个程序来说,是可以起到作用的.但是当我们把事情想的更复杂一点,如果我们的程序有几百个源程序的时候,难道也要编译器重新一个一个的去编译? 
 为此,聪明的程序员们想出了一个很好的工具来做这件事情,这就是make.我们只要执行以下make,就可以把上面的问题解决掉.在我们执行make之前,我们要先编写一个非常重要的文件.--Makefile.对于上面的那个程序来说,可能的一个Makefile的文件是: 
 #   这是上面那个程序的Makefile文件
 main:main.o mytool1.o mytool2.o
 	gcc -o main main.o mytool1.o mytool2.o
 main.o:main.c mytool1.h mytool2.h
 	gcc -c main.c
 mytool1.o:mytool1.c mytool1.h
 	gcc -c mytool1.c
 mytool2.o:mytool2.c mytool2.h
 	gcc -c mytool2.c
 
 有了这个Makefile文件,不过我们什么时候修改了源程序当中的什么文件,我们只要执行make命令,我们的编译器都只会去编译和我们修改的文件有关的文件,其它的文件她连理都不想去理的. 
 下面我们学习Makefile是如何编写的. 
 在Makefile中也#开始的行都是注释行.Makefile中最重要的是描述文件的依赖关系的说明.一般的格式是: 
 target:	components
 TAB	rule
 
 第一行表示的是依赖关系.第二行是规则. 
 比如说我们上面的那个Makefile文件的第二行 
 main:main.o mytool1.o mytool2.o 
 表示我们的目标(target)main的依赖对象(components)是main.o mytool1.o mytool2.o 当倚赖的对象在目标修改后修改的话,就要去执行规则一行所指定的命令.就象我们的上面那个Makefile第三行所说的一样要执行 gcc -o main main.o mytool1.o mytool2.o 注意规则一行中的TAB表示那里是一个TAB键 
 Makefile有三个非常有用的变量.分别是$@,$^,$<代表的意义分别是: 
 $@--目标文件,$^--所有的依赖文件,$<--第一个依赖文件. 
 如果我们使用上面三个变量,那么我们可以简化我们的Makefile文件为: 
 # 这是简化后的Makefile
 main:main.o mytool1.o mytool2.o
 	gcc -o $@ $^
 main.o:main.c mytool1.h mytool2.h
 	gcc -c $<
 mytool1.o:mytool1.c mytool1.h
 	gcc -c $<
 mytool2.o:mytool2.c mytool2.h
 	gcc -c $<
 
 经过简化后我们的Makefile是简单了一点,不过人们有时候还想简单一点.这里我们学习一个Makefile的缺省规则 
 .c.o:
 	gcc -c $<
 
 这个规则表示所有的 .o文件都是依赖与相应的.c文件的.例如mytool.o依赖于mytool.c这样Makefile还可以变为: 
 # 这是再一次简化后的Makefile
 main:main.o mytool1.o mytool2.o
 	gcc -o $@ $^
 .c.o:
 	gcc -c $<
 
 好了,我们的Makefile 也差不多了,如果想知道更多的关于Makefile规则可以查看相应的文档. 
 3.程序库的链接 
     试着编译下面这个程序 
 
 /* temp.c */
   #include <math.h>
   
   int main(int argc,char **argv)
    {
          double value;
 	 printf("Value:%f\n",value);	
    }
 
 这个程序相当简单,但是当我们用 gcc -o temp temp.c 编译时会出现下面所示的错误. 
 /tmp/cc33Kydu.o: In function `main':
 /tmp/cc33Kydu.o(.text+0xe): undefined reference to `log'
 collect2: ld returned 1 exit status
 
 出现这个错误是因为编译器找不到log的具体实现.虽然我们包括了正确的头文件,但是我们在编译的时候还是要连接确定的库.在Linux下,为了使用数学函数,我们必须和数学库连接,为此我们要加入 -lm 选项. gcc -o temp temp.c -lm这样才能够正确的编译.也许有人要问,前面我们用printf函数的时候怎么没有连接库呢?是这样的,对于一些常用的函数的实现,gcc编译器会自动去连接一些常用库,这样我们就没有必要自己去指定了. 有时候我们在编译程序的时候还要指定库的路径,这个时候我们要用到编译器的 -L选项指定路径.比如说我们有一个库在 /home/hoyt/mylib下,这样我们编译的时候还要加上 -L/home/hoyt/mylib.对于一些标准库来说,我们没有必要指出路径.只要它们在起缺省库的路径下就可以了.系统的缺省库的路径/lib /usr/lib /usr/local/lib 在这三个路径下面的库,我们可以不指定路径. 
 还有一个问题,有时候我们使用了某个函数,但是我们不知道库的名字,这个时候怎么办呢?很抱歉,对于这个问题我也不知道答案,我只有一个傻办法.首先,我到标准库路径下面去找看看有没有和我用的函数相关的库,我就这样找到了线程(thread)函数的库文件(libpthread.a). 当然,如果找不到,只有一个笨方法.比如我要找sin这个函数所在的库. 就只好用 nm -o /lib/*.so|grep sin>~/sin 命令,然后看~/sin文件,到那里面去找了. 在sin文件当中,我会找到这样的一行libm-2.1.2.so:00009fa0 W sin 这样我就知道了sin在 libm-2.1.2.so库里面,我用 -lm选项就可以了(去掉前面的lib和后面的版本标志,就剩下m了所以是 -lm). 如果你知道怎么找,请赶快告诉我,我回非常感激的.谢谢! 
 4.程序的调试 
     我们编写的程序不太可能一次性就会成功的,在我们的程序当中,会出现许许多多我们想不到的错误,这个时候我们就要对我们的程序进行调试了. 
 最常用的调试软件是gdb.如果你想在图形界面下调试程序,那么你现在可以选择xxgdb.记得要在编译的时候加入 -g选项.关于gdb的使用可以看gdb的帮助文件.由于我没有用过这个软件,所以我也不能够说出如何使用. 不过我不喜欢用gdb.跟踪一个程序是很烦的事情,我一般用在程序当中输出中间变量的值来调试程序的.当然你可以选择自己的办法,没有必要去学别人的.现在有了许多IDE环境,里面已经自己带了调试器了.你可以选择几个试一试找出自己喜欢的一个用. 
 
 5.头文件和系统求助 
     有时候我们只知道一个函数的大概形式,不记得确切的表达式,或者是不记得着函数在那个头文件进行了说明.这个时候我们可以求助系统. 
 比如说我们想知道fread这个函数的确切形式,我们只要执行 man fread 系统就会输出着函数的详细解释的.和这个函数所在的头文件<stdio.h>说明了. 如果我们要write这个函数的说明,当我们执行man write时,输出的结果却不是我们所需要的. 因为我们要的是write这个函数的说明,可是出来的却是write这个命令的说明.为了得到write的函数说明我们要用 man 2 write. 2表示我们用的write这个函数是系统调用函数,还有一个我们常用的是3表示函数是C的库函数. 
 记住不管什么时候,man都是我们的最好助手. 
 
 
 --------------------------------------------------------------------------------
 好了,这一章就讲这么多了,有了这些知识我们就可以进入激动人心的Linux下的C程序探险活动. 
 Linux下进程的创建
 
 
 作者:Hoyt    Email:[email protected] 
 
 前言: 
     这篇文章是用来介绍在Linux下和进程相关的各个概念.我们将会学到: 
 进程的概念 
 进程的身份 
 进程的创建 
 守护进程的创建 
 
 --------------------------------------------------------------------------------
 1。进程的概念 
     Linux操作系统是面向多用户的.在同一时间可以有许多用户向操作系统发出各种命令.那么操作系统是怎么实现多用户的环境呢? 在现代的操作系统里面,都有程序和进程的概念.那么什么是程序,什么是进程呢? 通俗的讲程序是一个包含可以执行代码的文件,是一个静态的文件.而进程是一个开始执行但是还没有结束的程序的实例.就是可执行文件的具体实现. 一个程序可能有许多进程,而每一个进程又可以有许多子进程.依次循环下去,而产生子孙进程. 当程序被系统调用到内存以后,系统会给程序分配一定的资源(内存,设备等等)然后进行一系列的复杂操作,使程序变成进程以供系统调用.在系统里面只有进程没有程序,为了区分各个不同的进程,系统给每一个进程分配了一个ID(就象我们的身份证)以便识别. 为了充分的利用资源,系统还对进程区分了不同的状态.将进程分为新建,运行,阻塞,就绪和完成五个状态. 新建表示进程正在被创建,运行是进程正在运行,阻塞是进程正在等待某一个事件发生,就绪是表示系统正在等待CPU来执行命令,而完成表示进程已经结束了系统正在回收资源. 关于进程五个状态的详细解说我们可以看《操作系统》上面有详细的解说。 
 2。进程的标志 
     上面我们知道了进程都有一个ID,那么我们怎么得到进程的ID呢?系统调用getpid可以得到进程的ID,而getppid可以得到父进程(创建调用该函数进程的进程)的ID. 
 
 	#include  <unistd>
 
 	pid_t	getpid(void);
 	pid_t	getppid(void);
 
 进程是为程序服务的,而程序是为了用户服务的.系统为了找到进程的用户名,还为进程和用户建立联系.这个用户称为进程的所有者.相应的每一个用户也有一个用户ID.通过系统调用getuid可以得到进程的所有者的ID.由于进程要用到一些资源,而Linux对系统资源是进行保护的,为了获取一定资源进程还有一个有效用户ID.这个ID和系统的资源使用有关,涉及到进程的权限. 通过系统调用geteuid我们可以得到进程的有效用户ID. 和用户ID相对应进程还有一个组ID和有效组ID系统调用getgid和getegid可以分别得到组ID和有效组ID 
 	#include  <unistd>
 	#include  <sys/types.h>
 	
 	uid_t	getuid(void);
 	uid_t	geteuid(void);
 
 	gid_t	getgid(void);
 	git_t	getegid(void);	
 
 有时候我们还会对用户的其他信息感兴趣(登录名等等),这个时候我们可以调用getpwuid来得到. 
 	struct passwd {
 		char 	*pw_name;	/*	登录名称	*/
 		char 	*pw_passwd;	/*	登录口令	*/
 		uid_t	pw_uid;		/*	用户ID		*/
 		gid_t	pw_gid;		/*	用户组ID	*/
 		char 	*pw_gecos;	/*	用户的真名	*/
 		char	*pw_dir;	/*	用户的目录	*/
 		char	*pw_shell;	/*	用户的SHELL	*/
 	};
 
 	#include  <pwd.h>
 	#include  <sys/types.h>
 	
 	struct passwd *getpwuid(uid_t uid);
 
 下面我们学习一个实例来实践一下上面我们所学习的几个函数: 
 #include <unistd.h>
 #include <pwd.h>
 #include <sys/types.h>
 #include <stdio.h>
 
 int main(int argc,char **argv)
 {
  pid_t my_pid,parent_pid;
  uid_t my_uid,my_euid;
  gid_t my_gid,my_egid;
  struct passwd *my_info;
 
  my_pid=getpid();
  parent_pid=getppid();
  my_uid=getuid();
  my_euid=geteuid();
  my_gid=getgid();
  my_egid=getegid();
  my_info=getpwuid(my_uid);
  
  printf("Process ID:%ld\n",my_pid);
  printf("Parent  ID:%ld\n",parent_pid);
  printf("User	 ID:%ld\n",my_uid);
  printf("Effective User ID:%ld\n",my_euid);
  printf("Group   ID:%ld\n",my_gid);
  printf("Effective Group ID:%ld\n",my_egid):
 
  if(my_info)
  {
    printf("My Login Name:%s\n" ,my_info->pw_name);
    printf("My Password	:%s\n" ,my_info->pw_passwd);
    printf("My User  ID	:%ld\n",my_info->pw_uid);
    printf("My Group ID	:%ld\n",my_info->pw_gid);
    printf("My Real  Name:%s\n" ,my_info->pw_gecos);
    printf("My Home Dir  :%s\n", my_info->pw_dir);
    printf("My Work Shell:%s\n", my_info->pw_shell);
  }
 }
 
 3。进程的创建 
     创建一个进程的系统调用很简单.我们只要调用fork函数就可以了. 
 
 	#include <unistd.h>
 	
 	pid_t   fork();
 
 当一个进程调用了fork以后,系统会创建一个子进程.这个子进程和父进程不同的地方只有他的进程ID和父进程ID,其他的都是一样.就象符进程克隆(clone)自己一样.当然创建两个一模一样的进程是没有意义的.为了区分父进程和子进程,我们必须跟踪fork的返回值. 当fork掉用失败的时候(内存不足或者是用户的最大进程数已到)fork返回-1,否则fork的返回值有重要的作用.对于父进程fork返回子进程的ID,而对于fork子进程返回0.我们就是根据这个返回值来区分父子进程的. 父进程为什么要创建子进程呢?前面我们已经说过了Linux是一个多用户操作系统,在同一时间会有许多的用户在争夺系统的资源.有时进程为了早一点完成任务就创建子进程来争夺资源. 一旦子进程被创建,父子进程一起从fork处继续执行,相互竞争系统的资源.有时候我们希望子进程继续执行,而父进程阻塞直到子进程完成任务.这个时候我们可以调用wait或者waitpid系统调用. 
 	#include <sys/types.h>
 	#include <sys/wait.h>
 	
 	pid_t	wait(int *stat_loc);
 	pid_t	waitpid(pid_t pid,int *stat_loc,int options);
 
 wait系统调用会使父进程阻塞直到一个子进程结束或者是父进程接受到了一个信号.如果没有父进程没有子进程或者他的子进程已经结束了wait回立即返回.成功时(因一个子进程结束)wait将返回子进程的ID,否则返回-1,并设置全局变量errno.stat_loc是子进程的退出状态.子进程调用exit,_exit 或者是return来设置这个值. 为了得到这个值Linux定义了几个宏来测试这个返回值. 
 WIFEXITED:判断子进程退出值是非0 
 WEXITSTATUS:判断子进程的退出值(当子进程退出时非0). 
 WIFSIGNALED:子进程由于有没有获得的信号而退出. 
 WTERMSIG:子进程没有获得的信号号(在WIFSIGNALED为真时才有意义). 
 waitpid等待指定的子进程直到子进程返回.如果pid为正值则等待指定的进程(pid).如果为0则等待任何一个组ID和调用者的组ID相同的进程.为-1时等同于wait调用.小于-1时等待任何一个组ID等于pid绝对值的进程. stat_loc和wait的意义一样. options可以决定父进程的状态.可以取两个值 WNOHANG:父进程立即返回当没有子进程存在时. WUNTACHED:当子进程结束时waitpid返回,但是子进程的退出状态不可得到. 
 父进程创建子进程后,子进程一般要执行不同的程序.为了调用系统程序,我们可以使用系统调用exec族调用.exec族调用有着5个函数. 
 	#include <unistd.h>
 
 	int execl(const char *path,const char *arg,...);
 	int execlp(const char *file,const char *arg,...);
  	int execle(const char *path,const char *arg,...);
 	int execv(const char *path,char *const argv[]);
 	int execvp(const char *file,char *const argv[]):
 
 exec族调用可以执行给定程序.关于exec族调用的详细解说可以参考系统手册(man execl). 下面我们来学习一个实例.注意编译的时候要加 -lm以便连接数学函数库. 
 
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <stdio.h>
 #include <errno.h>
 #include <math.h>
 
 void main(void)
 {
  pid_t child;
  int status;
 
  printf("This will demostrate how to get child status\n");
  if((child=fork())==-1)
 	{
 		printf("Fork Error :%s\n",strerror(errno));
 		exit(1);
 	}
  else if(child==0)
 	{
 		int i;
 		printf("I am the child:%ld\n",getpid());
 		for(i=0;i<1000000;i++) sin(i);
 		i=5;
 		printf("I exit with %d\n",i);
 		exit(i);
 	}
 while(((child=wait(&status))==-1)&(errno==EINTR));
 if(child==-1)
 	printf("Wait Error:%s\n",strerror(errno));
 else if(!status)
 	printf("Child %ld terminated normally return status is zero\n",
 		child);
 else if(WIFEXITED(status))
 	printf("Child %ld terminated normally return status is %d\n",
 		child,WEXITSTATUS(status));
 else if(WIFSIGNALED(status))
 	printf("Child %ld terminated due to signal %d znot caught\n",
 		child,WTERMSIG(status));	 
 }
 
 strerror函数会返回一个指定的错误号的错误信息的字符串. 
 4。守护进程的创建 
     如果你在DOS时代编写过程序,那么你也许知道在DOS下为了编写一个常驻内存的程序我们要编写多少代码了.相反如果在Linux下编写一个"常驻内存"的程序却是很容易的.我们只要几行代码就可以做到. 实际上由于Linux是多任务操作系统,我们就是不编写代码也可以把一个程序放到后台去执行的.我们只要在命令后面加上&符号SHELL就会把我们的程序放到后台去运行的. 这里我们"开发"一个后台检查邮件的程序.这个程序每个一个指定的时间回去检查我们的邮箱,如果发现我们有邮件了,会不断的报警(通过机箱上的小喇叭来发出声音). 后面有这个函数的加强版本加强版本 
 后台进程的创建思想: 首先父进程创建一个子进程.然后子进程杀死父进程(是不是很无情?). 信号处理所有的工作由子进程来处理. 
 
 
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <stdio.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <signal.h>
 
 /*  Linux  的默任个人的邮箱地址是 /var/spool/mail/用户的登录名 */
 
 #define  MAIL	"/var/spool/mail/hoyt"
 
 /*	睡眠10秒钟	*/
 		
 #define  SLEEP_TIME	10
 
 main(void)
 {
  pid_t child;
 
  if((child=fork())==-1)
 	{
 		printf("Fork Error:%s\n",strerror(errno));
 		exit(1);
 	}
  else if(child>0)
 	while(1);
  if(kill(getppid(),SIGTERM)==-1)
 	{
  	printf("Kill Parent Error:%s\n",strerror(errno));
  	exit(1);
 	}	
  {
   int mailfd;
 
   while(1)
    {
 	if((mailfd=open(MAIL,O_RDONLY))!=-1)
 	  {
 	    fprintf(stderr,"%s","\007");
 	    close(mailfd);
 	 }	       
 	sleep(SLEEP_TIME);
    }
  }
 }
 
 你可以在默认的路径下创建你的邮箱文件,然后测试一下这个程序.当然这个程序还有很多地方要改善的.我们后面会对这个小程序改善的,再看我的改善之前你可以尝试自己改善一下.比如让用户指定邮相的路径和睡眠时间等等.相信自己可以做到的.动手吧,勇敢的探险者. 
 好了进程一节的内容我们就先学到这里了.进程是一个非常重要的概念,许多的程序都会用子进程.创建一个子进程是每一个程序员的基本要求! 
 --------------------------------------------------------------------------------
 
 </code> | 
 
 
 |