什么是Linux

发布时间:2011-11-27 12:12:27

 

什么是Linux

Linux是一个类UNIX内核的可以自由发布的实现版本,是一个操作系统的底层核心。几乎所有为UNIX编写的程序都可以在Linux上编译运行。

Linux是由赫尔辛基大学的Linus Torvalds开发的,期间得到了因特网上广大UNIX程序员的帮助。它最初只是受Andy Tanenbaum教授的Minix(一个小型类UNIX系统)启发而开发的个人爱好的程序,但后来逐步发展成为一个拥有自己版权的完整系统。其目的是,保证Linux除包含自由发布的代码外,不会集成任何私有代码

 

GNU项目和自由软件基金会

Linux社团支持自由软件的概念,即软件本身不应受限,它们应该遵守GNU通用公共许可证(GPL)。虽然获得软件可能要支付一定的费用,但此后就可以随意使用,并且它们通常是以源代码的形式发布的。

GPL规则限制下,所有基于这种概念开发的软件都应遵循GPL。大家可以在http://www.gnu.org上找到更多关于自由软件的概念。

C语言编译器

这里我们使用GNU C编译器,简称为gcc。因为它随Linux的发行版一起提供,并且它支持ANSI C的标准语法。在http://www.gnu.org上可以获取gcc软件包。

Linux下的C编译器使用GCC,由于历史的原因,在POSIX兼容的操作系统中,C编译器都叫cc,所以Linux下也有一个cc命令,它是一个到gcc的软链接。

 

Linux程序

Linux应用程序表现为两种特殊类型的文件:可执行文件和脚本文件。

可执行文件是计算机可以直接运行的程序,它们相当于Windows中的exe文件。

脚本文件是一组指令的集合,这些指令将由另一个程序(解释器,比如shell或者perl)来执行,它们相当于Windows中的bat文件、cmd文件或解释执行的BASIC程序。

Windows相比,Linux程序并不要求可执行程序或脚本具有特殊的文件名或扩展名。当登录Linux系统时,我们与一个shell程序(通常是bash)进行交互,它像Windows中的命令提示窗口一样运行程序。在当前环境下,必定有一组环境变量与之匹配,其中PATH变量指明了当前可以自动搜索的目录:当需要执行的程序在PATH指定的目录中时,你将不需指明待执行程序的全路径(除非有同名程序存在);否则必须指定需要执行程序的路径(相对路径或者绝对路径)。

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11"

Linux使用正斜线(/分割文件名里的目录名,而不是像Windows那样使用反斜线(\),此外,冒号分隔每个路径名(Windows是分号)

 

开发系统导引

 

应用程序 应用程序通常存放在系统为之保留的特定目录中。

/bin,该路径存放系统启动时需要使用的程序;

/usr/bin,标准程序,系统为正常使用提供的程序(包括用于程序开发的工具);

/usr/local/bin,该路径存放本地安装的程序,系统管理员为某个特定的主机或本地网络添加的程序,通常可在目录/usr/local/bin/opt中找到。

建议:在/usr/local目录结构下编译、运行自己的程序,并访问必须的文件。

头文件

/usr/include,在编译程序时,编译器会自动查找该目录。那些依赖于特定Linux版本的头文件通常可以在目录/usr/include/sys/usr/include/linux中找到。

可以使用-I标志,来包含保存在子目录或非标准位置中的头文件,比如:$ gcc –I/usr/openwin/include fred.c。它指示编译器不仅在标准位置,也在/usr/openwin/include目录中查找fred.c中包含的头文件。

提示:可以使用grep命令来搜索包含某些特定定义和函数原型的头文件。

库文件 库是一组预先编译好的函数的集合,这些函数都是按照可以重用的原则编写的。标准库文件一般存储在/lib/usr/lib目录中。

库文件的名字总是以lib开头,随后的部分指明这是什么库(比如,libm就代表了数学库)。文件名的最后部分以.开始,然后给出库文件的类型:.a代表传统的静态函数库;.so代表共享函数库。

库文件,是一组已编译的函数集合,可方便我们重用代码。默认存放在/lib/usr/lib目录。库文件可分为静态和共享两类。

 .a,静态库文件。使用静态库将会把所有的库代码引入程序,占用更多的磁盘空间和内存空间,所以一般建议使用共享库。

 .so,共享库文件。使用共享库的程序不包含库代码,只在程序运行才调用共享库中的代码。

在编译时可用包含路径的库文件名或用-l参数指定使用的库文件,/usr/lib/libm.a等价于-lm。如:

 gcc -o hello hello.c /usr/lib/libm.a

或用-l参数写成

 gcc -o hello hello.c -lm

如果我们要使用的库文件不在默认位置,在编译程序时可用-L参数指定库文件的路径。下面例子使用了/usr/hello/lib目录下的libhello库文件:

 gcc -o hello -L/usr/hello/lib hello.c –lhello

创建和使用静态库

分别创建两个函数,函数a的内容如下:

#include <stdio.h>

void a(char *arg)

{

printf("function a,hello world %s\n",arg);

}

函数b的内容如下:

#include <stdio.h>

void b(int arg)

{

printf("function b,hello world %d\n",arg);

}

接着,生成两个对象文件。

debian:~/c# gcc -c a.c b.c

debian:~/c# ls *.o

a.o b.o

最后,用ar归档命令把生成的对象文件打包成一个静态库libhello.a

debian:~/c# ar crv libhello.a a.o b.o

r - a.o

r - b.o

为我们的静态库定义一个头文件lib.h,包含这两个函数的定义。

/*

* this is a header file.

*/

void a(char *arg);

void b(int arg);

创建jims.c程序,内容如下:

#include "lib.h"

int main()

{

a("jims.yang");

b(3);

exit(0);

}

利用静态链接库编译程序

debian:~/c# gcc -c jims.c

debian:~/c# gcc -o jims jims.o libhello.a

debian:~/c# ./jims

function a,hello world jims.yang

function b,hello world 3

debian:~/c#

gcc -o jims jims.o libhello.a也可以写成gcc -o jims jims.o -L -lhello

预处理,在程序开头以“#”开头的命令就是预处理命令,它在语法扫描和分析法时被预处理程序处理。预处理有以下几类:

宏定义,用#define指令定义。如:#define BUFFER 1024。取消宏定义用#undef指令。宏还可带参数,如:

#define BUF(x) x*3

包含头文件,用#include指令,可把包含的文件代码插入当前位置。如:

#include <stdio.h>

包含的文件可以用尖括号,也可用双引号,如:

#include "stdio.h"

不同之处是,使用尖括号表示在系统的包含目录(/usr/include)下查找该文件,而双引号表示在当前目录下查找包含文件。

条件编译,格式如下:

格式一,如果定义了标识符,则编译程序段1,否则编译程序段2

#ifdef 标识符

程序段1

#else

程序段2

#endif

格式二,如果定义了标识符,则编译程序段2,否则编译程序段1,与格式一相反:

#ifndef 标识符

程序段1

#else

程序段2

#endif

格式三,常量表达式为真则编译程序段1,否则编译程序段2

#if 常量表达式

程序段1

#else

程序段2

#endif

使用gcc编译程序时,要经过四个步骤。

Shell程序设计

 

管道和重定向

重定向分为

输入重定向’<’

输出重定向’>’

附加输出重定向’>>’

提示:默认情况下,如果使用>操作符把输出重定向到一个文件而该文件已经存在时,它的内容将被覆盖;如果想改变该默认行为,可以使用set –C命令设置noclobber选项。

技巧:可以使用UNIX的通用“回收站”/dev/null来有效的丢弃输出信息,比如:

$ kill –l 1234 >/dev/null 2>&1          (&把标准错误重定向到标准输出同样的地方)

使用管道操作符|来连接进程。LinuxMS-DOS不同,在Linux下通过管道连接的进程可以同时运行,并且随着数据流在它们之间的传递可以自动地进行协调。举个例子:

$ grep –l POSIX* | more 它将输出包含POSIX字符串的文件名。实际上,上述命令还有另外两种编写方式:

$ more `grep –l POSIX*` 或者 $ more $(grep –l POSIX*)

 

Shell脚本程序

 

运行脚本程序有两个办法:一种是在命令行上直接输入命令PATH=$PATH:.或者编辑.bash_profile文件,将刚才的命令添加在文件的末尾,然后退出登录再重新登录进来;另一种就是在保存脚本程序的目录中先键入./再输入脚本命令,如此做的作用是把脚本程序的完整的相对路径告诉Shell

建议:考虑到系统的安全性,最好的办法是在当前目录中的所有命令前都加上一个./

推荐:如果你想深入地学习使用Shell,可以去ChinaUnixShell子论坛

 

Find命令

只要你是在UNIX或者类UNIX环境下,一个必不可少的命令就是find命令。它的功能是查找文件,比如说,我们在编译某个C程序时,发现错误提示说它不识别STDIN;那么,很明显的,你的C程序缺少某个头文件,但是如何才能知道它是在哪个头文件中定义的呢?一个非常有效地办法就是结合grep使用find命令:

$ find /usr/include –name “*.h” | xargs grep “STDIN”

上面命令先在/usr/include目录下搜索所有包含了.h的文件,继而使用grep命令对这些头文件查找STDIN字符串。当然,你可能说我知道STDIN定义在,不用这么麻烦!但在实际的研发工作中,我们经常会在某个并不熟悉的环境(比如设备驱动程序的开发)下编写程序,我们会不可避免的查找某个函数或者宏定义的来源。

提示:find命令学习,使用man文档。很多情况下,比如不熟悉的命令或者函数都可以从man文档中得到解答。

文件操作

 Linux中,一切(或几乎一切)都是文件。

文件和设备

硬件设备在Linux操作系统中通常被映射为文件。可以使用mount命令加载CD-ROMWindows下的文件系统或者其他的设备。

UNIXLinux中比较重要的设备文件有三个:

1. dev/console 该设备代表系统控制台,错误信息和诊断信息通常会被发送到这个设备上。在现代的工作站和Linux上,它通常是“活跃”的虚拟控制台;而在X窗口系统中,它会是屏幕上一个特殊的控制台窗口。

2. dev/tty 如果一个进程有控制终端的话,那么特殊文件/dev/tty就是这个控制终端(键盘、显示屏或者窗口)的别名(逻辑设备)。注意:虽然/dev/console设备只有一个,但通过/dev/tty却能够访问许多不同的物理设备。

3. /dev/null 这是空设备,所有写向该设备的输出都将被丢弃。提示:还有一个特殊设备/dev/zero经常被用到,它的作用是以内容为null字节的源文件来来创建零长度文件。它经常用在dd命令的if参数中。

Linux系统内所有东西都是以文件的形式来表示的,除一般的磁盘文件外,还有设备文件,如硬盘、声卡、串口、打印机等。设备文件又可分为字符设备文件(character devices)和块设备文件(block devices)。使用man hier命令可以查看Linux文件系统的分层结构。文件的处理方法一般有五种,分别是:

open,打开一个文件或设备。

close,关闭一个打开的文件或设备。

read,从一个打开的文件或者设备中读取信息。

write,写入一个文件或设备。

ioctl,把控制信息传递给设备驱动程序。

这些都是低级,没有缓冲的文件操作函数,在实际程序开发中较少使用,一般我们使用标准I/O函数库来处理文件操作。如:fopenfclosefreadfwritefflush等。在使用标准I/O库时,需用到stdio.h头文件。

一些常用的文件和目录维护函数:chmodchownunlinklinksymlinkmkdirrmdirchdirgetcwdopendirclosedirreaddirtelldirseekdir等。fcntl用于维护文件描述符,mmap用于分享内存。

创建文档并输入信息的示例代码:

#include

main(void)

{

FILE *fp1;

char c;

fp1 = fopen("text.txt","w");

while ((c = getchar())!= '\n')

putc(c,fp1);

fclose(fp1);

}

 

扫描目录

与目录操作相关的函数在dirent.h头文件中声明。除了opendirclosedirreaddir这三个常见的函数外,还有两个函数:telldirseekdir

telldir函数的返回值记录了一个目录流里的当前位置;接着,我们可以在随后的seekdir调用中利用这个值将目录指针重置到之前的位置。

 

Linux系统上一个常见的问题就是对目录进行扫描,也就是确定一个特定目录下存放的文件。这里给出一个扫描当前目录下(包含子目录)所有文件的实例printdir

显示路径的示例代码

#include

#include

#include

#include <string.h>

#include

#include

int main(int argc, char *argv[])

{

char *topdir = ".";

if (argc >= 2)

topdir = argv[1];

printf("Directory scan of %s\n", topdir);

printdir(topdir,0);

printf("done.\n");

exit(0);

}

printdir(char *dir, int depth)

{

DIR *dp;

struct dirent *entry;

struct stat statbuf;

if((dp = opendir(dir)) == NULL)

{

fprintf(stderr,"cannot open directory:%s\n",dir);

return;

}

chdir(dir);

while((entry = readdir(dp)) != NULL)

{

lstat(entry->d_name,&statbuf);

if(S_ISDIR(statbuf.st_mode))

{

if(strcmp(".",entry->d_name) == 0 || strcmp("..",entry->d_name) == 0)

continue;

printf("%*s%s/\n",depth,"",entry->d_name);

printdir(entry->d_name,depth+4);

}

else printf("%*s%s\n",depth,"",entry->d_name);

}

chdir("..");

closedir(dp);

}

提示:为了在输出时对于不同层次的目录有缩进,这里使用了可变字段宽度%*s。其中*可以由一个整形值来指定,代表了在输出后面字符串时所要求的宽度。

 

/proc文件系统

Linux提供了一个特殊的文件系统procfs,它通常表现为/proc目录。该目录中包含了许多特殊文件以允许对驱动和内核信息进行高层访问。只要应用程序有正确的访问权限,它们就可以通过读写这些文件来获得信息或设置参数。

/proc目录中的文件会随系统的不同而不同,当Linux版本中有更多的驱动和设施支持procfs文件系统时,该目录中就会包含更多的文件。不过,该目录下有许多东西是在任何Linux系统中都存在的。

大多情况下,只需要直接读取这些文件就可以获取信息。比如/proc/cpuinfo/proc/meminfo/proc/version/proc/net/sockstat就分别给出了CPU、内存、Linux版本和网络套接字的信息。

其实,/proc目录下的有些文件不但可以读取,还可以修改。比如说,系统中所有运行的程序同时能够打开的文件总数是Linux内核的一个参数。它的值可以从/proc/sys/fs/file-max文件得到;同样地,你也可以直接修改该文件来更改可以直接打开的文件总数。

提示:对/proc目录中文件进行写操作需要超级用户的权限。在修改数据时一定要小心,写入不当的值很可能会导致严重的后果。

    /proc目录中还有一类文件以数字命名。比如,当我们使用ps命令查看当前正在运行的进程时,会显示每个进程的PID。每个进程都会对应/proc目录下一个以该pid值命名的文件。如果你要查看该进程的具体信息,可以直接读取文件/proc/(pid)。在列出的文件中,cmdline文件会显示该进程由谁启动的;你可以使用cat命令或者od命令来查看。

 

 

Linux环境

Linux编写程序时,必须考虑到程序将在一个多任务环境中运行。这意味着在同一时间会有多个程序运行,它们共享内存、磁盘空间和CPU周期等机器资源。甚至同一程序也会有多个实例同时运行。最重要的是,这些程序能够互不干扰,了解他们的环境,并且能正确运行以避免冲突——例如试图与其他程序同时写同一个文件。 

程序参数

无论操作系统何时启动新程序,参数argcargv都被设置并传递给main。这些参数通常由其他程序提供,这个程序一般是shell,它请求操作系统启动该新程序。

注意Linuxshell一般会在设置argcargv之前对文件名参数进行通配符扩展,而MS-DOSshell则期望程序接受带通配符的参数并执行它们自己的通配符扩展。

命令行参数在向程序传递信息方面是很有用的。许多工具程序都是有命令行参数来改变程序的行为或设置选项。这些参数大多以短横线(-)开头;不带后续参数的选项还可在一个短横线后归并到一起。

当需要在自己的程序中处理这些命令行参数时,我们需要自己对这些参数解析从而判断出有效的参数。当参数个数很多时,这一过程将是非常繁琐的。实际上,X/Open规范定义了命令行选项的标准用法,同时定义了在C语言程序中提供命令行开关的标准编程接口:getopt函数。

getopt函数将传递给main程序的argcargv作为参数,同时接受一个选项指定字符串optstring,该字符串高速getopt哪些选项可用,以及每个选项是否有关联值。程序将循环调用getopt对选项参数进行处理,直到getopt返回-1时处理完毕。关于getopt的详细信息请查阅man手册。

我们在实际使用中可能会选择长参数,它以双短横线(--)表示。GNU C库包含了getopt的另一个版本,称为getopt_long,它能同时接受短参数和长参数。getopt_long函数getopt函数多了两个参数,一个被定义为option结构(它指定了函数可以接受的长参数和函数对应返回的值),另一个通常被置NULL

提示:在使用getopt_long函数时,除了要包含头文件getopt.h外,还需要把常量_GNU_SOURCE一同包含进来。

GNU官方手册

#include

#include

int main(int argc, char *argv[])

{

int opt;

while((opt=getopt(argc,argv,"if:lr")) != -1) /* 返回-1”表示已没选项需要处理。*/

{

switch(opt){

case 'i':

case 'l':

case 'r':

printf("option: %c\n", opt);

break;

case 'f':

printf("filename: %s\n", optarg); /*如果选项需要一个参数,则参数存放在外部变量optarg中。*/

break;

case ':':

printf("option needs a value \n"); /*“表示选项需要参数*/

break;

case '?':

printf("unknown option: %c\n", optopt); /*返回“?”表示无效的选项,并把无效的选项存放在外部变量optopt中。*/

break;

}

}

for(; optind < argc; optind++)

printf("argument: %s\n", argv[optind]);

}

 

无参数和void参数

在定义自己的程序时,当不需要传递参数时我们可能置参数列表为空或者填入void。那么这两种方式相同么?考虑下面两个函数

void foo1();

void foo2(void);

当我们使用foo1(1,2)调用foo1函数时编译器将不报任何错误,同时可以正常运行;它的执行结果与调用foo1()的结果一致。然而,当我们使用foo2(1,2)调用foo2函数时编译器将报错。因此,无参数和void参数使得函数显现出两种不同的行为。

实际上,void参数指定函数在调用时不能有任何参数;而无参数则没有对参数的传递做任何的规范,也就是说,你可以传递任何类型的任意个数的参数。

 

环境变量

bash shell中使用set命令可以列出Linux系统的环境变量,在C程序中我们也可以用putenv()getenv()函数来获取Linux系统的环境变量。这两个函数的声明如下:

char *getenv(const char *name);
int putenv(const char *string);

系统有一个environ变量记录了所有的系统变量。下面的示例代码可把environ的值显示同来。

#include

extern char **environ;

int main()
{
    char **env = environ;

    while(*env){
        printf("%s\n",*env);
        env++;
    }

环境变量是一把双刃剑,使用它的时候要小心!与命令行选项相比,它们对用户来说更加“隐蔽”,这样就使得调试变得更加困难。从某种意义上来说,环境变量就像全局变量一样,它们会改变程序的行为,产生不可预期的结果。

 

时间和日期

linux和其它unix一样,使用GMT197011子夜作为系统时间的开始,也叫UNIX纪元的开始。现在的时间表示为UNIX纪元至今经过的秒数。

#include

  time_t time(time_t *t);

我们可以使用time函数获取一个time_t类型的时间值,该值是从格林威治时间到当前时间点的秒数。函数localtime将一个time_t类型的时间值转换为tm结构,通过该结构可以清晰得了解当前的年、月、日、时、分等。函数mktime的功能则相反,它将一个tm结构的时间值转换为time_t类型。

#include
char *ctime(const time_t *timeval);

为了得到更“友好”的时间和日期表示,像date命令输出的那样,我们可以使用asctime函数和ctime函数。实际上,为了对时间和日期字符串的格式有更多的控制,Linux和现代的类UNIX系统提供了strftimestrptime函数。它很像是一个针对时间和日期的sprintfsscanf函数,具体的信息请查看man手册。

注意:编译包含了strptime函数的程序时,需要在包含time.h头文件的语句之前包含_XOPEN_SOURCE宏定义。

提示:我们在程序中经常使用sleep函数来完成指定时间的睡眠。实际上,sleep函数只能指定秒级的时间,如果要精确到微妙级(10-6s)可以使用usleep函数。除此之外,还有一个timeval结构可以完成微妙级的操作。

#include

#include

int main(void)

{

time_t time1;

(void)time(&time1);

printf("The date is: %s\n",ctime(&time1));

}

 

临时文件

很多情况下,程序会利用一些文件形式的临时存储手段。这些临时文件可能保存着一个计算的中间结果,也可能是关键操作的文件备份。

临时文件的用法很常见,但必须确保应用程序为临时文件选取的文件名是唯一的。GNU C提供了两个函数tmpnametmpfile来创建临时文件。详细情况请参考man手册。

提示:当需要创建临时文件且需要对其进行读写时,请优先考虑使用tmpfile函数。该函数同时创建和打开临时文件,这样就避免了使用tmpname函数时可能有另一个程序用同样的名字打开文件的风险。

mkstemp()函数创建临时文件。声明:

#include

  int mkstemp(char * template);

#include

int main(void)

{

char template[] = "template-XXXXXX";

int fp;

fp = mkstemp(template);

printf("template = %s\n", template);

close(fp);

}

 

用户及主机信息

程序能够通过检查环境变量和读取系统时钟来在很大程度上了解它所处的运行环境。除此之外,程序还可以发现它的使用者的相关信息。

函数getuidgetlogin分别获取程序关联的UID和与该关联ID相应的登录名。

实际上,系统文件/etc/passwd包含了一个用户账户数据库。要获取某个用户的信息,UNIX系统并不推荐直接对该系统文件读写,它定义了一组函数来提供一个标准二又有效地获取用户信息的编程接口getpwuidgetpwnam。它们均返回一个指向与某个用户对应的passwd结构指针。

提示:你可以对程序进行设置,让它们的运行看上去好像是由另一个用户启动的。当一个程序的SUID权限被置位时,它的运行就好像是由该可执行文件的属主启动的。一个典型的例子是su命令。

UNIX环境下,可以使用uname系统调用来获取主机信息。它将主机信息写入一个utsname结构。详细细节请查阅man手册。

获取用户信息。

声明:

#include

#include

struct passwd *getpwuid(uid_t uid); /* 根据uid返回用户信息 */

struct passwd *getpwnam(const char *name); /* 根据用户名返回用户信息 */

passwd结构体说明:

passwd Member Description

char *pw_name The user's login name

uid_t pw_uid The UID number

gid_t pw_gid The GID number

char *pw_dir The user's home directory

char *pw_gecos The user's full name

char *pw_shell The user's default shell

示例代码:

#include

#include

#include

#include

int main(void)

{

uid_t uid;

gid_t gid;

struct passwd *pw;

uid = getuid();

gid = getgid();

pw = getpwuid(uid);

printf("User is %s\n", getlogin());

printf("The uid is:%d\n", uid);

printf("The gid is:%d\n",gid);

printf("The pw struct:\n name=%s, uid=%d, gid=%d, home=%s,shell=%s\n", pw->pw_name, pw->pw_uid, pw->pw_gid, pw->pw_dir, pw->pw_shell);

}

gethostname()函数获取主机名。

函数声明:

#include

int gethostname(char *name, size_t namelen); /* 主机名返回给name变量 */

示例代码:

#include

#include

int main(void)

{

char computer[100];

int status;

status = gethostname(computer, 100);

printf("The status is %d\n", status);

printf("The hostname is: %s\n", computer);

}

uname()函数获取主机详细信息,就像shelluname命令返回的信息一样。

函数声明:

#include

int uname(struct utsname *name);

utsname结构体说明:

utsname Member Description

char sysname[] The operating system name

char nodename[] The host name

char release[] The release level of the system

char version[] The version number of the system

char machine[] The hardware type

示例代码:

#include

#include

#include

int main(void)

{

char computer[100];

int status;

struct utsname uts;

status = gethostname(computer,100);

printf("The computer's size is %d\n",sizeof(computer));

printf("The status is %d\n", status);

printf("The hostname is: %s\n", computer);

uname(&uts);

printf("The uname's information.\n uts.sysname=%s\n uts.machine=%s\n uts.nodename=%s\n uts.release=%s\n uts.version=%s\n", uts.sysname,uts.machine,uts.nodename,uts.release,uts.version);

}

日志

许多应用程序需要记录它们的活动。系统程序经常需要向控制台或日志文件写消息。这些消息能指示错误、警告或是与系统状态有关的一般信息。

提示:当程序短小时我们经常使用gdb工具来调试;但当程序比较庞大时,一个有效地调试手段就是使用日志功能,在日志中搜索出错信息。

UNIX规范为所有程序提供了一个接口,通过syslog函数来产生日志信息。这些信息往往被记录在日志文件/var/log/message中,部分调试信息可能记录在/var/log/debug中。

通过syslog函数向系统的日志文件发送的每条日志信息都有一个优先级。而且不同程序写入的日志信息并不能明显地区分开来。实际上,UNIX提供了几个函数来改变日志记录行为:openlogcloselogsetlogmask函数。有效地使用这三个函数可以在日志文件中与其他程序写入的日志区分开。关于详细信息请查阅man手册。

使用syslog()函数处理日志信息。

函数声明:

#include

void syslog(int priority, const char *message, arguments...);

priority参数的格式(severity level|facility code

示例:

LOG_ERR|LOG_USER

severity level

Priority Level Description

LOG_EMERG An emergency situation

LOG_ALERT High-priority problem, such as database corruption

LOG_CRIT Critical error, such as hardware failure

LOG_ERR Errors

LOG_WARNING Warning

LOG_NOTICE Special conditions requiring attention

LOG_INFO Informational messages

LOG_DEBUG Debug messages

facility value(转自syslog.h头文件):

/* facility codes */

#define LOG_KERN (0<<3) /* kernel messages */

#define LOG_USER (1<<3) /* random user-level messages */

#define LOG_MAIL (2<<3) /* mail system */

#define LOG_DAEMON (3<<3) /* system daemons */

#define LOG_AUTH (4<<3) /* security/authorization messages */

#define LOG_SYSLOG (5<<3) /* messages generated internally by syslogd */

#define LOG_LPR (6<<3) /* line printer subsystem */

#define LOG_NEWS (7<<3) /* network news subsystem */

#define LOG_UUCP (8<<3) /* UUCP subsystem */

#define LOG_CRON (9<<3) /* clock daemon */

#define LOG_AUTHPRIV (10<<3) /* security/authorization messages (private) */

#define LOG_FTP (11<<3) /* ftp daemon */

示例代码:

#include

#include

int main(void)

{

FILE *f;

f = fopen("abc","r");

if(!f)

syslog(LOG_ERR|LOG_USER,"test - %m\n");

}

上面的日志信息由系统自动给出,我们也可过滤日志信息。用到以下函数:

#include

void closelog(void);

void openlog(const char *ident, int logopt, int facility);

int setlogmask(int maskpri);

logopt参数的选项:

logopt Parameter Description

LOG_PID Includes the process identifier, a unique number allocated to each process by the system, in the messages.

LOG_CONS Sends messages to the console if they can’t be logged.

LOG_ODELAY Opens the log facility at first call to .

LOG_NDELAY Opens the log facility immediately, rather than at first log.

示例代码:

#include

#include

#include

int main(void)

{

int logmask;

openlog("logmask", LOG_PID|LOG_CONS, LOG_USER); /*日志信息会包含进程id*/

syslog(LOG_INFO, "informative message, pid=%d", getpid());

syslog(LOG_DEBUG,"debug message, should appear"); /*记录该日志信息。*/

logmask = setlogmask(LOG_UPTO(LOG_NOTICE)); /*设置屏蔽低于NOTICE级别的日志信息。*/

syslog(LOG_DEBUG, "debug message, should not appear"); /*该日志信息被屏蔽,不记录。*/

}

不同安全级别的日志信息存放在/var/log目录下的哪个文件中是由/etc/syslog.conf文件控制的,下面是我系统中syslog.conf文件的内容:

# /etc/syslog.conf Configuration file for syslogd.

#

# For more information see syslog.conf(5)

# manpage.

#

# First some standard logfiles. Log by facility.

#

auth,authpriv.* /var/log/auth.log

*.*;auth,authpriv.none -/var/log/syslog

#cron.* /var/log/cron.log

daemon.* -/var/log/daemon.log

kern.* -/var/log/kern.log

lpr.* -/var/log/lpr.log

mail.* -/var/log/mail.log

user.* -/var/log/user.log

uucp.* /var/log/uucp.log

#

# Logging for the mail system. Split it up so that

# it is easy to write scripts to parse these files.

#

mail.info -/var/log/mail.info

mail.warn -/var/log/mail.warn

mail.err /var/log/mail.err

# Logging for INN news system

#

news.crit /var/log/news/news.crit

news.err /var/log/news/news.err

news.notice -/var/log/news/news.notice

#

# Some `catch-all' logfiles.

#

*.=debug;\

auth,authpriv.none;\

news.none;mail.none -/var/log/debug

*.=info;*.=notice;*.=warn;\

auth,authpriv.none;\

cron,daemon.none;\

mail,news.none -/var/log/messages

#

# Emergencies are sent to everybody logged in.

#

*.emerg *

#

# I like to have messages displayed on the console, but only on a virtual

# console I usually leave idle.

#

#daemon,mail.*;\

# news.=crit;news.=err;news.=notice;\

# *.=debug;*.=info;\

# *.=notice;*.=warn /dev/tty8

# The named pipe /dev/xconsole is for the `xconsole' utility. To use it,

# you must invoke `xconsole' with the `-file' option:

#

# $ xconsole -file /dev/xconsole [...]

#

# NOTE: adjust the list below, or you'll go crazy if you have a reasonably

# busy site..

#

daemon.*;mail.*;\

news.crit;news.err;news.notice;\

*.=debug;*.=info;\

*.=notice;*.=warn |/dev/xconsole

终端

对终端进行读写

在编写程序时,我们往往需要从终端读入数据。一种情况是需要连续地读入用户键入的选择项,这往往出现在数据库程序中。程序员往往会使用getchar函数来读取数据,继而判断输入的数据是否有效,从而做出反应。其实如此做带有很大的风险,一个实例程序如下

实例程序中,用户需要键入“A/回车/Q/回车”才能做出选择。但这种处理有着很大的风险,读者可以自己测试一下。这也是初学者经常碰到的问题。

默认情况下,只有当用户按下回车键后,程序才能读到终端的输入。这种处理方式是规范模式或标准模式。在这种模式下,所有的输入都给予行进行处理,在一个输入行完成前,终端接口负责管理所有的用户键盘输入,包括退格键,应用程序读不到用户输入的任何字符。

与标准模式相对的另一种模式为非标准模式,这种模式下,应用程序对用户输入字符的处理拥有更大的控制权。

在上述程序中,Linux会暂存用户读入的内容,直到用户按下回车键,然后将用户选择的字符及紧随其后的回车符一起传送到程序。所以,每当你选择一个菜单时,程序就调用getchar函数处理该字符,而当程序在下一次循环再次调用getchar函数时,它会立刻返回一个回车符。一个解决方案是程序在每次读入数据前首先清空回车键之前的所有数据,典型代码如下:

 

终端驱动程序和通用终端接口

有时,程序需要更加精细的终端控制能力,而不是仅通过简单的文件操作来完成对终端的一些控制。Linux提供了一组编程接口,这使得我们能够控制终端驱动程序的行为,从而允许我们对终端的输入和输出进行更好的控制。

有一组函数调用(GTI)用作控制终端,这组函数调用与用于读写数据的函数是分离的,这就使得读写数据的接口非常简洁,同时又保证用户可以对终端的行为进行更精细的控制。

 

termios结构

通过设置termios结构中的值和使用一组函数调用,我们就可以对终端接口进行控制。

提示:使用termios结构及相关的函数调用,需要包含termios.h头文件;同时需要包含curses函数库。

控制终端的操作模式有以下几种:输入模式、输出模式、控制模式、本地模式和特殊的控制字符。具体操作由tcgetattr函数和tcsetattr函数来完成。其中,本地模式是最常用,也是最重要的一种操作模式。

注意:程序要将终端设置恢复到程序开始运行之前的状态,这一点是非常重要的。首先保存这些值,然后在程序结束时恢复它们,这永远是程序的职责。

输入模式控制输入数据在被传递给程序之前的处理方式。通过设置termios结构中的c_iflag成员的标志对它们进行控制。

输出模式控制输出字符的处理方式,即由程序发送出去的字符在传递到串行口或屏幕之前是如何处理的。通过设置termios结构中c_oflag成员的标志对输出模式进行控制。

控制模式控制终端的硬件特性。通过设置termios结构中的c_cflag成员的标志对控制模式进行配置。控制模式主要用于串行线连接调制解调器的情况。

本地模式控制终端的各种特性。通过设置termios结构中的c_lflag成员的标志对本地模式进行配置。其中最常用的两个标志是ECHOICANON。前者抑制键入字符的回显,后者将终端在两个截然不同的接收字符处理模式之间进行切换。如果设置了ICANON标志,就启用标准输入行处理模式,否则就启动非标准模式。

当用户键入类似Ctrl-C这样的组合键时,终端会采取一些特殊的处理方式。termios结构中的c_cc数组成员将各种特殊的控制字符映射到对应的支持函数。每个字符的位置是由一个宏定义的,但不限制这些字符必须是控制字符。

注意:在两种不同的模式(标准模式和非标准模式)下,c_cc数组的下标值有一部分是重叠的。出于这个原因,一定要注意不要将两种模式各自的下标值混淆。

可以通过stty命令查询及修改终端模式。

通过termios结构我们还可以控制终端的传入和传出的速度(波特率)。

 

终端的输出

编写能够应付连接到UNIX系统上的各种不同类型终端的程序看上去是一件非常让人畏惧的事情。因为这样的程序必须针对各种类型的终端编写相应的代码。termifo软件包的出现解决了这一问题。在绝大多数现代的UNIX系统上,这个软件包和另一个软件包curses集成在一起。

注意:在Linux系统上,在使用termifo软件包时可能需要包含ncurses库;该库实现了curses软件包的所有功能。

termifo的功能标识由属性描述,它们被保存在一组编译好的terminfo文件中,而这些文件可以方便地在/usr/lib/terinfo/usr/share/terinfo目录下找到。例如,VT100终端的定义就放在文件/usr/share/terminfo/v/vt100中。你可以使用infocmp程序输出terminfo编译数据项的可读版本。

 

虚拟控制台

Linux的典型安装中将配置12个虚拟控制台。虚拟控制台通过字符设备文件/dev/ttyN使用,ttyTeletype的缩写,而N代表一个数字,从1开始。

通过whops命令,可以查看目前登录进系统的用户,以及目前在使用的虚拟控制台及其上运行的shell和程序。

Linux系统一般在前六个虚拟控制台上运行一个getty进程,这样用户即可用同一个屏幕、键盘和鼠标在六个不同的虚拟控制台上登录。可以通过组合键Ctrl+Alt+F在这六个不同的虚拟控制台之间进行切换。

如果Linux系统使用的是图形登录界面或者使用startx切入图形界面,X视窗系统将使用第一个未使用的控制台,通常是/dev/tty7

伪终端由字符设备文件/dev/pty使用,其中ptypseudo tty的缩写。它与tty终端的区别在于伪终端没有对于的硬件设备。

运程登录的终端由字符设备文件/dev/pts/N使用。

 

curses函数库

Curses标准作为过渡,位于简单的文本行程序和完全图形化界面(一般也更难于编程)的X视窗系统程序(如GTK/GNOMEQt/KDE)之间。

Curses函数库的名称来自它所提供的功能,它能够优化光标的移动并减少需要对屏幕进行的刷新,因此它也减少了必须向字符终端发送的字符数目。

 

基本使用方法

Curses例程工作在屏幕、窗口和子窗口上。所谓“屏幕”就是正在写的设备(通常是终端屏幕,也有可能是xterm屏幕)。Curses函数库使用两个数据结构来映射终端屏幕,它们是stdscrcurscr。其中stdscr数据结构对应的是“标准屏幕”,它的工作原理和stdio函数库中的标准输出stdout非常相似,它是curses程序中的默认输出插口;而curscr数据结构和stdscr相似,但它对应的是当前屏幕的样子。

一个使用curses函数库的典型例程如下:

当对使用curses函数库的程序进行编译时,必须在程序中包含头文件curses.h,它是需要在编译命令行中用-lcurses选项对curses函数库进行链接。

从上面的程序可以看到,所有curses程序必须以初始化函数initscr开始,以函数endwin结束。函数initscr在一个程序中只能调用一次。

提示:我们可以先调用endwin函数退出curses,然后通过调研clrearok(strscr,1)refresh函数继续curses操作。这样,实际上是首先让curses忘记物理屏幕的样子,然后强迫它执行一次完整的屏幕原文重现。

函数moveprintw的功能是移动光标和在当前位置上输出文本。在调用refresh函数之前,输出到stdscr上的内容是不会显示在屏幕上的。refresh函数的作用就是刷新物理屏幕。

当需要在屏幕上显示比较松散的多行文本时,典型方式就是通过move函数与printw函数的配合来完成。

简单来说,Curses函数库有几种函数:屏幕输出函数、输入函数、清除函数和光标移动函数。通过这几种函数的配合,我们就可以实现一个简单的全屏界面。

字符属性:每个curses字符都可以有特定的属性,该属性控制着该字符在屏幕上的显示方式,前提是用于显示的硬件设备能够支持要求的属性。预定义的属性有A_BLINKA_BOLDA_DIMA_REVERSEA_STANDOUTA_UNDERLINE。相关函数有attronattroffattrset等。一个典型的使用片段如下:

键盘curses函数库还提供了控制键盘的简单方法。通过调用两个echo函数,我们可以简单地关闭或开启输入字符的回显功能。通过调用break函数,可以将输入模式设置为字符中止模式,在这种模式下,字符一经键入立刻传递给程序,而不是像在行模式中那样首先缓存字符,知道用户按下回车键才将用户输入的字符传递给程序。通过调用两个raw函数则可以关闭或开启特殊字符的处理。

提示curses环境下,输入模式分行模式和字符中止模式。默认输入模式是行模式,当用户键入回车符时才会将输入的数据传递给程序;而字符中止模式则当字符一经键入就传递给程序。

 

窗口与子窗口

Curses函数库在物理屏幕上能够同时显示多个不同尺寸的窗口。

curses环境下,窗口由WINDOW数据结构来表示。实际上,标准屏幕stdscr只是WINDOW结构的一个特例。下面是一个使用了窗口的例程

上面的例程中,先在全屏幕上填满字符,然后创建一个10*20的新窗口,继而在新窗口上输出“HelloWorld”。

新窗口的建立是由newwin函数来实现的,它指定了新窗口的大小和位置。删除一个窗口时则使用delwin函数。函数box的作用在于使用特殊的字符来界定新窗口。

用于窗口的通用函数有几类:前缀w用于窗口、前缀mv用于光标移动、前缀mvw用于在制定窗口中移动光标。wrefresh函数用于刷新窗口。而mvwin函数的作用是移动指定的窗口到指定的位置;如果移动后窗口超出屏幕范围,mvwin函数调用将会失败。

子窗口是多窗口的一种特例,我们使用subwin函数和delwin函数创建和删除子窗口。与前面提到的新窗口相比,子窗口没有自己独立的屏幕字符存储空间,它们与它们的父窗口(在调用subwin时指定)共享同一字符存储空间。这意味着,对子窗口中内容的任何修改都会反映到它的父窗口中,所以删除子窗口时,屏幕不会发生任何变化。

子窗口主要的用途是提供了一种简洁的方式来卷动另一窗口里的部分内容。在编写curses程序时,经常需要卷动屏幕的某个小区域,将这个小区域定义为一个子窗口,然后对其卷动,就能达到我们想要的效果。

注意:使用子窗口有个强加的限制:在应用程序刷新屏幕之前必须先对其父窗口调用touchwin函数。

 

keypad模式

curses函数库提供了一个精巧的用于管理功能键的功能。对每个终端来说,它的每个功能键所对应的转义序列都被保存,通常是保存在一个terminfo结构中,而头文件curses.h通过一组以KEY_为前缀的定义来管理逻辑键。

curses在启动时会关闭转义序列与逻辑键之间的转换功能,这功能需要通过调用keypad函数来启用。

实际上,使用keypad模式还是有一定的限制的:

1)识别escape转义序列的过程是与时间相关的。在处理许多网络协议时这个问题会变得很突出,唯一解决办法是设法对终端进行编程,让它针对用户希望使用的每个功能键只发送一个单独的、唯一的字符,但这将限制可使用的控制字符的数目。

2)为了让curses能够区分“单独按下Escapce键”和“一个以Escape字符开头的键盘转义字符”,它必须等待一小段时间。

3curses不能处理二义性的Escape转义序列。如果你的终端上两个不同的按键会产生完全相同的转义序列,就回导致curses不知该返回哪个逻辑按键。Curses对这一问题的处理方式是简单地放弃对这个转义序列的处理。

 

彩色显示

鉴于历史性原因,curses只能以一种非常受限的方式来使用彩色。

Curses函数库对颜色的支持有些不同:字符颜色的定义及其背景色的定义并不完全独立。必须同时定义一个字符的前景色和背景色,称为颜色组合。

把颜色作为字符属性使用之前,必须首先调用init_pair函数对装备使用的颜色组合进行初始化,而对颜色属性的访问则通过COLOR_PAIR函数来完成。而颜色属性的激活则由wattron函数来完成,它的第二个参数指定了需要设置的颜色属性。

 

Pad

在编写高级curses程序时,有时需要先建立一个逻辑屏幕,然后再把它的全部或者部分内容输出到物理屏幕上。Curses提供了一个特殊的数据结构pad来解决这个问题。

Pad结构非常类似于WINDOW结构,所有执行写窗口操作的curses函数同样可以应用于pad。但是pad有自己的创建函数newpad和刷新函数prefresh

 

本章最后展示了一个CD唱片应用程序的完整代码。它详细地描述了如何使用curses来编写应用程序,为我们自己设计使用curses函数库带来了很大的帮助。

 

数据管理

 

内存管理

Linux为应用程序提供了一个简洁的视图,它能反映一个巨大的可直接寻址的内存空间。此外,Linux还提供了内存保护机制,它避免了不同的应用程序之间的互相干扰。

我们使用mallocfree函数来完成动态内存的分配和释放。与DOS下的程序不能访问超过640K的内存相比,在Linux系统上使用malloc可以开辟兆字节的内存空间。实际上,在Linux系统上使用malloc可以申请的内存也是有限的:当申请的内存空间足够大时,物理内存将耗尽,此时内核将会开始使用所谓的交换空间(swap space);当物理内存和交换空间都耗尽时,或者栈超出了最大长度时,内核将拒绝内存申请并可能提前终止程序。

提示Linux擅长管理内存,它允许应用程序使用数量非常巨大的内存,甚至使用一个单独的非常大的内存块。但是,必须要记住的是:分配了两块内存并不见得肯定能够得到一个单独的可以连续寻址的内存块,而很有可能是两个分开的内存块。

注意:由于malloc函数返回的是一个void*指针,因此我们需要通过类型转换,将其转换至我们需要的指针类型。实际上,malloc函数可以保证其返回的内存是地址对齐的(很有可能是4字节对齐),因此它返回的指针可以转换为任何类型。

空指针:当使用malloc开辟内存时,往往需要测试返回值是否是空指针。这里要说的是:空指针并非等价于(void *)0,实际上“空指针的内部(或运行期)表达形式很可能并不是全零,而且对不同的指针类型可能不一样”。因此,不要想当然地把NULL看成零值,也尽量不要再使用if(!ptr)方式来测试malloc函数的返回值,应该使用if(ptr==NULL)

 

文件锁定

Linux提供了多种特性来实现文件锁定。最简单的方法是以原子操作的方式创建锁文件,所谓“原子操作”就是在创建锁文件时,系统将不允许任何其他的事情发生。

锁文件仅仅只是充当一个指示器的角色,程序间需要通过相互协作来使用它们。为了创建一个用作锁指示器的文件,使用在fcntl.h文件中定义的带O_CREATO_EXCL标志的open系统调用。

注意:由上述锁文件方式建立的文件严格仅属于创建它的进程,也就是说,即使使用open系统调用创建该锁文件的进程调用close关闭了该文件,其他进程也再无法使用open系统调用打开该文件。

实际上,用创建锁文件的办法来控制诸如串行口之类的资源的独占式访问时一个不错的选择,但它并不适用于大型的共享文件。这种情况下,我们可以通过文件中的锁定区域来解决这个问题:文件的一部分被锁定,但其他程序可以访问这个文件的其他部分。这杯称为文件段锁定或文件区锁定。

Linux提供了至少两种方式来完成这一工作:使用fcntl系统调用和使用lockf调用。其中fcntl系统调用时最常使用的方式。而使用fcntl对文件锁定的操作在《精通UNIXC语言编程与项目实践》的学习笔记3中有详细的解释。

注意fcntllockf的锁定机制不能同时工作。它们使用不同的底层实现,因此你决不能混合使用两种类型的调用,而应该坚持使用其中的一种。

提示:当对文件的区域加锁之后,访问文件中的数据应该使用底层的readwrite调用,而不要使用高级的freadfwrite函数,这一点是非常重要的。

 

dbm数据库

所有版本的Linux以及大多数的UNIX版本都随系统带有一个基本的,但却非常高效的数据存储的例程集,称为dbm数据库。dbm数据库适合于存储相对比较静态的索引化数据。

dbm数据库的优点是它非常容易被编译进一个可发布的二进制可执行程序,因为它无需安装独立的服务器,而且即使它需要的底层库文件还未被安装,也不会有什么危险。

dbm数据允许通过使用索引来存储可变长的数据结构,然后通过索引或简单的顺序扫描数据库来检索结构。dbm数据适用于处理那些被频繁访问但却很少被更新的数据,因为它创建数据项时非常慢,而检索时却非常快。

dbm数据有各种各样不同的版本,这里重点介绍ndbm接口。

dbm数据库的基本元素是需要储存的数据块以及与它关联的在检索数据时用作关键字的数据块。每个dbm数据库必须有一个针对每个要处处的数据块的唯一的关键字。为了操纵这些数据块,头文件ndbm.h定义了一个名为datum的新数据类型。该类型确切的定义依赖于具体实现,但至少包含下面这些成员:

当我们打开一个dbm数据库时,将创建两个物理文件,它们的后缀分别是.pag.dir,而仅仅返回一个DBM类型指针,它被用来访问这两个文件。这两个文件不应该被直接读写,对它们的访问一定要通过dbm例程来进行。类似于FILE类型,DBM类型用来访问数据库的结构。下面给出一个典型的dbm使用例程:

上面程序中,我们先存储了两个数据块,继而按关键字读取一个数据块。

首先,我们将需要存储的数据定义为test_data结构。dbm数据库的打开和关闭使用dbm_opendbm_close函数来完成。dbm_store函数用于向一个打开的数据库写入一个数据块,第一个参数指定了打开的dbm数据库对应的DBM指针,第二、三个参数指明了数据块及其关键字对应的datum结构变量,第四个参数则指明了操作类型(常用DBM_REPLACE)。每个数据块对应的关键字可以是任意形式的,但在实际编程中最好统一模式。函数dbm_fetch用来从打开的dbm数据库中提取一个数据块,第二个参数指明了需要提取数据块的关键字。

至于其他的dbm函数可以自行查看手册。本章最后展示了CD唱片应用程序的完整代码。它详细地描述了如何使用dbm数据库来完成数据的存储和访问,为我们自己设计使用dbm数据库带来了很大的帮助。

MySQL

Just do one thingdo it well!

两个最著名的开源RDBMS应用软件是PostgreSQLMySQLPostgreSQL能在任何情况下免费使用。MySQL在某些情况下需要收取许可费用。本书专注于MySQL,《Linux高级程序设计》对PostgreSQL有所涉及,同时你可以在www.postgresql.org上找到更多详细资料。

 

安装MySQL

一般情况下,在你安装的Linux套件中会有MySQL软件包。当你发现系统上没有安装MySQL时你就需要自己去手动安装了。安装方法一般分为两种:rpm软件包分发和源代码分发。对于如何使用rpm二进制软件分发方式安装MySQL,网上有篇帖子介绍了在RedHat 9上的详细安装步骤;对于源代码分发方式,你可以参照源代码目录中INSTALL_BINARY文件。

MySQL的安装过程中,安装脚本会为你自动创建一个初始数据库。同样,你也能得到一个用于启动和停止服务器的init.d脚本(通常在/etc/rc.d/init.d目录中)mysqldmysql。通常情况下,在特定Linux发行版上查找数据库文件最简单的方法就是,在init.d目录中找到这个脚本并查看它的内容。标准路径和定义位于脚本的开头,你可以很容易地看到文件都被放在哪里了。

你也可以使用chkconfig命令来设置MySQL服务器的运行方式。该命令提供了一种更简单的方式来设置一个服务的运行级别。比如说,为了设置MySQL服务器在运行级别34上运行,可以按下面命令设置:

安装之后,可以以root身份使用/etc/rc.d/init.d/mysql start命令来手动启动服务器。MySQL在安装过程中还会创建用户mysql,在默认情况下,MySQL服务器进程将以此用户的身份来运行。

MySQL安装完成后,你可以使用下面的命令进入控制台

在控制台上,你可以输入\s获得关于MySQL服务器的信息;当然你也可以使用mysqladmin命令来查看服务器的信息。

注意MySQLroot用户和系统的root用户(超级用户)没有任何关联。对于Linux系统来说,除非绝对需要,否则使用root账号来登录MySQL是一个不好的习惯,所以你应该为日常使用添加一个普通用户。

 

管理MySQL

MySQL有一系列有用的工具程序来完成管理工作。

myisamchk命令:用来检查和修复使用默认MYISAM表格格式的任何数据表格,MYISAM表格格式由MySQL自身支持。

mysql命令:这是MySQL主要的也是唯一完全交互式的命令行工具。当然你也可以用重定向方式读入sql文件以非交互式模式来运行sql命令:

mysqladmin命令:可以快速进行MySQL数据库管理。可以完成创建数据库、删除数据库、更改密码和查看状态等操作。

mysqldump命令:可以以SQL命令集的形式把部分或整个数据库导出到一个单独文件中;同时该文件也能被重新导入MySQL或其他的SQL RDBMS

mysqlimport命令:与mysqldump对应的一个工具。

如果你是MySQL管理员,就必须维护用户信息。从MySQL 3.22开始,可以在MySQL控制台中使用grantrevoke命令管理用户。详细情况请参照MySQL用户手册。

提示MySQL有一个叫做MySQL GUI的图形工具。如果你实在不喜欢文本方式下的操作,可以选择使用这个图形工具。

 

使用C语言访问MySQL

C语言提供了一系列mysql例程来完成对mysql数据库的操作,下面是一个典型的连接例程:

在编译上面程序之前,需要在MySQL服务器上创建一个新的数据库foo,并且创建用户rick1,在MySQL客户端上命令如下

要使用C语言连接MySQL数据库,首先调用mysql_init函数初始化连接句柄,如果出错,返回NULL;调用mysql_read_connect创建实际连接,如果连接失败,返回NULL

当完成连接后,通常在程序正常退出前,需要调用mysql_close关闭连接。

在上面的例程中,我们使用了assert断言来确保mysql接口调用后的有效性。实际上,为了获得更多的错误信息,我们应该使用mysql_errnomysql_error函数。

当编译该例程时,你很有可能需要添加头文件路径和库文件路径,并且需要指定链接的库模块mysqlclient。在某些系统上,你可能还需要使用-lz选项来链接压缩库。

注意:当运行该例程时,一定要保证链接库文件libmysqlclient.so.16在有效的LIB路径(比如/usr/lib路径)上。

使用C语言接口操作MySQL数据库,实际上就是执行sql语句。实际编程中,我们使用mysql_query或者mysql_real_query执行sql语句。要获取sql语句的执行结果,我们使用mysql_store_result或者mysql_use_result函数,其中,前者一次性提取所有数据并将其存储在MYSQL_RES结构中,而后者则一次提取一行数据,同样存储在MYSQL_RES结构中。调用mysql_num_rows函数将得到结果集中的行数。在得到MYSQL_RES结构后,则循环调用mysql_fetch_row获取一行执行结果保存为MYSQL_ROW结构,要想提取列数据,只需对MYSQL_ROW结构进行下标操作,下面是一个典型例程:

在执行上述例程前,需要在foo数据库中完成children表的创建和数据插入操作。你可以选择直接在MySQL控制台上执行sql语句,也可以执行存储了sql语句的文件。定义如下,你可以多写入几行数据

 

详细的MySQL编程接口可以自行查看手册。本章最后展示了CD唱片应用程序的完整代码。它详细地描述了如何设计MySQL数据库以及使用C语言接口来完成数据的存储和访问,为我们自己设计使用MySQL数据库带来了很大的帮助。

 

开发工具

工具

make工具可以完成多个源文件的编译和链接自动化处理。为了合理有效地编译多个源文件,尤其是在项目比较大时,make工具的使用尤为重要。

为了正确有效地使用make工具,我们需要为项目提供一个makefile文件,该文件提供了一种机制,它告诉应用程序应该如何构造。Makefile文件一般都会和项目的其他源文件放在同一个目录下。实际上,如果管理的是一个大项目,你可以用多个不同的makefile文件来分别管理项目的不同部分。如果你分析Linux的内核源码,你将清楚地看到这一点。

Makefile文件由一组依赖关系和规则构成。每个依赖关系由一个目标和一组该目标依赖的源文件组成。而规则描述了如何通过这些依赖文件创建目标。一般来说,目标是一个单独的可执行文件。如果你想深入得学习makefile的各种规则,可以选择阅读手册或者网上的一篇GNU make指南

提示:规则所在的行必须以制表符tab开头,用空格是不行的。

注意:在makefile中有些命令可能会以减号-开头,比如说“-rm”,它的含义是让make命令忽略rm命令的执行结果,这意味着即使由于目标文件不存在而导致rm命令返回错误,执行make命令时也会成功。

提示:当我们的源文件只有一个hello.c时,可以选择使用make hello命令来编译,它的执行效果与cc hello.c –o hello相同,这是make命令的一条内置规则。

 

源代码控制

UNIX在源代码管理方面有三个被广泛使用的系统,它们分别是RCSCVSSCCS。它们的使用方法可以去参考手册,我们可以在大项目的实践中去摸索使用。

实际上,现在还多人都会选择svn系统(SubVersion),你可以在网上得到它的中英文手册。

 

其他开发工具

我们可以使用nroff或者groff来编写使用手册

patch程序可以用来更新软件,它通过界定两个版本之间的差异来更新软件。

发布软件可以有两种方式:源代码软件包或者RPM软件包。源代码软件包一般使用tar工具包装;而RPM软件包则要复杂的多,详细情况可以参考手册。

除了在命令行上直接编写、编译软件外,我们还可以选择某些IDE工具,比如xwpeC-ForceKdevelopEclipse等。

 

调试

软件中每个重要代码段都会有缺陷,一般来说,每100行代码会有两到三个错误。

 

常用调试技巧

调试和测试Linux程序的方法一般是先运行程序并观察其输出结果,如果不能正常工作,我们就需要决定应该采取哪些措施。可以修改程序然后重新尝试(代码检查-试运行-出错法),也可以在程序中增加一些语句以获得更多关于程序内部运行情况的信息(取样法),还可以直接检查程序的执行情况(受控执行法)。

当程序的运行情况和预期不同时,重新阅读程序通常是一个好办法。有些工具可以帮助你完成代码检查工作,编译器就是其中比较明显的一个。如果程序有语法错误,它就会告诉你。

提示:在编译程序时为了获得更多的信息可以使用gcc -Wall -pedantic -ansi

取样法是指在程序中添加一些代码以手机与程序运行时的行为相关的更多信息的方法。取样法的常见做法是,在程序中添加printf函数调用以打印出变量在程序运行的不同阶段的值。不过需要注意的是,添加额外的代码时必须要非常小心地避免引入新的漏洞。

取样法的实现有两种技巧。一种是用C语言的预处理器有选择的包括取样代码,这样只需重新编译程序就可以达到包含或去除调试代码的目的。实现方式很简单,只需使用下面的结构:

在编译程序时可以加上编译器标志-DDEBUG,这样就可以将额外的调试代码添加进来。如果更进一步可以设计得稍微复杂一些,实际上它也是使用了C语言的预处理功能,典型结构如下:

这种情况下,我们必须总是定义DEBUG宏,但我们可以设置它为代表一组调试信息或代表一个调试级别。比如编译器标志-DDEBUG=5将启用BASIC_DEBUGSUPER_DEBUG,但不包括EXTRA_DEBUG;标志-DDEBUG=0将禁止所有的调试信息。

另一种技巧则无需重新编译,它在程序中增加一个作为调试标志的全局变量,这使得用户可以在命令行上通过-d选项切换是否启用调试模式,即使程序已经发行了,仍然可以这样做,该方法同时还会在程序中增加一个用于记录调试信息的函数。典型结构如下:

这样做法的好处是如果用户遇到了问题,他们自己就可以在运行程序时打开调试功能,替你完成诊断错误的工作;而明显的不足则是它会使程序的长度大大增加。

受控执行法就是使用某些工具在程序运行或者源代码级别上查看程序的比较详细的状态信息。这种工具包括adbsdbdbx等。一般情况下,我们可以使用gdb对程序进行受控调试运行。不过,为了能够调试程序,我们需要在编译它时加上一个或者多个特殊的编译器选项,比如-g标志就是对程序进行调试性编译时常用的一个选项。

 

使用gdb进行调试

Gdb是一个功能强大的调试器,它是一个自由软件,能够用在许多UNIX平台上。它同时也是Linux系统的默认调试器。关于它的详细使用信息可以参考手册,在我的博客上有几篇文章专门介绍了gdb

 

其他调试工具

除了像gdb这样彻底的调试器外,Linux系统一般还会提供许多能够帮助你完成调试工作的其他工作。其中有的是提供关于程序的静态信息,另外一些则是提供动态分析。

静态分析只能通过程序的源代码提供信息。ctagscxrefcflow等就是一些静态分析程序,它们可以通过源文件提供有关函数调用和函数所在位置的有用信息。

动态分析提供的是与程序执行过程中的行为有关的信息。profgprof等就是一些动态分析程序,它们提供的信息包括已经执行了哪些函数以及这些函数的执行时间。

工具lintC语言编译器的一个前端,它增加了一些常识性的测试并可以产生一些警告信息。它可以检测出未经复制的变量使用、函数的参数未使用等异常情况。

ctagscxrefcflow这三个工具构成了X/Open规范的一部分内容,因此具有软件开发能力的UNIX系统都会有这三个工具。

工具ctags为程序的所有函数创建索引。每个函数对于一个列表,在列表中累出该函数在程序中的调用位置,就像书籍的索引。

工具cxref分析C语言源代码并生成一个交叉引用表。它可以显示每个符号(变量、#define定义和函数)都在程序的哪个位置使用过。

工具cflow打印出一个函数调用树。该图示按函数之间的正向调用顺序显示了函数之间调用的关系,它可以让我们看清楚一个程序的框架结构,理解它的操作流程,理解对某个函数的改动将会产生怎样的影响。有些版本的cflow除了可以处理源代码外,还可以处理目标文件。

如果想查找程序的性能问题,常用的一种技巧是使用执行记录。它通常需要特殊的编译器选项和辅助程序的支持。程序的执行记录可以显示执行它所花费的时间具体都用在什么操作上了。

编译程序时,给编译器加上-p标志(针对prof工具)或-pg标志(针对gprof工具)就可以创建出profile程序。而工具prof(或者gprof)就可以根据profile程序运行时所产生的执行记录文件打印出一个报告。当使用了上述标志的程序运行时,监控数据将被写入当前目录下的文件mon.out(工具gprof使用gmon.out)中。详细细节请查看手册,51cto一篇文章“使用GNU gprof进行Linux平台下的程序分析”专门讲解了如何使用gprof工具。

 

断言

经常有这样的情况,程序运行中出现的问题与不正确的假设有关但并非代码的错误。这些不正确的假设往往是被直观认为不会发生的事件,因此我们需要对系统的内部逻辑做出确认。

针对这种情况,X/Open提供了assert宏,它的作用就是测试某个假设是否成立,如果不成立就立即停止程序的运行。

assert宏对表达式进行求值,如果结果非零,它就往标准错误写一些诊断信息,然后调用abort函数结束程序的运行。

头文件assert.h定义的宏受NDEBUG的影响。如果程序在处理这个头文件时已经定义了NDEBUG就不定义assert宏。这意味着,可以在编译期间使用-DNDEBUG关闭断言功能。

断言的常用用法以及注意事项大概有以下几个方面:

1)在函数开始处检验传入参数的合法性如

2)每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败
    不建议使用assert(nOffset>=0 && nOffset+nSize<=m_nInfomationSize);
    建议使用 assert(nOffset >= 0);
               assert(nOffset+nSize <= m_nInfomationSize);
    3)不能使用改变环境的语句,因为assertNDEBUG被定义时无效。如果这么做,会使用程序在真正运行时遇到问题
    错误assert(i++ < 100)  这是因为如果出错,比如在执行之前i=100,那么这条语句就不会执行,那么i++这条命令就没有执行。
    正确assert(i < 100)
          i++;

 

内存调试

动态内存分配时一个很容易出现持续漏洞的领域,而且漏洞一旦出现还很难查找。如果在程序中惯用mallocfree函数来分配内存,你就必须清楚自己分配过的每一块内存,并且要确定没有使用已经释放的内存块,这一点非常重要。

内存块通常都是由malloc函数分配给指针变量。如果指针变量的取值发生了变化,又没有其他指针指向这块内存,这块内存变得无法使用,这就是一种内存泄露现象。

如果在一个已分配的内存块尾部的后面写数据,就很可能会损坏malloc库用于记录内存分配情况的数据结构。出现这种情况,经过一段时间,一个malloc调用,甚至是一个free调用都会引发段错误并导致程序崩溃。

实际上,目前已有工具可以帮助解决这两类问题。ElectricFence函数库可以用Linux的虚拟内存机制来保护mallocfree所使用的内存,当它发现内存被破坏时就通知程序的运行。ElectricFence会将malloc及其管理函数替换为适应计算机处理器虚拟内存机制的版本,从而保护系统不受非法内存访问的破坏。

Valgrind工具可以检测出前面提出的许多问题,特别是它可以检测出数组访问错误和内存泄露。程序不需要重新编译就可以直接使用该工具,甚至还可以用它来调试一个正在运行程序的内存访问情况。

 

进程和信号

进程的基本概念

UNIX98规范和UNIX95规范把进程定义为“一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。

实际上,正在运行的程序或进程由程序代码、数据、变量(占用着系统内存)、打开的文件(文件描述符)和环境组成。一般来说,Linux系统会在进程之间共享程序代码和系统函数库,所以在任何时刻内存中都只有程序的一份拷贝。

每个进程都会被分配一个唯一的数字编号,称为进程标识符或PID,它通常是一个范围从232768的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID,当数字已经回绕一圈时,新的PID重新从2开始。数字1为特殊进程init保留,它负责管理其他的进程。所有其他的系统进程要么是由init进程启动,要么由被init进程启动的其他进程启动。

在许多Linux系统上,目录/proc中有一组特殊的文件,这些文件的特殊之处在于它们允许你“窥视”这在运行的进程的内部情况,就好像这些进程是目录中的文件一样。这在学习笔记03/proc文件系统部分提到过。

Linux进程表就像一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID、进程的状态、命令字符串和其他一些ps命令输出的各类信息。操作系统通过进程的PID对它们进行管理,这些PID是进程表的索引。进程表的长度是有限制的,所有系统能够支持的同时运行的进程数也是有限制的。早期的UNIX系统只能同时运行256个进程。最新的实现版本已大幅度放宽这一限制,可以同时运行的进程数可能只与用于建立进程表项的内存容量有关,而没有具体的数字限制了。

我们可以使用ps命令查看当前正在运行的进程。默认情况下,ps程序只显示与终端、主控台、串行口或伪终端(比如pts/0)保持连接的进程的信息。其他进程在运行时不需要通过终端与用户通信,它们通常是一些系统进程,Linux用它们来管理共享的资源。我们可以使用ps命令的-a选项查看所有的进程,用-f选项显示进程完整的信息。ps命令的详细资料请查阅手册。

 

进程调度

在一台单处理器计算机上,同一时间只能有一个进程可以运行,其他进程处于等待运行状态。每个进程轮到的运行时间(时间片)是相当短暂的,这就给人一种多个程序在同时运行的印象。

Linux内核用进程调度器来决定下一个时间片应该分配给哪个进程。它的判断依据是进程的优先级,优先级高的进程运行得更为频繁。在Linux中,进程的运行时间不可能超过分配给它们的时间片,它们采用的是抢占式多任务处理,所以进程的挂起和仅需运行无需彼此之间的协作。

在一个如Linux这样的多任务系统中,多个程序可能会竞争使用同一个资源,在这种情况下,我们认为,执行短期的突发性工作并暂停运行以等待输入的程序,要比持续占用处理器以进行计算或不断轮询系统以查看是否有新的输入到达的程序要更好。我们称表现良好的程序为nice程序。一个进程的nice值默认为0并将根据这个程序的表现而不断变化。我们可以使用nice命令设置进程的nice值,使用renice命令调整它的值。可以使用ps命令的-f-l详细查看这在运行的进程的nice值(NI栏)。

如果你对进程调度感兴趣,可以去参阅《操作系统》或《Linux内核》相关的书籍。

 

启动新进程

在《精通UNIX环境下C语言编程及项目实践》的学习笔记04中曾提过有三种执行新进程的方法。

一种就是直接调用库函数system来实现。然而一般来说,使用system函数远非启动其他进程的理想手段,因为它必须用一个shell来启动需要的程序。由于在启动程序之前需要先启动一个shell,而且对shell的安装情况及使用的环境的依赖也很大,所以使用system函数的效率不高。

另两种方式则是fork-execvfork-exec,日常编程中则常用前者。

Exec函数系列有六个函数,具体定义如下:

当我们在程序中直接调用exec函数时,指定运行的程序将替换当前的程序,看下面的一个简单程序pexec.c

我们使用make pexec & ./pexec & 运行程序,在语句0执行前使用ps命令查看当前的进程列表,你将发现pexec进程存在于列表中;但在语句0执行的结果中却找不到pexec进程,实际上当execl函数执行时,新启动的ps进程已经把pexec进程替换掉了。

注意:对于exec函数启动的进程来说,它的参数表和环境加在一起的总长度是有限制的。上限由ARG_MAX给出,在Linux系统上它是128K字节。其他系统可能会设置一个非常有限的长度,这有可能会导致出现问题,POSIX规范要求ARG_MAX至少要有4096个字节。

提示:在原进程中已打开的文件描述符在新进程中仍将保持打开,除非它们的“执行时关闭标志”(close on exec flag)被置位。任何在原进程中已经打开的目录流将在新进程中被关闭。

我们可以通过调用fork创建一个新进程。通过与exec函数配合,我们可以实现多进程编程的目的。当用fork启动一个子进程时,子进程就有了它自己的生命周期并将独立运行。我们可以通过在父进程中调用waitwaitpid函数来等待子进程的结束。

fork来创建进程确实很有用,但必须清楚子进程的运行情况。子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常地终止或父进程调用wait才结束。因此,进程表中代表子进程的表项不会立刻释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来以备父进程今后的wait调用使用。这时它将成为一个死进程(defunct)或僵尸进程(zombie。关于僵尸进程的详细介绍同样在《精通UNIX环境下C语言编程及项目实践》的学习笔记04中有所描述。

 

信号

信号是UNIXLinux系统响应某些条件而产生的一个事件,接收到信号的进程会相应地采取一些行动。信号的名称在头文件signal.h中定义,每个都以SIG开头。

我们可以使用signal函数处理信号,信号的处理方式可以是SIG_IGN(忽略信号)、SIG_DEF(默认方式)或者自行定义处理方式。关于如何使用signal处理信号在《精通UNIX环境下C语言编程及项目实践》的学习笔记05中有所描述。

注意:在信号处理程序中,调用如printf这样的函数是不安全的。一个有用的技巧是,在信号处理程序中设置一个标志,然后在主程序中检查该标志,如需要就打印一条信息。书中的表11-6列出了可以在信号处理程序中被安全调用的函数。

进程可以通过调用kill函数向包括它本身在内的其他进程发送一个信号。如果程序没有发送该信号的权限,对kill函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。

提示不推荐使用signal接口,应该使用定义更清晰、执行更可靠的函数sigaction,在所有的新程序中都应该使用这个函数。

X/OpenUNIX规范推荐了一个更新和更健壮的信号编程接口:sigaction,定义如下

下面是一个简单的例程,它用sigaction来截获SIGINT信号:

sigaction函数的调用方式与signal函数差不多。sigaction结构定义在文件signal.h中,它的作用是定义在接受到参数sig指定的信号后应该采取的行动。该结构应该至少包括以下几个成员:

其中,sa_mask字段指定了一个信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中。这是一组将被阻塞且不会传递给该进程的信号,在使用signal函数时,可能会出现有些信号在处理函数中还未运行结束时就被接收到,设置信号屏蔽字可以防止这种现象的发生。头文件signal.h中有一组函数用来操作信号集,它们分别是sigaddsetsigemptysetsigfillsetsigdelset等。

什么是Linux

相关推荐