unanao

为网络启动设置PXE服务器

服务搭建 — 作者 unanao @ 00:44

       如果你的系统完全崩溃掉了,不想刻盘,不想USB安装。又或是你需要给一个机房所有的机器安装系统。那么如果硬件支持,你应该试试网络安装。下面仅以debian为例介绍一下如何网络安装系统。
首先你的主板要支持网络启动,具体可以看bios。同时你需要设置一台 TFTP 服务器,并且对于很多机器来说,还需要一台 DHCP 服务器 ,或 BOOTP 服务器。BOOTP 是一种 IP 协议,用来告诉一台计算机它自己 IP 地址以及从网络何处获得启动映像。 DHCP (Dynamic Host Configuration Protocol) 是一个更灵活,向后兼容的 BOOTP 扩展。有些系统只能通过 DHCP 来配置。这里只介绍如何通过dhcp和tftp完成网络安装。

1、安装、配置tftp
    1.1 安装
    # aptitude install tftpd-hpa
    1.2 配置
    $ vim /etc/inetd.conf
        写入一下内容(lenny中默认已经设置好了):
        tftp dgram udp wait root /usr/sbin/in.tftpd /usr/sbin/in.tftpd -s /var/lib/tftpboot
        在较新版本的系统中(lenny中需要),还需要将/etc/default/tftpd-hpa中的RUN_DAEMON置为yes。

    1.3 启动tftp服务(始配置生效)
    #/etc/init.d/tftp-hda start


2、安装、配置dhcp服务器
    2.1 安装:
        $ apt-get install dhcp3-server
    2.2 配置   
    $ vim /etc/dhcp3/dhcpd.conf
加入如下内容:
# the configuration file for dhcpd when "Network boot linux via PXE"
# configure the ipaddress range, here is needded ***
subnet 192.168.1.0 netmask 255.255.255.0 {
    range 192.168.1.2 192.168.1.254;
}
# configure the ipaddress of the tftp server and the default file name
# please setup tftpd and put pxelinux.0 in the root directory of it
next-server 192.168.1.1;
filename "pxelinux.0";
# configure the ipaddress of the tftp server as 192.168.1.1 to avoid it getting
# a dynamical one, you'd modify the ethernet to your own before you using this
# configuration file
host server {                   # ensure the ip address of tftp
  hardware ethernet 00:e0:b0:f5:82:61;   #
  fixed-address 192.168.1.1;
}

range指定了客户机可以得到的ip的范围;next-server指定tftp服务器的ip地址;filename指定所启动的文件,这里注意,路径为相对于/etc/inetd.conf -s 后所指定的路径,一定不能写成绝对路径!!hardware ethernet 指定服务器的网卡地址,将服务器ip绑定为192.168.1.1避免服务器ip发生变化,使客户机不能tftp到指定的文件。至此,dhcp服务器配置完毕,现在一定不能在一个局域网里面启动,否则整个网络会出现问题。
下面通过交叉线(当然也可以通过交换机)将欲装系统的客户机和配置好的服务器相连。
    2.3 配置ip 与指定的fixed-address相同
    #ifconfig eth0 192.168.1.1
    2.4 重启使配置生效
        #/etc/init.d/dhcp3-server start
    2.5 若无法启动dhcp服务。
    若无法启动,看看自己ip与fixed-address是否一样,不一样则无法启动。

3、配置PXE
只需要vmlinuz(or linux) 和initrd这两个文件就可以了。
3.1 充分利用debian的netboot.tar.gz来配置PXE
到http://http.us.debian.org/debian/dists/lenny/main/installer-i386/current/images/netboot/netboot.tar.gz (或者其它镜像站下载)下载netboot.tar.gz并解压到/var/lib/tftpboot.当然如果inetd.conf中你指定是别的路径,就解压到相应的地方,解压后得到debian-installer目录。

3.2 在/var/lib/tftpboot目录下,利用debian-installer目录下的boot-screens创建menu目录,用于在进入安装之前,设置选择安装哪个发行版
#cd /var/lib/tftpboot
#mv debian-installer/i386/boot-screens menu 
其实只要保留 menu.cfg  syslinux.cfg  txt.cfg vesamenu.c32 这四个文件就可以了,但是全部保留也没关系,可以只删掉*.txt文件(rm *.txt)

3.3
# cp -a /var/lib/tftpboot/debian-installer/i386/pxelinux.* /var/lib/tftpboot
修改default文件为:
# D-I config version 1.0
include menu/menu.cfg
default menu/vesamenu.c32
prompt 0
timeout 0

3.4 添加一个需要安装的发行版
3.4.1 添加debian的网络安装
3.4.1.1 以安装debian lenny i386为例:
准备linux和initrd.gz
#mkdir /var/lib/tftpboot/debian_lenny_i386
#cp /var/lib/tftpboot/debian-installer/i386/linux /var/lib/tftpboot/debian_lenny_i386
#cp /var/lib/tftpboot/debian-installer/i386/initrd.gz /var/lib/tftpboot/debian_lenny_i386
3.4.1.2 配置新加入的网络安装的发行版,使之可以显示
#cd /var/lib/tftpboot/menu/
#cp txt.cfg lenny_i386.cfg
修改lenny_i386.cfg 为
label install
    menu label ^Install lenny i386
    menu default
    kernel debian_lenny_i386/linux
    append vga=normal initrd=debian_lenny_i386/initrd.gz -- quiet
(命名根据自己的习惯命就可以了)
最后,
unanao:/var/lib/tftpboot/menu#cat menu.cfg
menu hshift 13
menu width 49

menu title Installer boot menu
include debian-install/i386/boot-screens/stdmenu.cfg
include menu/lenny_i386.cfg              --这里加入include lenny_i386.cfg    (写的时候不要加--后面的内容)                     
include menu/centos.cfg
include debian-install/i386/boot-screens/amdtxt.cfg
include debian-installer/i386/boot-screens/gtk.cfg
include debian-installer/i386/boot-screens/amdgtk.cfg
menu begin advanced
    menu title Advanced options
    include debian-installer/i386/boot-screens/stdmenu.cfg
    label mainmenu
        menu label ^Back..
        menu exit
    include debian-installer/i386/boot-screens/adtxt.cfg
    include debian-installer/i386/boot-screens/amdadtxt.cfg
    include debian-installer/i386/boot-screens/adgtk.cfg
    include debian-installer/i386/boot-screens/amdadgtk.cfg
menu end
label help
    menu label ^Help
    text help
   Display help screens; type 'menu' at boot prompt to return to this menu
    endtext
    config debian-installer/i386/boot-screens/prompt.cfg
(其实这个配置文件里面好多都没有用了,不过懒着删了呵呵)
一个debian lenny i386 的网络启动就做好了,其它的发行版也大同小异。
3.4.2 添加centos的网络安装
准备:vmlinuz 和initrd.img两个文件
#cd /var/lib/tftpboot/
#mkdir /var/lib/tftpboot/centos_i386
#wget http://centos.ustc.edu.cn/centos/5.4/isos/i386/CentOS-5.4-i386-netinstall.iso
将vmlinuz 和initrd.img 拖出放入 centos_i386目录中

3.4.1.2 配置新加入的网络安装的发行版,使之可以显示
#cd /var/lib/tftpboot/menu/
#cp txt.cfg centos_i386.cfg
修改centos_i386.cfg 为
label install
    menu label ^Install centos i386
    menu default
    kernel centos_i386/linux
    append vga=normal initrd=centos_i386/initrd.gz -- quiet
(命名根据自己的习惯命就可以了)
最后,
unanao:/var/lib/tftpboot/menu#cat menu.cfg
menu hshift 13
menu width 49

menu title Installer boot menu
include debian-install/i386/boot-screens/stdmenu.cfg
include menu/lenny_i386.cfg              --这里加入include lenny_i386.cfg   (写的时候不要加--后面的内容)                
include menu/centos.cfg                  --这里加入include centos_i386.cfg  (写的时候不要加--后面的内容)      
include debian-install/i386/boot-screens/amdtxt.cfg
include debian-installer/i386/boot-screens/gtk.cfg
include debian-installer/i386/boot-screens/amdgtk.cfg
menu begin advanced
    menu title Advanced options
    include debian-installer/i386/boot-screens/stdmenu.cfg
    label mainmenu
        menu label ^Back..
        menu exit
    include debian-installer/i386/boot-screens/adtxt.cfg
    include debian-installer/i386/boot-screens/amdadtxt.cfg
    include debian-installer/i386/boot-screens/adgtk.cfg
    include debian-installer/i386/boot-screens/amdadgtk.cfg
menu end
label help
    menu label ^Help
    text help
   Display help screens; type 'menu' at boot prompt to return to this menu
    endtext
    config debian-installer/i386/boot-screens/prompt.cfg
(其实这个配置文件里面好多都没有用了,不过懒着删了呵呵)
一个cenos i386 的网络启动就做好了,其它的发行版也大同小异。
在centos安装的时候有一个地方 Installation Method
选择 HTTP

Web site name:centos.ustc.edu.cn
CentOS directory: centos/5.4/os/i386
3.4.3 网络安装arch
下载的vmlinuz和initrd.img的地址:
ftp://mirror.lzu.edu.cn/archlinux/iso/archboot/2009.08/archlinux-2009.08-1-archboot.iso
在isolinux目录里面的vmlinuz和initrd.img的两个文件。
其它的方法同上面两个发行版的安装。

4、安装系统
启动客户机,进bios设为从网络启动。等待一会儿,熟悉的安装界面出来了,可以开始安装了。
(注意在获取ip之前,请将server的dhcp服务停掉)
然后本本从PXE启动就可以了,进入安装后到了选择DEBIAN安装镜像那一步时候由于台式机和本本组成的小局域网和外界隔离所以无法找到镜像,这个时候可以拔下本本的网线然后直接插到可以可以连到外面的交换机上(就是大局域网上),运行一把dhcpclient 就可以(让本本在不重新启动的情况下在新网络里通过DHCP重新分配IP,如果你的台式机没有和外界隔离并且提供DNS和网关服务就不需要这样插拔网线啦)。这样就完成了安装。

5、参考资料:
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=1437&forum=6

http://www.thusa.co.za/blog/warwick-chapman/howto-pxe-boot-menu-both-debian-50-lenny-and-40-etch-network-installs
http://www.chrisgountanis.com/technical/45-centos-netinstall.html


Linux忘记密码,利用single-user mode重新设定密码

Debian — 作者 unanao @ 06:11
进入GRUB后(如果默认不进入grub,直接启动系统,按“Esc”键进入):
1、移动到有“(single-user mode)”的那一行。
2、按“e”键
3、移动到有“kernel”的那一行,,
4、按“e”键,
把“ro single” 修改成“ro init=/bin/bash”
5、按“回车”键
6、按 “b”键
7、mount -o remount,rw /
8、使用“passwd”修改忘记的密码就可以了
9、reboot

Debian 下写 kernel module--/proc 文件系统

嵌入式 — 作者 unanao @ 05:00

By unanao
<sunjianjiao@gmail.com>

一、linux内核模块
    Linux 内核具有模块化设计,提供了可伸缩的、动态的内核。在引导时,只有少量的驻留内核被载入内存。这之后,无论何时用户要求使用驻留内核中没有的功能,某内核模块(kernel module),有时又称驱动程序(driver)。就会被动态地载入内存。 通过 Linux 内核模块(LKM)可以在运行时动态地更改 Linux。可动态更改 是指可以将新的功能加载到内核、从内核去除某个功能,甚至添加使用其他 LKM 的新 LKM。LKM 的优点是可以最小化内核的内存占用,只加载需要的元素(这是嵌入式系统的重要特性)。
    Linux 的众多优良特性之一就是可以在运行时扩展由内核提供的特性的能力. 这意味着你可以在系统正在运行着的时候增加内核的功能( 也可以去除 ).每块可以在运行时添加到内核的代码, 被称为一个模块. Linux 内核提供了对许多模块类型的支持, 包括但不限于, 设备驱动. 每个模块由目标代码组成( 没有连接成一个完整可执行文件 ), 可以动态连接到运行中的内核中, 通过 insmod 程序, 以及通过 rmmod 程序去连接.“图内核的划分”表示了负责特定任务的不同类别的模块, 一个模块是根据它提供的功能来说它属于一个特别类别的. “图内核的划分”中模块的安排涵盖了最重要的类别, 但是远未完整, 因为在 Linux 中越来越多的功能被模块化了.

内核的划分
                        图 内核的划分
二、在Debian下编译内核需要安装内核头文件:
#aptitude install linux-headers-`uname -r`
(把目前所使用的内核的头文件安装上,也同样会安装上build-essential,linux-kbuild等包,如果之前没安装过的话。然后在/usr/src下会出现一个linux-header-xxxxx的目录,这个目录里就是当前使用的内核的头文件以及build模块的时候所需要用到的scripts和Makefile,这些scripts是linux-kbuild中提供的。)

三、简单的hello world 内核模块
(先写一个简单的hello world 模块,熟悉linux内核模块编程,为procfs做准备)
unanao@debian:~/programming/modules/hello$ cat hello.c
#include <linux/init.h>
#include <linux/module.h>

static int hello_init(void)//void can't be abbreviation,or we will get a warning
{
    printk(KERN_EMERG "hello, world\n");
    return 0
}

static void hello_exit(void)
{
    printk(KERN_ALERT "Goodbye!\n");
}

module_init(hello_init);//There is no "",This is not a string
module_exit(hello_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("unanao");
MODULE_DESCRIPTION("simple module ,just output some words\n");
MODULE_ALIAS("hello");

   这个模块定义了两个函数, 一个在模块加载到内核时被调用( hello_init )以及一个在模块被去除时被调用( hello_exit ). moudle_init 和 module_exit 这几行使用了特别的内核宏来指出这两个函数的角色. 另一个特别的宏 (MODULE_LICENSE) 是用来告知内核, 该模块带有一个自由的许可证; 没有这样的说明, 在模块加载时内核会抱怨.

四、编写Makefile
unanao@debian:~/programming/modules/hello$ cat Makefile
ifneq ($(KERNELRELEASE),) # after ifneq ,there is a space! or,will get an error
    obj-m := hello.o
else
    KERNELDIR := /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
default:
     $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
    rm -rf *.o *.ko *mod.c [mM]odule* .*.cmd *.o.d .tmp_versions
endif

详细的信息参考linux内核源代码中“Documentation/kbuild/modules.txt”文件

五、内核模块的编译,插入,查看,卸载
debian:/home/unanao/hello$make
debian:/home/unanao/hello# insmod hello.ko
debian:/home/unanao/hello# lsmod
Module                  Size  Used by
hello                   1344  0
procfs                  2096  0
ipv6                  235396  10
ppdev                   6468  0
lp                      8164  0
vboxvfs                29536  0
nls_base                6820  1 vboxvfs
loop                   12748  0
parport_pc             22500  0
parport                30988  3 ppdev,lp,parport_pc
ac                      4196  0
serio_raw               4740  0
battery                10180  0
psmouse                32336  0
button                  6096  0
i2c_piix4               7216  0
pcspkr                  2432  0
i2c_core               19828  1 i2c_piix4
vboxadd                47008  4 vboxvfs
evdev                   8000  3
ext3                  105576  1
jbd                    39476  1 ext3
mbcache                 7108  1 ext3
ide_cd_mod             27684  0
cdrom                  30176  1 ide_cd_mod
ide_disk               10496  3
floppy                 47716  0
piix                    6568  0 [permanent]
ide_pci_generic         3908  0 [permanent]
ide_core               96168  4 ide_cd_mod,ide_disk,piix,ide_pci_generic
ata_generic             4676  0
libata                140448  1 ata_generic
pcnet32                27396  0
mii                     4896  1 pcnet32
scsi_mod              129548  1 libata
dock                    8304  1 libata
thermal                15228  0
processor              32576  1 thermal
fan                     4196  0
thermal_sys            10856  3 thermal,processor,fan
debian:/home/unanao/hello# rmmod hello

六、查看内核的输出
6.1、使用dmesg查看:
debian:/home/unanao/hello# dmesg |tail -2
[ 9344.308292] hello, world
[ 9389.298477] Goodbye, I will Come to a more beautiful word

6.2、直接通过pritk输出到终端:
6.2.1在xwindow环境下开的终端窗口,
    printk(KERN_EMERG "hello, world\n"); //输出
    printk(KERN_ALERT "Goodbye,I will Come to a more beautiful word\n");//不输出
6.2.2在黑屏的Linux控制台模式(Ctrl+Alt+(F1~F6))两个都可以显示输出。

6.3、printk()函数的总结
我们在使用printk()函数中使用日志级别为的是使编程人员在编程过程中自定义地进行信息的输出,更加容易地掌握系统当前的状况,对程序的调试起到了很重要的作用。
(下文中的日志级别和控制台日志控制级别是一个意思)

printk(日志级别 "消息文本");这里的日志级别通俗的说指的是对文本信息的一种输出范围上的指定。
日志级别一共有8个级别,printk的日志级别定义如下(在linux26/includelinux/kernel.h中):
#defineKERN_EMERG"<0>"/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/
#defineKERN_ALERT"<1>"/*报告消息,表示必须立即采取措施*/
#defineKERN_CRIT"<2>"/*临界条件,通常涉及严重的硬件或软件操作失败*/
#defineKERN_ERR"<3>"/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/
#defineKERN_WARNING"<4>"/*警告条件,对可能出现问题的情况进行警告*/
#defineKERN_NOTICE"<5>"/*正常但又重要的条件,用于提醒。常用于与安全相关的消息*/
#defineKERN_INFO"<6>"/*提示信息,如驱动程序启动时,打印硬件信息*/
#defineKERN_DEBUG"<7>"/*调试级别的消息*/

没有指定日志级别的printk语句默认采用的级别是 DEFAULT_ MESSAGE_LOGLEVEL(这个默认级别一般为<4>,即与KERN_WARNING在一个级别上),其定义在linux26/kernel/printk.c中可以找到。
下面是一个比较简单的使用
printk(KERN_INFO "INFO\n"); //这里可以使用数字代替 KERN_INFO,即可以写成printk("<6> INFO\n");
在这个格式的定义中,日志级别和信息文本之间不能够使用逗号隔开,因为系统在进行编译的时候,将日志级别转换成字符串于后面的文本信息进行连接。


七、/proc文件系统 
 /proc 文件系统在linux 内核中是一个特殊的文件系统。他是一个许你的文件系统,它不和块设备联系,仅仅在内存中存在。/proc 文件系统包含目录(作为组织信息的方式)和虚拟文件虚拟文件可以表示内核到用户的信息,并作为从用户发送信息到内核的手段。
 
八、利用procfs实现“数据从用户空间-->内核空间-->用户空间”
8.1 源代码:
unanao@debian:~/programming/modules/read_write$ cat procfs.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h> //如果要使用procfs的函数,需要包含头文件:
#include <linux/string.h>
#include <linux/vmalloc.h>
#include <asm/uaccess.h>
 
#define MAX_LENGTH PAGE_SIZE
static struct proc_dir_entry *proc_entry;
 
static char *buf; //space where in the kernel sapce for input data
 
/*function to write the data*/
int data_write(struct file *filp,const char __user *buff,unsigned long len, void *data)
{
    if(len >MAX_LENGTH){
        printk(KERN_EMERG "virtual_proc:buf is full");
        return -ENOSPC;//no space left on device
    }
    /* Copy buffer to kernel-space from user-space function: 
     *unsigned long copy_from_user( void *to, const void __user *from,\
     *unsigned long n );
     */
    if(copy_from_user(buf,buff,len)){
        return -EFAULT;    //asm-generic/errno-bash.h ,bad address     
    }
    return len;
}
/* Function to read a fortune*/
int data_read(char *page,char **start,off_t off,int count,int *eof,void *data)
{
    int len;
    if(off > 0){
        *eof=1;
        return 0;
    }
    /*wrap around*/
    len=sprintf(page,"%s\n",buf);
    return len;
}
 
int init_virtual_proc(void)
{
    int ret=0;
    /*
    *when vmalloc come across the error return 0(NULL address)
    *success return back a pointer
     */
    buf=(char *)vmalloc(MAX_LENGTH);
    if(!buf){
        return -ENOMEM;//out of memory
    }
    else{
        memset(buf,0,MAX_LENGTH);
        /*
        *create_proc_entry function. This function accepts a file name, a 
        *set of permissions, and a location in the /proc filesystem in 
        *which the file is to reside. The return value of 
        *create_proc_entry is a proc_dir_entry pointer (or NULL,indicating 
        *an error in create). You can then use the return pointer to 
        *configure other aspects of the virtual file, such as the function 
        *to call when a read is performed on the file
         */
        proc_entry=create_proc_entry("virtual_proc",0644,NULL);
        if(proc_entry==NULL){
            ret=-ENOMEM;//no memory
            vfree(buf);
            printk(KERN_EMERG "virtual_proc:couldn't create virtual_proc entry\n");
        }
        else{
            /*
            *use the read_proc and write_proc commands to plug in functions 
            *forreading and writing the virtual file. 
             */
            proc_entry->read_proc=data_read;//proc read function
            proc_entry->write_proc=data_write;//proc write function
            proc_entry->owner=THIS_MODULE;
            proc_entry->mode=S_IRWXU|S_IRGRP|S_IROTH|S_IWOTH;
            printk(KERN_EMERG "virtual_proc :Module loaded\n");
        }
    }
    return ret;
}
 
void cleanup_virtual_proc(void)
{
    remove_proc_entry("virtual_proc",NULL);
    vfree(buf);
    printk(KERN_EMERG "virtual:module unloaded\n");
}
module_init(init_virtual_proc);
module_exit(cleanup_virtual_proc);
 
MODULE_LICENSE("Dual BSD/GPL");
MODULE_DESCRIPTION("ProcFileSystem");
MODULE_AUTHOR("unanao<sunjianjiao@gmail.com>");
 
8.2 Makefile 文件
unanao@debian:~/programming/modules/read_write$ cat Makefile 
ifneq ($(KERNELRELEASE),)
    obj-m := procfs.o
else
    KERNELDIR ?=/lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
    rm -rf *.o *.ko *.mod.c [mM]odule* .*.cmd .tmp_versions
endif
 
九、编译、插入,执行
编译:
debian:/home/unanao/programming/modules/read_write$ make
插入模块:
debian:/home/unanao/programming/modules/read_write# insmod procfs.ko 
在用户空间写入数据:
unanao@debian:~/programming/modules/read_write$ echo "jianjiao" >/proc/virtual_proc 
在用户空间读出数据:
unanao@debian:~/programming/modules/read_write$ cat /proc/virtual_proc 
jianjiao
写在模块:
debian:/home/unanao/programming/modules/read_write# rmmod procfs
 
十、模块一些函数的解释
 
struct proc_dir_entry 配置虚拟文件的其它方面,如:调用一个read函数。
struct proc_dir_entry {
 
    const char *name;            // virtual file name
 
    mode_t mode;                // mode permissions
 
    uid_t uid;                // File's user id
 
    gid_t gid;                // File's group id
 
    struct inode_operations *proc_iops;    // Inode operations functions
 
    struct file_operations *proc_fops;    // File operations functions
 
    struct proc_dir_entry *parent;        // Parent directory
 
    ...
 
    read_proc_t *read_proc;            // /proc read function
 
    write_proc_t *write_proc;        // /proc write function
 
    void *data;                // Pointer to private data
 
    atomic_t count;                // use count
 
    ...
};
 
如果procfs只是被一个模块使用,一定要把struct proc_dir_entry 中的owner设为THIS_MODULE。
struct proc_dir_entry* entry;
entry->owner = THIS_MODULE;
 
设置procfs的权限为和属主,如:
struct proc_dir_entry* entry;
entry->mode = S_IWUSR |S_IRUSR | S_IRGRP | S_IROTH;
entry->uid = 0;
entry->gid = 100;
(上面只是例子,可以根据需要进行自己的设置)
 
 
struct proc_dir_entry* create_proc_entry(const char* name, mode_t mode, struct proc_dir_entry* parent);
 
该函数创建一个名为“name”普通文件,在父目录中文件的模式是“mode”。在procfs的根目录创建文件,使用NULL作为“parent”的参数。当成功,函数将返回一个struct proc_dir_entry指针,否则将返回NULL。
 
移除一个入口(entry)
void remove_proc_entry(const char* name, struct proc_dir_entry* parent);
从procfs中,删除在“parent”目录中的“name”文件。entry是通过名字删除的,而不是由create_proc_entry 创建时返回的struct proc_dir_entry 的类型的变量。请注意,此功能不递归删除项目。
在remove_proc_entry之前,一定要释放data (即:如果有一些数据分配了内存)。
 
和用户空间通信
create_proc_entry 的返回值struct proc_dir_entry* 的read_proc 和 write_proc 用来初始化read和write函数。
struct proc_dir_entry* entry;
entry->read_proc = read_proc_foo;
entry->write_proc = write_proc_foo;
 
read 函数负责把它的信息写入buffer,read函数的格式为:
int read_func(char* buffer, char** start, off_t off, int count,int* peof, void* data);
 
peof:当到达文件末尾时写“1”
data:很少使用,用于回调函数
 
write 函数用于将用户空间的数据写入内核空间,write函数的格式:
int write_func(struct file* file, const char* buffer, unsigned long count, void* data);
 
其它一些函数:
* Create a directory in the proc filesystem */
struct proc_dir_entry *proc_mkdir( const char *name,struct proc_dir_entry *parent );
 
/* Create a symlink in the proc filesystem */
struct proc_dir_entry *proc_symlink( const char *name,struct proc_dir_entry *parent,const char *dest );
 
/* Create a proc_dir_entry with a read_proc_t in one call */
struct proc_dir_entry *create_proc_read_entry( const char *name,mode_t mode,struct proc_dir_entry *base, read_proc_t *read_proc,void *data );
 
/* Copy buffer to user-space from kernel-space */
unsigned long copy_to_user( void __user *to,const void *from,unsigned long n );
 
/* Copy buffer to kernel-space from user-space */
unsigned long copy_from_user( void *to,const void __user *from,unsigned long n );
 
/* Allocate a 'virtually' contiguous block of memory */
void *vmalloc( unsigned long size );
 
/* Free a vmalloc'd block of memory */
void vfree( void *addr );
 
/* Export a symbol to the kernel (make it visible to the kernel) */
EXPORT_SYMBOL( symbol );
 
/* Export all symbols in a file to the kernel (declare before module.h) */
EXPORT_SYMTAB

十一、参考资料:
11.1 http://linux.ctocio.com.cn/153/8627653.shtml
11.2 Jonathan Corbet, Alessandro Rubini 和 Greg Kroah-Hartman
《Linux 设备驱动》(第三版)
11.3 Linux 可加载内核模块剖析
http://www.ibm.com/developerworks/cn/linux/l-lkm/
11.4 Erik (J.A.K.) Mouw 《Linux Kernel Procfs Guide》


Debian 下用c语言写 cgi 程序

程序设计 — 作者 unanao @ 00:05

By unanao
<sunjianjiao@gmail.com>
2009年12月9日

   CGI全称 Common Gateway Interface (共同编程接口),是一种编程接口,不论什么语言,只要按照该接口的标准编写出来的程序,即可叫做 CGI 程序。CGI 程序的输入/输出是使用编程语言的标准输入/标准输出,所以用 C/C++ 来写 CGI 程序就好象写普通程序一样,不过还有一些东西需要注意的。

一、为什么要进行CGI编程?
在HTML中,当客户填写了表单,并按下了发送(submit)按钮后,表单的内容被发送到了服务器端,一般的,这时就需要有一个服务器端脚本来对表单的内容进行一些处理,或者是把它们保存起来,或者是按内容进行一些查询,或者是一些别的什么。
有的人认为可以用JavaScript来代替CGI程序,这其实是一个概念上的错误。JavaScript只能够在客户浏览器中运行,而CGI却是工作在服务器上的。他们所做的工作有一些交集,比如表单数据验证一类的,但是JavaScript是绝对无法取代CGI的。但可以这样说,如果一项工作即能够用 JavaScript来做,又可以用CGI来做,那么绝对要使用JavaScript,在执行的速度上JavaScript比CGI有着先天的优势。只有那些在客户端解决不了的问题,比如和某个远程数据库交互,这时就应该使用CGI了。
简单的说来,CGI是用来沟通HTML表单和服务器端程序的接口(interface)。说它是接口,也就是说CGI并不是一种语言,而是可以被其他语言所应用的一个规范集。理论上讲,你可以用任何的程序语言来编写CGI程序,只要在编程的时候符合CGI规范所定义的一些东西就可以了。

二、CGI 程序的通信方式和数据接收
   当有数据从浏览器传到 Web 服务器后,该服务器会根据传送的类型(基本有二类:GET/POST),将这些接收到的数据传入 QUERY_STRING 或变量中,CGI 程序可以通过标准输入,在程序中接收这些数据。当要向浏览器发送信息时,只要向 Web 服务器发送特定的文件头信息,即可通过标准输出将信息发往 Web 服务器,Web 服务器处理完这些由 CGI 程序发来的信息后就会将这些信息发送给浏览器。这样就是 CGI 程序的通信方式了。
  用 GET 方式接收到的数据保存在 Web 服务器的 QUERY_STRING 变量里,而通过 POST 方式接收到的数据是保存在这个 Web 服务器变量里。它们的唯一区别就是:以 GET 方式接收的数据是有长度限制,而用 POST 方式接收的数据是没有长度限制的。并且,以 GET 方式发送数据,可以通过 URL 的形式来发送,但 POST方式发送的数据必须要通过 Form 才到发送。 

三、编写一个cgi程序的一般步骤:
 1、像平常一样编译和测试C程序。
 2、进行被用于作为CGI脚本使用的一些更改。
 3、重新编译和测试。在此测试阶段,您可能设置环境变量QUERY_STRING中,使其包含测试,因为它将为表单中的数据发送的数据。例如,如果您打算使用其中一个字段名为foo包含输入数据的格式,你可以把命令
      setenv QUERY_STRING中以“foo = 42”(当使用tcsh的外壳)

  QUERY_STRING中=“foo= 42”(当使用bash shell的)。
 4、检查的编译版本的格式,是否可以工作在服务器上。可能需要重新编译。您可能需要登录到服务器计算机上(使用Telnet,SSH或其他终端仿真器),以便您可以使用服务器上的编译器。
 5、将二进制可执行程序(和任何需要在服务器上的数据和文件)放到study(我的实验放到了study目录下,根据需要,可以放到cgi-bin目录下的任何一个目录及其子目录,包括cgi-bin目录本身。)目录下。
 6、建立一个简单的包含表单的HTML文件,以便测试脚本。

四、创建实验目录及一些默认的命名规则
    在/var/www/目录下建立cgi目录,用来存放html文件
    在/usr/lib/cgi-bin目录下建立study目录,用来存放可执行文件,为了很容易区分这是用于cgi的,都是以.cgi结尾的(当然不以.cgi结尾也是可以的)。
    编译的名称都是在原来的名字基础上变为.cgi,例如,example.c编译成的可执行文件名字为:example.cgi
    如果没有安装 apache的话需要安装apache:#aptitude install apache2

五、用C语言写一个cgi程序,在浏览器上打印 hello world
unanao@debian:/usr/lib/cgi-bin/study$  cat hello.c
#include <stdio.h>
int main()
{
    printf("Content-Type:text/plain;charset=utf8\n\n");
    printf("hello world\n");
    return  0;
}

解释:程序中的printf(“Content-Type:text/html\n\”),“Content-Type”:头信息,将输出信息的格式告诉Web服务器;
prinft (“Content-Type :text/plain\n\n”); 第一行的输出内容是必须的,也是一个CGI程序所特有的,这个输出是作为HTML的文件头。
此行通过标准输出将字符串″Content type :text/plain\n\n″传送给Web服务器。它是一个MIME头信息,它告诉Web服务器随后的输出是以纯ASCII文本的形式。请注意在这个头信息中有两个新行符,这是因为Web服务器需要在实际的文本信息开始之前先看见一个空行。
(前往不要少写一个“\n”,否则在/var/log/apache2/error.log中会提示“malformed header ......”的错误)
例如shell要写成:echo Content-type: text/plain
                         echo
     c语言要写成这样printf("Content-Type: text/html\n\n");
对于text/plain和text/html这两种形式,text/plian是純文本的形式,“\n”可以换行的;而text/html可以嵌入html标记,比如printf(“<BR>hello<BR>”)实现了换行的功能,而“\n”不能达到效果的。

六、编译访问
6.1编译:
$gcc -o hello.cgi hello.c
可以直接在/usr/lib/cgi-bin/study/hello.c目录下直接编译,或者在其它位置编译好的可执行文件移动到/usr/lib/cgi-bin/study/目录。 

6.2将该程序放在 Web 服务器的 cgi-bin 目录下,然后通过以下方式访问:
http://127.0.0.1/cgi-bin/study/hello.cgi
会在浏览器里打印出 Hello,World!

七、GET表单的处理
对于那些使用了属性“METHOD=GET”的表单(或者没有METHOD属性,这时候GET是其缺省值),GET方法中,数据被保存在服务器上一个叫做QUERY_STRING的环境变量中,可以从这里面把内容读出。这种表单的处理相对简单,只要读取环境变量就可以了。在C语言中,你可以用库函数getenv(定义在标准库函数stdlib中)来把环境变量的值作为一个字符串来存取。你可以在取得了字符串中的数据后,运用一些小技巧进行类型的转换,这都是比较简单的了。在CGI程序中的标准输出(output)(比如在C中的printf函数)也是经过重定义了的。它并没有在服务器上产生任何的输出内容,而是被重定向到客户浏览器。这样,如果编写一个C的CGI程序的时候,把一个HTML文档输出到它的 stdout上,这个HTML文档会被在客户端的浏览器中显示出来。这也是CGI程序的一个基本原理。

一个具体的程序实现,把表单中输入的数值乘起来,然后输出结果。
HTML表单包含两个文本框(输入两个数字)和一个提交按钮;
处理程序,用来做两个数字的乘积,并且输出结果。
表单:
unanao@debian:/var/www/cgi$ cat multi.html
<html>
<form action="/cgi-bin/study/mult.cgi">
    <div><label>multi 1:<input name="m" size="5"> </label></div>
    <div><label>multi 2:<input name="n" size="5"> </label></div>
    <div><input type="submit" value="Multiply"> </div>
</form>
</html>

下面就是处理这个表单的CGI程序,对应于FORM标签中的ACTION属性值。
unanao@debian:/usr/lib/cgi-bin/study$  cat mult.c
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("<TITLE>multiply Result</TITLE>\n");
    long m,n;
    char *data;
    printf("Content-Type: text/html\n\n");
    if(getenv("QUERY_STRING"))
        data=getenv("QUERY_STRING");
    else{
        perror("get the data error\n");
        exit(1);
    }
    /*sscanf 的用法,在“十、参考资料”3中有比较详细的讲述*/
    sscanf(data,"m=%ld & n=%ld",&m,&n);
    printf("<p>m=%ld,n=%ld,Mutiply result:\n%ld\n",m,n,m*n);
}

程序在后面调用了用了库函数getevn来得到QUERY_STRING的内容,然后使用sscanf函数把每个参数值取出来,sscanf函数的用法在“十、参考资料”3中有比较详细的讲述。
把程序编译后,将mult.cgi放在action指定的cgi-bin的目录下面,就可以被表单调用了。
     首先输入:http://127.0.0.1/cgi/multi.html
     然后在表单中输入数字,确定后就回得到两数的乘积。
  
八、POST表单的处理
GET是默认的方式,所以如果使用PSOT方式,不要忘method=”POST”!
看起来这个问题和上面讲的内容很相近,仅仅是用不同的表单和不同的脚本(程序)而已。但实际上,这中间是有一些区别的。在上面的例子中,GET的处理方法可以看作是“纯查询(pure query)”类型的,也就是说,它与状态无关。同样的数据可以被提交任意的次数,而不会引起任何的问题(除了服务器的一些小小的开销)。但是现在的任务就不同了,至少它要改变一个文件的内容。因而,可以说它是与状态有关的。这也算是POST和GET的区别之一。而且,GET对于表单的长度是有限制的,而 POST则不然,这也是在这个任务中选用POST方法的主要原因。但相对的,对GET的处理速度就要比POST快一些。
在CGI的定义中,对于POST类型的表单,其内容被送到CGI程序的标准输入(在C语言中是stdin),而被传送的长度被放在环境变量 CONTENT_LENGTH中。因而我们要做的就是,在标准输入中读入CONTENT_LENGTH长度的字符串。从标准输出读入数据听起来似乎要比从环境变量中读数据来的要容易一些,其实则不然,有一些细节地方要注意,这在下面的程序中可以看到。特别要注意的一点就是:CGI程序和一般的程序有所不同,一般的程序在读完了一个文件流的内容之后,会得到一个EOF的标志。但在CGI程序的表单处理过程中,EOF是永远不会出现的,所以千万不要读多于 CONTENT_LENGTH长度的字符,否这会有什么后果,谁也不知道(CGI规范中没有定义,一般根据服务器不同而有不同得处理方法)。

下面的程序实现了,在form表单中把读取的内容,显示到网页上面:
html表单:
unanao@debian:/var/www/cgi$ cat string.html
<html>
<form action="/cgi-bin/study/readoutput.cgi" method="POST">
<div> Your input (80 chars max.):<br>
<input name="data" size="60" maxlength="80"> <br>
<input type="submit" value="send">
</div>
</form>
</html>

读取输出程序:
unanao@debian:/usr/lib/cgi-bin/study$  cat readoutput.c
#include <stdio.h>
#include <stdlib.h>
#define MAXLEN 80
#define EXTRA 5
/* 4 for field name "data", 1 for "=" */
#define MAXINPUT MAXLEN+EXTRA+2
/* 1 for added line break, 1 for trailing NUL */

/*
*uncode的作用,因为如果输入的是 “Hello there!”
*但是传给程序的数据被编码为:“data=Hello+there%21”
*所以要uncode函数就是用来进行解码的
*/
void unencode(char *src, char *last, char *dest)
{
    /*对所有的数据进行遍历*/
    for(; src != last; src++, dest++){
        /*+在传输过程中,是两个word之间的连接符--空格*/
        if(*src == '+')
            *dest = ' ';
    /*如果发现以%开头的,则为特殊字符的编码,检查%后的两位十六进制是否是特殊
     * 符号编码,如果是则将取符号的ASCII值,比如!被编码为%21,
     * 其ASCII值为21,dest中将保存值21
*%xx:用其十六进制ASCII码值表示的特殊字符。根据值xx将其转换成相     *应的ASCII字符。
     */
        else if(*src == '%') {
            int code;
            /*jump %,then read two bits to code*/
            if(sscanf(src+1, "%2x", &code) != 1)
                code = '?';
            *dest = code;
            src +=2;
        }    
        /*非特殊字符直接保存*/
        else
            *dest = *src;
    }
    /*最后加上换行和字符串结束符*/
    *dest = '\n';
    *++dest = '';
}

int main(void)
{
    char *lenstr;
    char input[MAXINPUT], data[MAXINPUT];
    long len;
    printf("%s%c%c\n\n","Content-Type:text/html;charset=iso-8859-1",13,10);
    printf("<TITLE>Response</TITLE>\n");
    lenstr = getenv("CONTENT_LENGTH");
    if(lenstr == NULL || sscanf(lenstr,"%ld",&len)!=1 || len > MAXLEN)
        printf("<P>Error in invocation - wrong FORM probably.");
    else {
        /*
         *fetts从指定输入流读取字符串,原型为:char *fgets(char *s,int n,FILE *
         *stream)从stream读入字符串到s中,当读入n-1个字符或换行符时函数停止操作,
         *改函数在s的末尾增加一个NULL字符表明串的结束.     
         *成功返回指向s的指针,         
         *遇到文件结束或出错返回EOF对于POST类型的表单,         
         *其内容被送到CGI程序的标准输入(在C语言中是stdin),
         *而被传送的长度被放在环境变量 CONTENT_LENGTH中。因而我们要做的就是,
         *在标准输入中读入CONTENT_LENGTH长度的字符串。
         */
        printf("len=%d<br>",len);
        fgets(input, len+1, stdin);
        /*+EXTRA:传过去的地址后移5位,跳过data=
         *+len:    将input的指针移到最后
         *data:解码后的数据
         */
        unencode(input+EXTRA, input+len, data);
        printf("data=%s\n",data);
    }
    return 0;
}

下面的程序把表单中输入的一段文本内容添加到服务器上的一个fifo文件中,然后再从这个fifo文中读出。表单和回显的页面用frame放到同一个页面中。
HTML 表单:
together.html将input.html和output.html显示在同一个页面,input.html用于调用read.cgi将内容存入data fifo文件,output.html用于调用output.cgi文件,将写入的文件读出。
unanao@debian:/usr/lib/cgi-bin/study$  cat together.html
<html>
<head><title>Qemu Arm</title></head>
<frameset rows="%50,%50">
    <frame src="./input.html">
    <frame src="./output.html">
</frameset>
<html>

unanao@debian:/usr/lib/cgi-bin/study$  cat input.html
<html>
<body>
    <DIV> Input must less than 80 chars:
    <form method="POST" action="/cgi-bin/study/read.cgi">
        <input name="data" width="60" maxlength="70">
        <input type="submit" value="send">
    </DIV>
    </form>
</body>
</html>
unanao@debian:/usr/lib/cgi-bin/study$  cat output.html
</html>
<body>
    Please press the "view" button ,to see the result:
    <form method="POST" action="/cgi-bin/study/output.cgi">
        <input type="submit" value="View">
    </form>
</body>
</html>

内容储存和内容读出程序:
unanao@debian:/usr/lib/cgi-bin/study$  cat read.c
#include <stdio.h>
#include <stdlib.h>

#define DATAFILE "./data"
#define MAXLEN 80
#define EXTRA 5
#define MAXINPUT MAXLEN+EXTRA+2 //1 for line break,1 for traling NULL

void uncode(char *src,char *last,char *dest)
{
    int code;
    for(;src!=last;src++,dest++){
        if(*src=='+')
            *dest=' ';
        else if (*src=='%'){
            if(sscanf(src+1,"%2x",&code)!=1)
                code='?';
            *dest = code;
            src+=2;
        }
        else
            *dest=*src;
    }
    *dest='\n';
    *++dest='';
}

int main()
{
    char *lenstr;
    char input[MAXINPUT],data[MAXINPUT];
    long len;
    printf("Content-Type:text/html;charset=utf8\n\n");
    lenstr=getenv("CONTENT_LENGTH");
    if(lenstr == NULL || sscanf(lenstr,"%ld",&len)!=1 || len>MAXLEN)
        printf("Error in invocation -wrong form probably\n");
    else{
        FILE * fp;
        fgets(input,MAXLEN,stdin);
        uncode(input+EXTRA,input+len,data);
        fp=fopen("data","a");
        if(NULL==fp){
            perror("open data error\n");
            exit(1);
        }
        fputs(data,fp);
        fclose(fp);
    }
    printf("<a href='/cgi/input.html'> Return Back </a>");
}
debian:/usr/lib/cgi-bin/study# cat output.c
#include <stdio.h>
#include <stdlib.h>
#define DATAFILE "./data"
#define MAXLEN 80

int main()
{
    char c;
    FILE * fp;
    printf("Content-Type:text/html\n\n");
    fp=fopen(DATAFILE,"r");
    if(NULL==fp){
        printf("can't open the file <br>");
        exit(1);
    }
    while (EOF!=(c=getc(fp))){
        putchar(c);
    }
    printf("<br>");
    printf("<a href=/cgi/output.html>Return Back</a>");
    return 0;
}

从本质上来看,程序先从CONTENT_LENGTH环境变量中得到数据的字长,然后读取相应长度的字符串。因为数据内容在传输的过程中是经过了编码的,所以必须进行相应的解码。编码的规则很简单,主要的有这几条:
1. 表单中每个每个字段用字段名后跟等号,再接上上这个字段的值来表示,每个字段之间的内容用&连结;
2.所有的空格符号用加号代替,所以在编码码段中出现空格是非法的;

九、产生HTML输出
CGI程序产生的输出由两部分组成:MIME头信息和实际的信息。两部分之间以一个空行分开。我们已经看到怎样使用MIME头信息″Content type :text/plain\n\n″和printf()、put char()等函数调用来输出纯ASCII文本给Web服务器。实际上,我们也可以使用MIME头信息″Content type :text/html\n\n″来输出HTML源代码给Web服务器。请注意任何MIME头信息后必须有一个空行。一旦发送这个MIME头信息给We b服务器后,Web浏览器将认为随后的文本输出为HTML源代码,在HTML源代码中可以使用任何HTML结构,如超链、图像、Form,及对其他CGI 程 序的调用。也就是说,我们可以在CGI程序中动态产生HTML源代码输出 ,下面是一个简单的例子。

unanao@debian:/usr/lib/cgi-bin/study$  cat html.c
#include <stdio.h>

int main()
{
    printf("Content-Type:text/html\n\n");
    printf("<html>");
    printf("<body>");
    printf("<head><title>Cgi Html<title></head>");
    printf("<H2><b>hello Cgi in c </b></H2><br>");
    printf("<a href=\"/cgi/together.html\"> Jump to hello.html</a>");
    printf("</body>");
    printf("</html>");
    return 0;
}

上面的CGI程序简单地用printf()函数来产生HTML源代码。请注意在输出的字符串中如果有双引号,在其前面必须有一个后斜字符\,这是因为整个HTML代码串已经在双引号内,所以HTML代码串中的双引号符必须用一个后斜字符\来转义。 或者写成单引号,或者干脆不写单引号和双引号。

十、参考资料:
Getting Started with CGI Programming in C: http://www.cs.tut.fi/~jkorpela/forms/cgic.html
用c写CGI 程序简要指南:
http://www.programfan.com/article/2858.html
sscanf的用法(百度百科):
http://baike.baidu.com/view/1364018.htm 


1 2 3 4 5 6 7 8 9 10  下一篇»

Powered by LifeType