|
|
2008年6月 2年前就大学本科毕业了,今天才真正的拿到文凭,法国人的效率可见不一般呀。 今天算是真正意思上的本科生了,尽管在过几个月就要硕士毕业了,拿到了文凭其实 心里也高兴不起来,2年前考试通过的时候心里才高兴。 我已经催了3次了,因为前段时间要用到文凭,所以催的紧了。尼斯那边的负责人都已经 认识我了。 记得本科我考了13,94分,满分20。在文凭上的mention 写了assez bien。可惜真可惜,要是我 到了14分,那就是bien 了,高一个等级呢。 希望硕士能够到bien,目前为止我分数还不错,有14分多,如果实习报告能够到14分就很好了。 在法国那个及格还不难,但是要高分就相当困难。不过话说回来,分数高没有用,我们外国人 不能申请奖学金。个把吧的。 2008年5月 sscanf() - 从一个字符串中读进与指定格式相符的数据. 函数原型: Int sscanf( string str, string fmt, mixed var1, mixed var2 ... ); int scanf( const char *format [,argument]... ); 说明: sscanf与scanf类似,都是用于输入的,只是后者以屏幕(stdin)为输入源,前者以固定字符串为输入源。 其中的format可以是一个或多个 {%[*] [width] [{h | l | I64 | L}]type | ' ' | '\t' | '\n' | 非%符号} 注: 1、 * 亦可用于格式中, (即 %*d 和 %*s) 加了星号 (*) 表示跳过此数据不读入. (也就是不把此数据读入参数中) 2、{a|b|c}表示a,b,c中选一,[d],表示可以有d也可以没有d。 3、width表示读取宽度。 4、{h | l | I64 | L}:参数的size,通常h表示单字节size,I表示2字节 size,L表示4字节size(double例外),l64表示8字节size。 5、type :这就很多了,就是%s,%d之类。 6、特别的:%*[width] [{h | l | I64 | L}]type 表示满足该条件的被过滤掉,不会向目标参数中写入值 支持集合操作: %[a-z] 表示匹配a到z中任意字符,贪婪性(尽可能多的匹配) %[aB'] 匹配a、B、'中一员,贪婪性 %[^a] 匹配非a的任意字符,贪婪性 例子: 1. 常见用法。 char buf[512] = {0}; sscanf("123456 ", "%s", buf); printf("%s\n", buf); 结果为:123456 2. 取指定长度的字符串。如在下例中,取最大长度为4字节的字符串。 sscanf("123456 ", "%4s", buf); printf("%s\n", buf); 结果为:1234 3. 取到指定字符为止的字符串。如在下例中,取遇到空格为止字符串。 sscanf("123456 abcdedf", "%[^ ]", buf); printf("%s\n", buf); 结果为:123456 4. 取仅包含指定字符集的字符串。如在下例中,取仅包含1到9和小写字母的字符串。 sscanf("123456abcdedfBCDEF", "%[1-9a-z]", buf); printf("%s\n", buf); 结果为:123456abcdedf 5. 取到指定字符集为止的字符串。如在下例中,取遇到大写字母为止的字符串。 sscanf("123456abcdedfBCDEF", "%[^A-Z]", buf); printf("%s\n", buf); 结果为:123456abcdedf 6、给定一个字符串iios/12DDWDFF@122,获取 / 和 @ 之间的字符串,先将 "iios/"过滤掉,再将非'@'的一串内容送到buf中 sscanf("iios/12DDWDFF@122", "%*[^/]/%[^@]", buf); printf("%s\n", buf); 结果为:12DDWDFF 7、给定一个字符串““hello, world”,仅保留world。(注意:“,”之后有一空格) sscanf(“hello, world”, "%*s%s", buf); printf("%s\n", buf); 结果为:world %*s表示第一个匹配到的%s被过滤掉,即hello被过滤了 如果没有空格则结果为NULL。 2008年5月 exec函数族 也许有不少读者从本系列文章一推出就开始读,一直到这里还有一个很大的疑惑:既然所有新进程都是由fork产生的,而且由fork产生的子进程和父进程几乎完全一样,那岂不是意味着系统中所有的进程都应该一模一样了吗?而且,就我们的常识来说,当我们执行一个程序的时候,新产生的进程的内容应就是程序的内容才对。是我们理解错了吗?显然不是,要解决这些疑惑,就必须提到我们下面要介绍的exec系统调用。 1.10.1 简介 说是exec系统调用,实际上在Linux中,并不存在一个exec()的函数形式,exec指的是一组函数,一共有6个,分别是: #include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID 等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个 -1,从原程序的调用点接着往下执行。
现在我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和拥护做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化,我们已经知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种"写时拷贝(copy-on-write)"技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。
1.10.2 稍稍深入
上面6条函数看起来似乎很复杂,但实际上无论是作用还是用法都非常相似,只有很微小的差别。在学习它们之前,先来了解一下我们习以为常的main函数。
下面这个main函数的形式可能有些出乎我们的意料: int main(int argc, char *argv[], char *envp[])
它可能与绝大多数教科书上描述的都不一样,但实际上,这才是main函数真正完整的形式。
参数argc指出了运行该程序时命令行参数的个数,数组argv存放了所有的命令行参数,数组envp存放了所有的环境变量。环境变量指的是一组值,从用户登录后就一直存在,很多应用程序需要依靠它来确定系统的一些细节,我们最常见的环境变量是PATH,它指出了应到哪里去搜索应用程序,如 /bin;HOME也是比较常见的环境变量,它指出了我们在系统中的个人目录。环境变量一般以字符串"XXX=xxx"的形式存在,XXX表示变量名, xxx表示变量的值。
值得一提的是,argv数组和envp数组存放的都是指向字符串的指针,这两个数组都以一个NULL元素表示数组的结尾。
我们可以通过以下这个程序来观看传到argc、argv和envp里的都是什么东西: /* main.c */
int main(int argc, char *argv[], char *envp[])
{
printf("\n### ARGC ###\n%d\n", argc);
printf("\n### ARGV ###\n");
while(*argv)
printf("%s\n", *(argv++));
printf("\n### ENVP ###\n");
while(*envp)
printf("%s\n", *(envp++));
return 0;
}
编译它: $ cc main.c -o main
运行时,我们故意加几个没有任何作用的命令行参数: $ ./main -xx 000
### ARGC ###
3
### ARGV ###
./main
-xx
000
### ENVP ###
PWD=/home/lei
REMOTEHOST=dt.laser.com
HOSTNAME=localhost.localdomain
QTDIR=/usr/lib/qt-2.3.1
LESSOPEN=|/usr/bin/lesspipe.sh %s
KDEDIR=/usr
USER=lei
LS_COLORS=
MACHTYPE=i386-redhat-linux-gnu
MAIL=/var/spool/mail/lei
INPUTRC=/etc/inputrc
LANG=en_US
LOGNAME=lei
SHLVL=1
SHELL=/bin/bash
HOSTTYPE=i386
OSTYPE=linux-gnu
HISTSIZE=1000
TERM=ansi
HOME=/home/lei
PATH=/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/home/lei/bin
_=./main
我们看到,程序将"./main"作为第1个命令行参数,所以我们一共有3个命令行参数。这可能与大家平时习惯的说法有些不同,小心不要搞错了。
现在回过头来看一下exec函数族,先把注意力集中在execve上: int execve(const char *path, char *const argv[], char *const envp[]);
对比一下main函数的完整形式,看出问题了吗?是的,这两个函数里的argv和envp是完全一一对应的关系。execve第1个参数path是被执行应用程序的完整路径,第2个参数argv就是传给被执行应用程序的命令行参数,第3个参数envp是传给被执行应用程序的环境变量。
留心看一下这6个函数还可以发现,前3个函数都是以execl开头的,后3个都是以execv开头的,它们的区别在于,execv开头的函数是以 "char *argv[]"这样的形式传递命令行参数,而execl开头的函数采用了我们更容易习惯的方式,把参数一个一个列出来,然后以一个NULL表示结束。这里的NULL的作用和argv数组里的NULL作用是一样的。
在全部6个函数中,只有execle和execve使用了char *envp[]传递环境变量,其它的4个函数都没有这个参数,这并不意味着它们不传递环境变量,这4个函数将把默认的环境变量不做任何修改地传给被执行的应用程序。而execle和execve会用指定的环境变量去替代默认的那些。
还有2个以p结尾的函数execlp和execvp,咋看起来,它们和execl与execv的差别很小,事实也确是如此,除execlp和 execvp之外的4个函数都要求,它们的第1个参数path必须是一个完整的路径,如"/bin/ls";而execlp和execvp的第1个参数 file可以简单到仅仅是一个文件名,如"ls",这两个函数可以自动到环境变量PATH制定的目录里去寻找。
1.10.3 实战
知识介绍得差不多了,接下来我们看看实际的应用: /* exec.c */
#include <unistd.h>
main()
{
char *envp[]={"PATH=/tmp",
"USER=lei",
"STATUS=testing",
NULL};
char *argv_execv[]={"echo", "excuted by execv", NULL};
char *argv_execvp[]={"echo", "executed by execvp", NULL};
char *argv_execve[]={"env", NULL};
if(fork()==0)
if(execl("/bin/echo", "echo", "executed by execl", NULL)<0)
perror("Err on execl");
if(fork()==0)
if(execlp("echo", "echo", "executed by execlp", NULL)<0)
perror("Err on execlp");
if(fork()==0)
if(execle("/usr/bin/env", "env", NULL, envp)<0)
perror("Err on execle");
if(fork()==0)
if(execv("/bin/echo", argv_execv)<0)
perror("Err on execv");
if(fork()==0)
if(execvp("echo", argv_execvp)<0)
perror("Err on execvp");
if(fork()==0)
if(execve("/usr/bin/env", argv_execve, envp)<0)
perror("Err on execve");
}
程序里调用了2个Linux常用的系统命令,echo和env。echo会把后面跟的命令行参数原封不动的打印出来,env用来列出所有环境变量。
由于各个子进程执行的顺序无法控制,所以有可能出现一个比较混乱的输出--各子进程打印的结果交杂在一起,而不是严格按照程序中列出的次序。
编译并运行: $ cc exec.c -o exec
$ ./exec
executed by execl
PATH=/tmp
USER=lei
STATUS=testing
executed by execlp
excuted by execv
executed by execvp
PATH=/tmp
USER=lei
STATUS=testing
果然不出所料,execle输出的结果跑到了execlp前面。
大家在平时的编程中,如果用到了exec函数族,一定记得要加错误判断语句。因为与其他系统调用比起来,exec很容易受伤,被执行文件的位置,权限等很多因素都能导致该调用的失败。最常见的错误是:
- 找不到文件或路径,此时errno被设置为ENOENT;
- 数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;
- 没有对要执行文件的运行权限,此时errno被设置为EACCES。
2008年5月 昨天有个程序除了问题,调试的时候发现是在SOCKET通信的时候对方关闭连接的情况下出现broken pipe错误。在网上搜索了一番,别人解释原因是向已经关闭连接的SOCKET管道写入数据造成的,但是我的程序只是在对方关闭的时候继续RECV,难道 RECV也会向管道写入数据,这个问题以后再研究吧。 知道了问题就好办了。UNIX的系统是采用信号机制向进程通知这种系统错误的,13 SIGPIPE 的默认操作是Exit,所以在程序里面写一个自己的信号处理函数,不让进程退出就OK。不过值得注意的是在截获一次信号以后,系统会把信号处理又恢复到默认状态,所以需要再次设置。另外,对于多线程,我是在主线程里面做的信号处理,其他线程没有做,但是我理解信号是发给进程的,所以应该只要有一个线程处理了信号就可以。 源码: void InitSignal(void); void handle_signal(int s) ; /*初始化时及每次处理完时调用*/ void InitSignal(void) { signal(SIGPIPE,handle_signal); } /*信号处理函数*/ void handle_signal(int s) { InitSignal(); } 2008年5月 在linux系统启动的时候,我们可以看到很多服务性程序一个接一个的被启动(就是那些后面有一个兰色[OK]的行),这些在后台运行的程序为我们使用计算机起了很重要的作用,以这种方式运行的程序有以下特点: 开机启动,关机停止(废话!关机了还运行啊???) 后台运行 通过service 命令控制或查看对应的程序的运行状态 恩,不错,那怎么把我们自己编制的程序做成这样的一个服务呢? 废话少说,主要分以下3步: 1、把可执行程序放到一个linux系统可以找到的地方。 在linux命令提示符下输入 env ,回车后可看到环境变量,里面有一项PATH的,对,这项里所指定的目录都是系统可找到的地方,把我们的可执行程序放入其中的一个目录下即可,我们一般放在/usr/sbin/目录下(放什么有放什么的地方,最好不要乱放)。 2、在目录 /etc/init.d/ 下,新建一个以服务名为文件名的文件。 如果我们打开目录 /etc/init.d/,看到的文件其实都是服务程序文件,每个文件的内容都大同小异,我们会看到,这里的文件在文件结构上几乎是一样的。几乎每个文件都有 start、stop、restart和status这样的标志,对,我们新建的这个文件也具有相同的结构,只是在这些标志内部把相应的可执行程序换成我们自己的可执行程序即可,知道怎么做了吗?那就是:复制——修改——保存。 3、在目录 /etc/rc3.d/ 下,新建一个指向在第2步中建立的服务文件的符号连接文件。 打开目录 /etc/rc3.d/ ,我们将看到这里都是符号连接文件(linux系统中的符号连接相当于windows中的快捷方式)。采用以下命令格式: ln -sf 目标文件名 连接文件名 连接文件名益采用“SXX目标文件名”的格式,其中XX一般是一个从1到100的整数,它表示启动优先级,数字越大,优先级越低,比如:服务A的运行要依赖服务B,那A的XX数字就应该大于B的。后跟“目标文件名”是为了一目了然,一看就知道是哪个文件的符号连接。 目录 /etc/rc3.d/ 是系统启动时自动搜索的目录,该目录下的符号连接文件的目标文件,都将被运行,这就是在这个目录建立符号连接的原因——为了开机就运行。 好了,完成以上3个步骤,你就把自己的一个可执行程序做成系统的一个服务了,它具有我们一开始说的几个特定。我们可以用service命令控制这个程序了: 启动:service hdz_service start 停止:service hdz_service stop 重启:service hdz_service restart 上面的 hdz_service 为服务名,也就是在第2步中建立的文件名。 不过,好象感觉比较麻烦,能否把这几个步骤自动化一下?这样在一台新机子上部署就方便了,也更象回事 -_^!办法当然有了,还记得我们那个记录代码文件之间依赖关系的makefile文件吗?呵呵,我们在这里要用到它了。 在makefile文件中添加一个标志,并在该标志下添加和下面类似的代码: install: cp ./hdz_pro /usr/sbin/ cp ./hdz_service /etc/init.d/ cd /etc/init.d/ chmod +x hdz_service cd /etc/rc3.d/ ln -sf ../init.d/hdz_service ./S99hdz_service 在标志install下的没一句话,前面一定要留空白,这不仅是有利于阅读,更是一个要求,makefile文件要求每一句可执行语句前都要有空白(空格或tab)。 上面代码中的 hdz_pro 和 hdz_service 分别是可执行文件名和服务名,这两个名称换成你自己的就行了,操作时用以下命令: make install 运行完就一切OK了,简单吧? 大家在看我写的东东时,时不时会碰到“hdz”这样的东西,这是我名字的缩写,说明这正是要替换的。 2008年5月 int my_write(int fd,void *buffer,int length) { int bytes_left; int written_bytes; char *ptr; ptr=buffer; bytes_left=length; while(bytes_left>0) { /* 开始写*/ written_bytes=write(fd,ptr,bytes_left); if(written_bytes<=0) /* 出错了*/ { if(errno==EINTR) /* 中断错误 我们继续写*/ written_bytes=0; else /* 其他错误 没有办法,只好撤退了*/ return(-1); } bytes_left-=written_bytes; ptr+=written_bytes; /* 从剩下的地方继续写 */ } return(0); } int my_read(int fd,void *buffer,int length) { int bytes_left; int bytes_read; char *ptr; bytes_left=length; while(bytes_left>0) { bytes_read=read(fd,ptr,bytes_read); if(bytes_read<0) { if(errno==EINTR) bytes_read=0; else return(-1); } else if(bytes_read==0) break; bytes_left-=bytes_read; ptr+=bytes_read; } return(length-bytes_left); } 2008年4月 要国内寄了内存和笔记本键盘给我,昨天收到了,马上就装上我用了5年的笔记本上面了。用了一天,发现内存的兼容性还不错,到目前还没有出现死机等问题。键盘也换了新的,到时候去公司就不用带外置键盘了。 不知道1 G 的内存速度怎么样,好像快了点,难道是因为心里因素吗,呵呵。换内存的主要目的,是因为单位用的是mandria的Linux 操作系统,但是我喜欢用XP,所以我准备装模拟器来模拟Linux,但是这个需要大内存,以前的512M肯定不够,1G足够了。明天5 1,装个Mandriva 2008看看。 2008年4月 介绍 Socket 编程让你沮丧吗?从man pages中很难得到有用的信息吗?你想跟上时代去编Internet相关的程序,但是为你在调用 connect() 前的bind() 的结构而不知所措?等等… 好在我已经将这些事完成了,我将和所有人共享我的知识了。如果你了解 C 语言并想穿过网络编程的沼泽,那么你来对地方了。 -------------------------------------------------------------------------------- 读者对象 这个文档是一个指南,而不是参考书。如果你刚开始 socket 编程并想找一本入门书,那么你是我的读者。但这不是一本完全的 socket 编程书。 -------------------------------------------------------------------------------- 平台和编译器 这篇文档中的大多数代码都在 Linux 平台PC 上用 GNU 的 gcc 成功编译过。而且它们在 HPUX平台 上用 gcc 也成功编译过。但是注意,并不是每个代码片段都独立测试过。 -------------------------------------------------------------------------------- 目录: 1) 什么是套接字? 2) Internet 套接字的两种类型 3) 网络理论 4) 结构体 5) 本机转换 6) IP 地址和如何处理它们 7) socket()函数 8) bind()函数 9) connect()函数 10) listen()函数 11) accept()函数 12) send()和recv()函数 13) sendto()和recvfrom()函数 14) close()和shutdown()函数 15) getpeername()函数 16) gethostname()函数 17) 域名服务(DNS) 18) 客户-服务器背景知识 19) 简单的服务器 20) 简单的客户端 21) 数据报套接字Socket 22) 阻塞 23) select()--多路同步I/O 24) 参考资料 -------------------------------------------------------------------------------- 什么是 socket? 你经常听到人们谈论着 “socket”,或许你还不知道它的确切含义。现在让我告诉你:它是使用 标准Unix 文件描述符 (file descriptor) 和其它程序通讯的方式。什么?你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”那个家伙也许正在说到一个事实: Unix 程序在执行任何形式的 I/O 的时候,程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。但是(注意后面的话),这个文件可能是一个网络连接, FIFO,管道,终端,磁盘上的文件或者什么其它的东西。Unix 中所有的东西就是文件!所以,你想和Internet上别的程序通讯的时候,你将要使用到文件描述符。你必须理解刚才的话。现在你脑海中或许冒出这样的念头:“那么我从哪里得到网络通讯的文件描述符呢?”,这个问题无论如何我都要回答:你利用系统调用 socket(),它返回套接字描述符 (socket descriptor),然后你再通过它来进行send() 和 recv()调用。“但是...”,你可能有很大的疑惑,“如果它是个文件描述符,那么为什么不用一般调用read()和write()来进行套接字通讯?”简单的答案是:“你可以使用!”。详细的答案是:“你可以,但是使用send()和 recv()让你更好的控制数据传输。”存在这样一个情况:在我们的世界上,有很多种套接字。有DARPA Internet 地址 (Internet 套接字),本地节点的路径名 (Unix套接字),CCITT X.25地址 (你可以将X.25 套接字完全忽略)。也许在你的Unix 机器上还有其它的。我们在这里只讲第一种:Internet 套接字。 -------------------------------------------------------------------------------- Internet 套接字的两种类型 什么意思?有两种类型的Internet 套接字?是的。不,我在撒谎。其实还有很多,但是我可不想吓着你。我们这里只讲两种。除了这些, 我打算另外介绍的 "Raw Sockets" 也是非常强大的,很值得查阅。 那么这两种类型是什么呢?一种是"Stream Sockets"(流格式),另外一种是"Datagram Sockets"(数据包格式)。我们以后谈到它们的时候也会用到 "SOCK_STREAM" 和 "SOCK_DGRAM"。数据报套接字有时也叫“无连接套接字”(如果你确实要连接的时候可以用connect()。) 流式套接字是可靠的双向通讯的数据流。如果你向套接字按顺序输出“1,2”,那么它们将按顺序“1,2”到达另一边。它们是无错误的传递的,有自己的错误控制,在此不讨论。 有什么在使用流式套接字?你可能听说过 telnet,不是吗?它就使用流式套接字。你需要你所输入的字符按顺序到达,不是吗?同样,WWW浏览器使用的 HTTP 协议也使用它们来下载页面。实际上,当你通过端口80 telnet 到一个 WWW 站点,然后输入 “GET pagename” 的时候,你也可以得到 HTML 的内容。为什么流式套接字可以达到高质量的数据传输?这是因为它使用了“传输控制协议 (The Transmission Control Protocol)”,也叫 “TCP” (请参考 RFC-793 获得详细资料。)TCP 控制你的数据按顺序到达并且没有错 误。你也许听到 “TCP” 是因为听到过 “TCP/IP”。这里的 IP 是指“Internet 协议”(请参考 RFC-791。) IP 只是处理 Internet 路由而已。 那么数据报套接字呢?为什么它叫无连接呢?为什么它是不可靠的呢?有这样的一些事实:如果你发送一个数据报,它可能会到达,它可能次序颠倒了。如果它到达,那么在这个包的内部是无错误的。数据报也使用 IP 作路由,但是它不使用 TCP。它使用“用户数据报协议 (User Datagram Protocol)”,也叫 “UDP” (请参考 RFC-768。) 为什么它们是无连接的呢?主要是因为它并不象流式套接字那样维持一个连接。你只要建立一个包,构造一个有目标信息的IP 头,然后发出去。无需连接。它们通常使用于传输包-包信息。简单的应用程序有:tftp, bootp等等。 你也许会想:“假如数据丢失了这些程序如何正常工作?”我的朋友,每个程序在 UDP 上有自己的协议。例如,tftp 协议每发出的一个被接受到包,收到者必须发回一个包来说“我收到了!” (一个“命令正确应答”也叫“ACK” 包)。如果在一定时间内(例如5秒),发送方没有收到应答,它将重新发送,直到得到 ACK。这一ACK过程在实现 SOCK_DGRAM 应用程序的时候非常重要。 -------------------------------------------------------------------------------- 网络理论 既然我刚才提到了协议层,那么现在是讨论网络究竟如何工作和一些 关于 SOCK_DGRAM 包是如何建立的例子。当然,你也可以跳过这一段, 如果你认为已经熟悉的话。 现在是学习数据封装 (Data Encapsulation) 的时候了!它非常非常重要。它重要性重要到你在网络课程学(图1:数据封装)习中无论如何也得也得掌握它。主要 的内容是:一个包,先是被第一个协议(在这里是TFTP )在它的报头(也许 是报尾)包装(“封装”),然后,整个数据(包括 TFTP 头)被另外一个协议 (在这里是 UDP )封装,然后下一个( IP ),一直重复下去,直到硬件(物理) 层( 这里是以太网 )。 当另外一台机器接收到包,硬件先剥去以太网头,内核剥去IP和UDP 头,TFTP程序再剥去TFTP头,最后得到数据。现在我们终于讲到声名狼藉的网络分层模型 (Layered Network Model)。这种网络模型在描述网络系统上相对其它模型有很多优点。例如, 你可以写一个套接字程序而不用关心数据的物理传输(串行口,以太网,连接单元接口 (AUI) 还是其它介质),因为底层的程序会为你处理它们。实际 的网络硬件和拓扑对于程序员来说是透明的。 不说其它废话了,我现在列出整个层次模型。如果你要参加网络考试, 可一定要记住: 应用层 (Application) 表示层 (Presentation) 会话层 (Session) 传输层(Transport) 网络层(Network) 数据链路层(Data Link) 物理层(Physical) 物理层是硬件(串口,以太网等等)。应用层是和硬件层相隔最远的--它 是用户和网络交互的地方。 这个模型如此通用,如果你想,你可以把它作为修车指南。把它对应 到 Unix,结果是: 应用层(Application Layer) (telnet, ftp,等等) 传输层(Host-to-Host Transport Layer) (TCP, UDP) Internet层(Internet Layer) (IP和路由) 网络访问层 (Network Access Layer) (网络层,数据链路层和物理层) 现在,你可能看到这些层次如何协调来封装原始的数据了。 看看建立一个简单的数据包有多少工作?哎呀,你将不得不使用 "cat" 来建立数据包头!这仅仅是个玩笑。对于流式套接字你要作的是 send() 发送数据。对于数据报式套接字,你按照你选择的方式封装数据然后使用 sendto()。内核将为你建立传输层和 Internet 层,硬件完成网络访问层。 这就是现代科技。 现在结束我们的网络理论速成班。哦,忘记告诉你关于路由的事情了。 但是我不准备谈它,如果你真的关心,那么参考 IP RFC。 -------------------------------------------------------------------------------- 结构体 终于谈到编程了。在这章,我将谈到被套接字用到的各种数据类型。 因为它们中的一些内容很重要了。 首先是简单的一个:socket描述符。它是下面的类型: int 仅仅是一个常见的 int。 从现在起,事情变得不可思议了,而你所需做的就是继续看下去。注 意这样的事实:有两种字节排列顺序:重要的字节 (有时叫 "octet",即八位位组) 在前面,或者不重要的字节在前面。前一种叫“网络字节顺序 (Network Byte Order)”。有些机器在内部是按照这个顺序储存数据,而另外 一些则不然。当我说某数据必须按照 NBO 顺序,那么你要调用函数(例如 htons() )来将它从本机字节顺序 (Host Byte Order) 转换过来。如果我没有 提到 NBO, 那么就让它保持本机字节顺序。 我的第一个结构(在这个技术手册TM中)--struct sockaddr.。这个结构 为许多类型的套接字储存套接字地址信息: struct sockaddr { unsigned short sa_family; /* 地址家族, AF_xxx */ char sa_data[14]; /*14字节协议地址*/ }; sa_family 能够是各种各样的类型,但是在这篇文章中都是 "AF_INET"。 sa_data包含套接字中的目标地址和端口信息。这好像有点 不明智。 为了处理struct sockaddr,程序员创造了一个并列的结构: struct sockaddr_in ("in" 代表 "Internet"。) struct sockaddr_in { short int sin_family; /* 通信类型 */ unsigned short int sin_port; /* 端口 */ struct in_addr sin_addr; /* Internet 地址 */ unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/ }; 用这个数据结构可以轻松处理套接字地址的基本元素。注意 sin_zero (它被加入到这个结构,并且长度和 struct sockaddr 一样) 应该使用函数 bzero() 或 memset() 来全部置零。 同时,这一重要的字节,一个指向 sockaddr_in结构体的指针也可以被指向结构体sockaddr并且代替它。这 样的话即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 struct sockaddr_in,并且在最后转换。同时,注意 sin_family 和 struct sockaddr 中的 sa_family 一致并能够设置为 "AF_INET"。最后,sin_port和 sin_addr 必须是网络字节顺序 (Network Byte Order)! 你也许会反对道:"但是,怎么让整个数据结构 struct in_addr sin_addr 按照网络字节顺序呢?" 要知道这个问题的答案,我们就要仔细的看一看这 个数据结构: struct in_addr, 有这样一个联合 (unions): /* Internet 地址 (一个与历史有关的结构) */ struct in_addr { unsigned long s_addr; }; 它曾经是个最坏的联合,但是现在那些日子过去了。如果你声明 "ina" 是数据结构 struct sockaddr_in 的实例,那么 "ina.sin_addr.s_addr" 就储 存4字节的 IP 地址(使用网络字节顺序)。如果你不幸的系统使用的还是恐 怖的联合 struct in_addr ,你还是可以放心4字节的 IP 地址并且和上面 我说的一样(这是因为使用了“#define”。) -------------------------------------------------------------------------------- 本机转换 我们现在到了新的章节。我们曾经讲了很多网络到本机字节顺序的转 换,现在可以实践了! 你能够转换两种类型: short (两个字节)和 long (四个字节)。这个函 数对于变量类型 unsigned 也适用。假设你想将 short 从本机字节顺序转 换为网络字节顺序。用 "h" 表示 "本机 (host)",接着是 "to",然后用 "n" 表 示 "网络 (network)",最后用 "s" 表示 "short": h-to-n-s, 或者 htons() ("Host to Network Short")。 太简单了... 如果不是太傻的话,你一定想到了由"n","h","s",和 "l"形成的正确 组合,例如这里肯定没有stolh() ("Short to Long Host") 函数,不仅在这里 没有,所有场合都没有。但是这里有: htons()--"Host to Network Short" htonl()--"Host to Network Long" ntohs()--"Network to Host Short" ntohl()--"Network to Host Long" 现在,你可能想你已经知道它们了。你也可能想:“如果我想改变 char 的顺序要怎么办呢?” 但是你也许马上就想到,“用不着考虑的”。你也许会想到:我的 68000 机器已经使用了网络字节顺序,我没有必要去调用 htonl() 转换 IP 地址。你可能是对的,但是当你移植你的程序到别的机器 上的时候,你的程序将失败。可移植性!这里是 Unix 世界!记住:在你将数据放到网络上的时候,确信它们是网络字节顺序的。 最后一点:为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢? 答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。 -------------------------------------------------------------------------------- IP 地址和如何处理它们 现在我们很幸运,因为我们有很多的函数来方便地操作 IP 地址。没有 必要用手工计算它们,也没有必要用"<<"操作来储存成长整字型。首先,假设你已经有了一个sockaddr_in结构体ina,你有一个IP地址"132.241.5.10"要储存在其中,你就要用到函数inet_addr(),将IP地址从 点数格式转换成无符号长整型。使用方法如下: ina.sin_addr.s_addr = inet_addr("132.241.5.10"); 注意,inet_addr()返回的地址已经是网络字节格式,所以你无需再调用 函数htonl()。 我们现在发现上面的代码片断不是十分完整的,因为它没有错误检查。 显而易见,当inet_addr()发生错误时返回-1。记住这些二进制数字?(无符号数)-1仅仅和IP地址255.255.255.255相符合!这可是广播地址!大错特 错!记住要先进行错误检查。 好了,现在你可以将IP地址转换成长整型了。有没有其相反的方法呢? 它可以将一个in_addr结构体输出成点数格式?这样的话,你就要用到函数 inet_ntoa()("ntoa"的含义是"network to ascii"),就像这样: printf("%s",inet_ntoa(ina.sin_addr)); 它将输出IP地址。需要注意的是inet_ntoa()将结构体in-addr作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的 指针。它是一个由inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的IP地址。例如: char *a1, *a2; . . a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */ a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */ printf("address 1: %s\n",a1); printf("address 2: %s\n",a2); 输出如下: address 1: 132.241.5.10 address 2: 132.241.5.10 假如你需要保存这个IP地址,使用strcopy()函数来指向你自己的字符 指针。 上面就是关于这个主题的介绍。稍后,你将学习将一个类 似"wintehouse.gov"的字符串转换成它所对应的IP地址(查阅域名服务,稍 后)。 -------------------------------------------------------------------------------- socket()函数 我想我不能再不提这个了-下面我将讨论一下socket()系统调用。 下面是详细介绍: #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); 但是它们的参数是什么? 首先,domain 应该设置成 "AF_INET",就 象上面的数据结构struct sockaddr_in 中一样。然后,参数 type 告诉内核 是 SOCK_STREAM 类型还是 SOCK_DGRAM 类型。最后,把 protocol 设置为 "0"。(注意:有很多种 domain、type,我不可能一一列出了,请看 socket() 的 man帮助。当然,还有一个"更好"的方式去得到 protocol。同 时请查阅 getprotobyname() 的 man 帮助。) socket() 只是返回你以后在系统调用种可能用到的 socket 描述符,或 者在错误的时候返回-1。全局变量 errno 中将储存返回的错误值。(请参考 perror() 的 man 帮助。) -------------------------------------------------------------------------------- bind()函数 一旦你有一个套接字,你可能要将套接字和机器上的一定的端口关联 起来。(如果你想用listen()来侦听一定端口的数据,这是必要一步--MUD 告 诉你说用命令 "telnet x.y.z 6969"。)如果你只想用 connect(),那么这个步骤没有必要。但是无论如何,请继续读下去。 这里是系统调用 bind() 的大概: #include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, int addrlen); sockfd 是调用 socket 返回的文件描述符。my_addr 是指向数据结构 struct sockaddr 的指针,它保存你的地址(即端口和 IP 地址) 信息。 addrlen 设置为 sizeof(struct sockaddr)。 简单得很不是吗? 再看看例子: #include <string.h> #include <sys/types.h> #include <sys/socket.h> #define MYPORT 3490 main() { int sockfd; struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); /*需要错误检查 */ my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = inet_addr("132.241.5.10"); bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ /* don't forget your error checking for bind(): */ bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); . . . 这里也有要注意的几件事情。my_addr.sin_port 是网络字节顺序, my_addr.sin_addr.s_addr 也是的。另外要注意到的事情是因系统的不同, 包含的头文件也不尽相同,请查阅本地的 man 帮助文件。 在 bind() 主题中最后要说的话是,在处理自己的 IP 地址和/或端口的 时候,有些工作是可以自动处理的。 my_addr.sin_port = 0; /* 随机选择一个没有使用的端口 */ my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的IP地址 */ 通过将0赋给 my_addr.sin_port,你告诉 bind() 自己选择合适的端 口。同样,将 my_addr.sin_addr.s_addr 设置为 INADDR_ANY,你告诉 它自动填上它所运行的机器的 IP 地址。 如果你一向小心谨慎,那么你可能注意到我没有将 INADDR_ANY 转 换为网络字节顺序!这是因为我知道内部的东西:INADDR_ANY 实际上就 是 0!即使你改变字节的顺序,0依然是0。但是完美主义者说应该处处一致,INADDR_ANY或许是12呢?你的代码就不能工作了,那么就看下面 的代码: my_addr.sin_port = htons(0); /* 随机选择一个没有使用的端口 */ my_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 使用自己的IP地址 */ 你或许不相信,上面的代码将可以随便移植。我只是想指出,既然你 所遇到的程序不会都运行使用htonl的INADDR_ANY。 bind() 在错误的时候依然是返回-1,并且设置全局错误变量errno。 在你调用 bind() 的时候,你要小心的另一件事情是:不要采用小于 1024的端口号。所有小于1024的端口号都被系统保留!你可以选择从1024 到65535的端口(如果它们没有被别的程序使用的话)。 你要注意的另外一件小事是:有时候你根本不需要调用它。如果你使 用 connect() 来和远程机器进行通讯,你不需要关心你的本地端口号(就象你在使用 telnet 的时候),你只要简单的调用 connect() 就可以了,它会检查套接字是否绑定端口,如果没有,它会自己绑定一个没有使用的本地端口。 -------------------------------------------------------------------------------- connect()程序 现在我们假设你是个 telnet 程序。你的用户命令你得到套接字的文件描述符。你听从命令调用了socket()。下一步,你的用户告诉你通过端口 23(标准 telnet 端口)连接到"132.241.5.10"。你该怎么做呢? 幸运的是,你正在阅读 connect()--如何连接到远程主机这一章。你可不想让你的用户失望。 connect() 系统调用是这样的: #include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); sockfd 是系统调用 socket() 返回的套接字文件描述符。serv_addr 是 保存着目的地端口和 IP 地址的数据结构 struct sockaddr。addrlen 设置 为 sizeof(struct sockaddr)。 想知道得更多吗?让我们来看个例子: #include <string.h> #include <sys/types.h> #include <sys/socket.h> #define DEST_IP "132.241.5.10" #define DEST_PORT 23 main() { int sockfd; struct sockaddr_in dest_addr; /* 目的地址*/ sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误检查 */ dest_addr.sin_family = AF_INET; /* host byte order */ dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */ dest_addr.sin_addr.s_addr = inet_addr(DEST_IP); bzero(&(dest_addr.sin_zero),; /* zero the rest of the struct */ /* don't forget to error check the connect()! */ connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)); . . . 再一次,你应该检查 connect() 的返回值--它在错误的时候返回-1,并 设置全局错误变量 errno。 同时,你可能看到,我没有调用 bind()。因为我不在乎本地的端口号。 我只关心我要去那。内核将为我选择一个合适的端口号,而我们所连接的 地方也自动地获得这些信息。一切都不用担心。 -------------------------------------------------------------------------------- listen()函数 是换换内容得时候了。假如你不希望与远程的一个地址相连,或者说, 仅仅是将它踢开,那你就需要等待接入请求并且用各种方法处理它们。处 理过程分两步:首先,你听--listen(),然后,你接受--accept() (请看下面的 内容)。 除了要一点解释外,系统调用 listen 也相当简单。 int listen(int sockfd, int backlog); sockfd 是调用 socket() 返回的套接字文件描述符。backlog 是在进入 队列中允许的连接数目。什么意思呢? 进入的连接是在队列中一直等待直到你接受 (accept() 请看下面的文章)连接。它们的数目限制于队列的允许。 大多数系统的允许数目是20,你也可以设置为5到10。 和别的函数一样,在发生错误的时候返回-1,并设置全局错误变量 errno。 你可能想象到了,在你调用 listen() 前你或者要调用 bind() 或者让内 核随便选择一个端口。如果你想侦听进入的连接,那么系统调用的顺序可 能是这样的: socket(); bind(); listen(); /* accept() 应该在这 */ 因为它相当的明了,我将在这里不给出例子了。(在 accept() 那一章的 代码将更加完全。)真正麻烦的部分在 accept()。 -------------------------------------------------------------------------------- accept()函数 准备好了,系统调用 accept() 会有点古怪的地方的!你可以想象发生 这样的事情:有人从很远的地方通过一个你在侦听 (listen()) 的端口连接 (connect()) 到你的机器。它的连接将加入到等待接受 (accept()) 的队列 中。你调用 accept() 告诉它你有空闲的连接。它将返回一个新的套接字文 件描述符!这样你就有两个套接字了,原来的一个还在侦听你的那个端口, 新的在准备发送 (send()) 和接收 ( recv()) 数据。这就是这个过程! 函数是这样定义的: #include <sys/socket.h> int accept(int sockfd, void *addr, int *addrlen); sockfd 相当简单,是和 listen() 中一样的套接字描述符。addr 是个指 向局部的数据结构 sockaddr_in 的指针。这是要求接入的信息所要去的地 方(你可以测定那个地址在那个端口呼叫你)。在它的地址传递给 accept 之 前,addrlen 是个局部的整形变量,设置为 sizeof(struct sockaddr_in)。 accept 将不会将多余的字节给 addr。如果你放入的少些,那么它会通过改 变 addrlen 的值反映出来。 同样,在错误时返回-1,并设置全局错误变量 errno。 现在是你应该熟悉的代码片段。 #include <string.h> #include <sys/socket.h> #include <sys/types.h> #define MYPORT 3490 /*用户接入端口*/ #define BACKLOG 10 /* 多少等待连接控制*/ main() { int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */ struct sockaddr_in my_addr; /* 地址信息 */ struct sockaddr_in their_addr; /* connector's address information */ int sin_size; sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误检查*/ my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */ bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ /* don't forget your error checking for these calls: */ bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); listen(sockfd, BACKLOG); sin_size = sizeof(struct sockaddr_in); new_fd = accept(sockfd, &their_addr, &sin_size); . . . 注意,在系统调用 send() 和 recv() 中你应该使用新的套接字描述符 new_fd。如果你只想让一个连接进来,那么你可以使用 close() 去关闭原 来的文件描述符 sockfd 来避免同一个端口更多的连接。 -------------------------------------------------------------------------------- send() and recv()函数 这两个函数用于流式套接字或者数据报套接字的通讯。如果你喜欢使 用无连接的数据报套接字,你应该看一看下面关于sendto() 和 recvfrom() 的章节。 send() 是这样的: int send(int sockfd, const void *msg, int len, int flags); sockfd 是你想发送数据的套接字描述符(或者是调用 socket() 或者是 accept() 返回的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就可以了。(详细的资料请看 send() 的 man page)。 这里是一些可能的例子: char *msg = "Beej was here!"; int len, bytes_sent; . . len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0); . . . send() 返回实际发送的数据的字节数--它可能小于你要求发送的数 目! 注意,有时候你告诉它要发送一堆数据可是它不能处理成功。它只是发送它可能发送的数据,然后希望你能够发送其它的数据。记住,如果 send() 返回的数据和 len 不匹配,你就应该发送其它的数据。但是这里也有个好消息:如果你要发送的包很小(小于大约 1K),它可能处理让数据一 次发送完。最后要说得就是,它在错误的时候返回-1,并设置 errno。 recv() 函数很相似: int recv(int sockfd, void *buf, int len, unsigned int flags); sockfd 是要读的套接字描述符。buf 是要读的信息的缓冲。len 是缓 冲的最大长度。flags 可以设置为0。(请参考recv() 的 man page。) recv() 返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。 很简单,不是吗? 你现在可以在流式套接字上发送数据和接收数据了。 你现在是 Unix 网络程序员了! -------------------------------------------------------------------------------- sendto() 和 recvfrom()函数 “这很不错啊”,你说,“但是你还没有讲无连接数据报套接字呢?” 没问题,现在我们开始这个内容。 既然数据报套接字不是连接到远程主机的,那么在我们发送一个包之 前需要什么信息呢? 不错,是目标地址!看看下面的: int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen); 你已经看到了,除了另外的两个信息外,其余的和函数 send() 是一样 的。 to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为 sizeof(struct sockaddr)。 和函数 send() 类似,sendto() 返回实际发送的字节数(它也可能小于 你想要发送的字节数!),或者在错误的时候返回 -1。 相似的还有函数 recv() 和 recvfrom()。recvfrom() 的定义是这样的: int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen); 又一次,除了两个增加的参数外,这个函数和 recv() 也是一样的。from 是一个指向局部数据结构 struct sockaddr 的指针,它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。 recvfrom() 返回收到的字节长度,或者在发生错误后返回 -1。 记住,如果你用 connect() 连接一个数据报套接字,你可以简单的调 用 send() 和 recv() 来满足你的要求。这个时候依然是数据报套接字,依 然使用 UDP,系统套接字接口会为你自动加上了目标和源的信息。 -------------------------------------------------------------------------------- close()和shutdown()函数 你已经整天都在发送 (send()) 和接收 (recv()) 数据了,现在你准备关 闭你的套接字描述符了。这很简单,你可以使用一般的 Unix 文件描述符 的 close() 函数: close(sockfd); 它将防止套接字上更多的数据的读写。任何在另一端读写套接字的企 图都将返回错误信息。 如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭,你可以使用: int shutdown(int sockfd, int how); sockfd 是你想要关闭的套接字文件描述复。how 的值是下面的其中之 一: 0 - 不允许接受 1 - 不允许发送 2 - 不允许发送和接受(和 close() 一样) shutdown() 成功时返回 0,失败时返回 -1(同时设置 errno。) 如果在无连接的数据报套接字中使用shutdown(),那么只不过是让 send() 和 recv() 不能使用(记住你在数据报套接字中使用了 connect 后 是可以使用它们的)。 -------------------------------------------------------------------------------- getpeername()函数 这个函数太简单了。 它太简单了,以至我都不想单列一章。但是我还是这样做了。 函数 getpeername() 告诉你在连接的流式套接字上谁在另外一边。函 数是这样的: #include <sys/socket.h> int getpeername(int sockfd, struct sockaddr *addr, int *addrlen); sockfd 是连接的流式套接字的描述符。addr 是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,它保存着连接的另一边的 信息。addrlen 是一个 int 型的指针,它初始化为 sizeof(struct sockaddr)。函数在错误的时候返回 -1,设置相应的 errno。 一旦你获得它们的地址,你可以使用 inet_ntoa() 或者 gethostbyaddr() 来打印或者获得更多的信息。但是你不能得到它的帐号。(如果它运行着愚蠢的守护进程,这是可能的,但是它的讨论已经超出了本文的范围,请参 考 RFC-1413 以获得更多的信息。) -------------------------------------------------------------------------------- gethostname()函数 甚至比 getpeername() 还简单的函数是 gethostname()。它返回你程 序所运行的机器的主机名字。然后你可以使用 gethostbyname() 以获得你 的机器的 IP 地址。 下面是定义: #include <unistd.h> int gethostname(char *hostname, size_t size); 参数很简单:hostname 是一个字符数组指针,它将在函数返回时保存 主机名。size是hostname 数组的字节长度。 函数调用成功时返回 0,失败时返回 -1,并设置 errno。 -------------------------------------------------------------------------------- 域名服务(DNS) 如果你不知道 DNS 的意思,那么我告诉你,它代表域名服务(Domain Name Service)。它主要的功能是:你给它一个容易记忆的某站点的地址, 它给你 IP 地址(然后你就可以使用 bind(), connect(), sendto() 或者其它 函数) 。当一个人输入: $ telnet whitehouse.gov telnet 能知道它将连接 (connect()) 到 "198.137.240.100"。 但是这是如何工作的呢? 你可以调用函数 gethostbyname(): #include <netdb.h> struct hostent *gethostbyname(const char *name); 很明白的是,它返回一个指向 struct hostent 的指针。这个数据结构 是这样的: struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; }; #define h_addr h_addr_list[0] 这里是这个数据结构的详细资料: struct hostent: h_name - 地址的正式名称。 h_aliases - 空字节-地址的预备名称的指针。 h_addrtype -地址类型; 通常是AF_INET。 h_length - 地址的比特长度。 h_addr_list - 零字节-主机网络地址指针。网络字节顺序。 h_addr - h_addr_list中的第一地址。 gethostbyname() 成功时返回一个指向结构体 hostent 的指针,或者 是个空 (NULL) 指针。(但是和以前不同,不设置errno,h_errno 设置错 误信息。请看下面的 herror()。) 但是如何使用呢? 有时候(我们可以从电脑手册中发现),向读者灌输 信息是不够的。这个函数可不象它看上去那么难用。 这里是个例子: #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> int main(int argc, char *argv[]) { struct hostent *h; if (argc != 2) { /* 检查命令行 */ fprintf(stderr,"usage: getip address\n"); exit(1); } if ((h=gethostbyname(argv[1])) == NULL) { /* 取得地址信息 */ herror("gethostbyname"); exit(1); } printf("Host name : %s\n", h->h_name); printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr))); return 0; } 在使用 gethostbyname() 的时候,你不能用 perror() 打印错误信息 (因为 errno 没有使用),你应该调用 herror()。 相当简单,你只是传递一个保存机器名的字符串(例如 "whitehouse.gov") 给 gethostbyname(),然后从返回的数据结构 struct hostent 中获取信息。 唯一也许让人不解的是输出 IP 地址信息。h->h_addr 是一个 char *, 但是 inet_ntoa() 需要的是 struct in_addr。因此,我转换 h->h_addr 成 struct in_addr *,然后得到数据。 -------------------------------------------------------------------------------- 客户-服务器背景知识 这里是个客户--服务器的世界。在网络上的所有东西都是在处理客户进 程和服务器进程的交谈。举个telnet 的例子。当你用 telnet (客户)通过23 号端口登陆到主机,主机上运行的一个程序(一般叫 telnetd,服务器)激活。 它处理这个连接,显示登陆界面,等等。 图2:客户机和服务器的关系 图 2 说明了客户和服务器之间的信息交换。 注意,客户--服务器之间可以使用SOCK_STREAM、SOCK_DGRAM 或者其它(只要它们采用相同的)。一些很好的客户--服务器的例子有 telnet/telnetd、 ftp/ftpd 和 bootp/bootpd。每次你使用 ftp 的时候,在远 端都有一个 ftpd 为你服务。 一般,在服务端只有一个服务器,它采用 fork() 来处理多个客户的连 接。基本的程序是:服务器等待一个连接,接受 (accept()) 连接,然后 fork() 一个子进程处理它。这是下一章我们的例子中会讲到的。 -------------------------------------------------------------------------------- 简单的服务器 这个服务器所做的全部工作是在流式连接上发送字符串 "Hello, World!\n"。你要测试这个程序的话,可以在一台机器上运行该程序,然后 在另外一机器上登陆: $ telnet remotehostname 3490 remotehostname 是该程序运行的机器的名字。 服务器代码: #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #define MYPORT 3490 /*定义用户连接端口*/ #define BACKLOG 10 /*多少等待连接控制*/ main() { int sockfd, new_fd; /* listen on sock_fd, new connection on new_fd */ struct sockaddr_in my_addr; /* my address information */ struct sockaddr_in their_addr; /* connector's address information */ int sin_size; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */ bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))== -1) { perror("bind"); exit(1); } if (listen(sockfd, BACKLOG) == -1) { perror("listen"); exit(1); } while(1) { /* main accept() loop */ sin_size = sizeof(struct sockaddr_in); if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, \ &sin_size)) == -1) { perror("accept"); continue; } printf("server: got connection from %s\n", \ inet_ntoa(their_addr.sin_addr)); if (!fork()) { /* this is the child process */ if (send(new_fd, "Hello, world!\n", 14, 0) == -1) perror("send"); close(new_fd); exit(0); } close(new_fd); /* parent doesn't need this */ while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */ } } 如果你很挑剔的话,一定不满意我所有的代码都在一个很大的main() 函数中。如果你不喜欢,可以划分得更细点。 你也可以用我们下一章中的程序得到服务器端发送的字符串。 -------------------------------------------------------------------------------- 简单的客户程序 这个程序比服务器还简单。这个程序的所有工作是通过 3490 端口连接到命令行中指定的主机,然后得到服务器发送的字符串。 客户代码: #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #define PORT 3490 /* 客户机连接远程主机的端口 */ #define MAXDATASIZE 100 /* 每次可以接收的最大字节 */ int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct hostent *he; struct sockaddr_in their_addr; /* connector's address information */ if (argc != 2) { fprintf(stderr,"usage: client hostname\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */ herror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; /* host byte order */ their_addr.sin_port = htons(PORT); /* short, network byte order */ their_addr.sin_addr = *((struct in_addr *)he->h_addr); bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */ if (connect(sockfd, (struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("Received: %s",buf); close(sockfd); return 0; } 注意,如果你在运行服务器之前运行客户程序,connect() 将返回 "Connection refused" 信息,这非常有用。 -------------------------------------------------------------------------------- 数据包 Sockets 我不想讲更多了,所以我给出代码 talker.c 和 listener.c。 listener 在机器上等待在端口 4590 来的数据包。talker 发送数据包到 一定的机器,它包含用户在命令行输入的内容。 这里就是 listener.c: #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #define MYPORT 4950 /* the port users will be sending to */ #define MAXBUFLEN 100 main() { int sockfd; struct sockaddr_in my_addr; /* my address information */ struct sockaddr_in their_addr; /* connector's address information */ int addr_len, numbytes; char buf[MAXBUFLEN]; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */ bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */ if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \ == -1) { perror("bind"); exit(1); } addr_len = sizeof(struct sockaddr); if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, \ (struct sockaddr *)&their_addr, &addr_len)) == -1) { perror("recvfrom"); exit(1); } printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr)); printf("packet is %d bytes long\n",numbytes); buf[numbytes] = '\0'; printf("packet contains \"%s\"\n",buf); close(sockfd); } 注意在我们的调用 socket(),我们最后使用了 SOCK_DGRAM。同时, 没有必要去使用 listen() 或者 accept()。我们在使用无连接的数据报套接 字! 下面是 talker.c: #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #define MYPORT 4950 /* the port users will be sending to */ int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; /* connector's address information */ struct hostent *he; int numbytes; if (argc != 3) { fprintf(stderr,"usage: talker hostname message\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */ herror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; /* host byte order */ their_addr.sin_port = htons(MYPORT); /* short, network byte order */ their_addr.sin_addr = *((struct in_addr *)he->h_addr); bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */ if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, \ (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) { perror("sendto"); exit(1); } printf("sent %d bytes to %s\n",numbytes,inet_ntoa(their_addr.sin_addr)); close(sockfd); return 0; } 这就是所有的了。在一台机器上运行 listener,然后在另外一台机器上 运行 talker。观察它们的通讯! 除了一些我在上面提到的数据套接字连接的小细节外,对于数据套接 字,我还得说一些,当一个讲话者呼叫connect()函数时并指定接受者的地址时,从这点可以看出,讲话者只能向connect()函数指定的地址发送和接受信息。因此,你不需要使用sendto()和recvfrom(),你完全可以用send() 和recv()代替。 -------------------------------------------------------------------------------- 阻塞 阻塞,你也许早就听说了。"阻塞"是 "sleep" 的科技行话。你可能注意 到前面运行的 listener 程序,它在那里不停地运行,等待数据包的到来。 实际在运行的是它调用 recvfrom(),然后没有数据,因此 recvfrom() 说" 阻塞 (block)",直到数据的到来。 很多函数都利用阻塞。accept() 阻塞,所有的 recv*() 函数阻塞。它们之所以能这样做是因为它们被允许这样做。当你第一次调用 socket() 建立套接字描述符的时候,内核就将它设置为阻塞。如果你不想套接字阻塞, 你就要调用函数 fcntl(): #include <unistd.h> #include <fontl.h> . . sockfd = socket(AF_INET, SOCK_STREAM, 0); fcntl(sockfd, F_SETFL, O_NONBLOCK); . . 通过设置套接字为非阻塞,你能够有效地"询问"套接字以获得信息。如 果你尝试着从一个非阻塞的套接字读信息并且没有任何数据,它不允许阻 塞--它将返回 -1 并将 errno 设置为 EWOULDBLOCK。 但是一般说来,这种询问不是个好主意。如果你让你的程序在忙等状 态查询套接字的数据,你将浪费大量的 CPU 时间。更好的解决之道是用 下一章讲的 select() 去查询是否有数据要读进来。 -------------------------------------------------------------------------------- select()--多路同步 I/O 虽然这个函数有点奇怪,但是它很有用。假设这样的情况:你是个服 务器,你一边在不停地从连接上读数据,一边在侦听连接上的信息。没问题,你可能会说,不就是一个 accept() 和两个 recv() 吗? 这么 容易吗,朋友? 如果你在调用 accept() 的时候阻塞呢? 你怎么能够同时接 受 recv() 数据? “用非阻塞的套接字啊!” 不行!你不想耗尽所有的 CPU 吧? 那么,该如何是好? select() 让你可以同时监视多个套接字。如果你想知道的话,那么它就 会告诉你哪个套接字准备读,哪个又准备写,哪个套接字又发生了例外 (exception)。 闲话少说,下面是 select(): #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); 这个函数监视一系列文件描述符,特别是 readfds、writefds 和 exceptfds。如果你想知道你是否能够从标准输入和套接字描述符 sockfd 读入数据,你只要将文件描述符 0 和 sockfd 加入到集合 readfds 中。参 数 numfds 应该等于最高的文件描述符的值加1。在这个例子中,你应该 设置该值为 sockfd+1。因为它一定大于标准输入的文件描述符 (0)。 当函数 select() 返回的时候,readfds 的值修改为反映你选择的哪个 文件描述符可以读。你可以用下面讲到的宏 FD_ISSET() 来测试。 在我们继续下去之前,让我来讲讲如何对这些集合进行操作。每个集 合类型都是 fd_set。下面有一些宏来对这个类型进行操作: FD_ZERO(fd_set *set) - 清除一个文件描述符集合 FD_SET(int fd, fd_set *set) - 添加fd到集合 FD_CLR(int fd, fd_set *set) - 从集合中移去fd FD_ISSET(int fd, fd_set *set) - 测试fd是否在集合中 最后,是有点古怪的数据结构 struct timeval。有时你可不想永远等待别人发送数据过来。也许什么事情都没有发生的时候你也想每隔96秒在终 端上打印字符串 "Still Going..."。这个数据结构允许你设定一个时间,如果 时间到了,而 select() 还没有找到一个准备好的文件描述符,它将返回让你继续处理。 数据结构 struct timeval 是这样的: struct timeval { int tv_sec; /* seconds */ int tv_usec; /* microseconds */ }; 只要将 tv_sec 设置为你要等待的秒数,将 tv_usec 设置为你要等待的微秒数就可以了。是的,是微秒而不是毫秒。1,000微秒等于1毫秒,1,000 毫秒等于1秒。也就是说,1秒等于1,000,000微秒。为什么用符号 "usec" 呢? 字母 "u" 很象希腊字母 Mu,而 Mu 表示 "微" 的意思。当然,函数 返回的时候 timeout 可能是剩余的时间,之所以是可能,是因为它依赖于 你的 Unix 操作系统。 哈!我们现在有一个微秒级的定时器!别计算了,标准的 Unix 系统 的时间片是100毫秒,所以无论你如何设置你的数据结构 struct timeval, 你都要等待那么长的时间。 还有一些有趣的事情:如果你设置数据结构 struct timeval 中的数据为 0,select() 将立即超时,这样就可以有效地轮询集合中的所有的文件描述 符。如果你将参数 timeout 赋值为 NULL,那么将永远不会发生超时,即一直等到第一个文件描述符就绪。最后,如果你不是很关心等待多长时间, 那么就把它赋为 NULL 吧。 下面的代码演示了在标准输入上等待 2.5 秒: #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define STDIN 0 /* file descriptor for standard input */ main() { struct timeval tv; fd_set readfds; tv.tv_sec = 2; tv.tv_usec = 500000; FD_ZERO(&readfds); FD_SET(STDIN, &readfds); /* don't care about writefds and exceptfds: */ select(STDIN+1, &readfds, NULL, NULL, &tv); if (FD_ISSET(STDIN, &readfds)) printf("A key was pressed!\n"); else printf("Timed out.\n"); } 如果你是在一个 line buffered 终端上,那么你敲的键应该是回车 (RETURN),否则无论如何它都会超时。 现在,你可能回认为这就是在数据报套接字上等待数据的方式--你是对 的:它可能是。有些 Unix 系统可以按这种方式,而另外一些则不能。你 在尝试以前可能要先看看本系统的 man page 了。 最后一件关于 select() 的事情:如果你有一个正在侦听 (listen()) 的套 接字,你可以通过将该套接字的文件描述符加入到 readfds 集合中来看是 否有新的连接。 这就是我关于函数select() 要讲的所有的东西。 2008年4月 CHROOT就是Change Root,也就是改变程序执行时所参考的根目录位置。 一般的目录架构: / /bin /sbin /usr/bin /home CHROOT的目录架构: /hell/ /hell/bin /hell/usr/bin /hell/home * 为何要CHROOT? 1.限制被CHROOT的使用者所能执行的程序,如SetUid的程序,或是会造成 Load 的 Compiler等等。 2.防止使用者存取某些特定档案,如/etc/passwd。 3.防止入侵者/bin/rm -rf /。 4.提供Guest服务以及处罚不乖的使用者。 5.增进系统的安全。 * 要如何建立CHROOT的环境? 1.chroot()这个function: chroot(PATH)这个function必须具有 root 的身份才能执行,执行后会 将跟目录切换到 PATH 所指定的地方。 2.login的过程: 使用者无论是从console或是telnet进入,都必须执行/usr/bin/login来 决定是否能进入系统,而login所做的动作大致是: (1)印出login的提示符号,等待使用者输入密码。 (2)检查密码是否正确,错误的话回到(1)。 (3)正确的话以setuid()来改变身份为login_user。 (4)以exec()执行user的shell。 因此我们必须先修改/usr/bin/login的source code,让login在(2)到(3) 的中间执行chroot($CHROOT_PATH)的动作,已达到CHROOT的目的,并以修 改过的login替代原先的/usr/bin/login。 (5)稍微好一点的方法必须在做chroot()之前检查login user的group,如果有某个特定的group(如chrootgrp) 才执行chroot(),不然所有的人都会被chroot了。 3.建立CHROOT所需的环境: (1)必须具备的目录:(假设$CHROOT为希望建立的路径) $CHROOT/etc $CHROOT/lib $CHROOT/bin $CHROOT/sbin $CHROOT/usr/lib $CHROOT/usr/bin $CHROOT/usr/bin $CHROOT/usr/local $CHROOT/home (2)仔细审查/etc中的档案,需具备执行程序时所需的文件 案,如passwd,groups,hosts,resolv.conf等等。 (3)拿掉不想给的执行档,如su,sudo等SetUid的程序, 以及compiler甚至telnet。 (4)测试一下,以root身份执行 chroot $CHROOT /bin/sh 即可进入CHROOT环境中。(man chroot for details) 4.在console或是以telnet进入试试。 5.Username/Password Resolve的考虑: 在CHROOT时你可能不希望被CHROOT的使用者(以后简 称CHROOTer)能拿到/etc/passwd或是/etc/shadow等檔 案,尤其是有root密码的。以下有三种情形: (1)/etc/passwd跟 $CHROOT/etc/passwd相同: 这是最差的作法,因为一来被CHROOTer有机会得到root 的encrypted password,二来要保持/etc/passwd及 $CHROOT/etc/passwd的同步性是个大问题。因为 /usr/bin/login参考的是/etc/passwd,可是一旦 CHROOTer被chroot后执行passwd时,他所执行的 passwd所更改的将是$CHROOT/etc/passwd。 (2)/etc/passwd跟$CHROOT/etc/passwd不同: 你可以把$CHROOT/etc/passwd中的重要人物(如root) 的密码拿掉,然后以比较复杂的方法修改 /usr/bin/login: if (has_chroot_group) { re-load $CHROOT/etc/passwd if (password is valid) { chroot($CHROOT) exec(shell) } else logout() } 此法的好处是你可以将/etc/passwd跟 $CHROOT/etc/passwd分开来。/etc/passwd只影响 CHROOTer在login时所使用的username,其它如 password甚至uid,gid,shell,home等等都是参 考$CHROOT/etc/passwd的。 缺点是你其它的daemon如ftpd,httpd都必须做相同 的修改才能正确取的CHROOTer的信息,而且你在把一 个user加入或移出chroot_group时都必须更改 /etc/passwd跟$CHROOT/etc/passwd。 (3)使用NIS/YP: 此法大概是最简单,且麻烦最少的了。因为一切的user information都经过NIS Bind来取得,不但可以保护住 root的密码,也省去/etc/passwd跟 $CHROOT/etc/passwd同步管理上的问题。不只是 passwd,连其它如groups,hosts,services, aliases等等都可以一并解决。 * 其它必须考虑的问题: 1.执行档的同步性: 再更新系统或是更新软件时,必须考虑到一并更换 $CHROOT目录下的档案,尤其如SunOS或是BSD等会用 nlist()来取得Kernel Information的,在更新kernel 时必须更新$CHROOT下的kernel。 2./dev的问题: 一般而言你必须用local loopback NFS将/dev read- write mount到$CHROOT/dev以使得一般user跟CHROOTer 可以互相write以及解决devices同步性的问题。 3./proc的问题: 在Linux或是SYSV或是4.4BSD的系统上许多程序会去 参考/proc的数据,你必须也将/proc mount到 $CHROOT/proc。 4./var的问题: 一般而言/var也是用local loopback NFS read-write mount到$CHROOT/var下,以解决spool同步性的问题, 否则你可能必须要修改lpd或是sendmail等daemon, 不然他们是不知道$CHROOT/var下也有spool的存在。 5.Daemon的问题: 你必须修改一些跟使用者相关的Daemon如ftpd,httpd 以使这些daemon能找到正确的user home。 * CHROOT无法解决的安全问题: 1.不小心或是忘记拿掉SetUid的程序: CHROOTer还是有机会利用SetUid的程序来取得root的 权限,不过因为你已经将他CHROOT了,所以所能影响到 的只有$CHROOT/目录以下的档案,就算他来个 "/bin/rm -rf /" 也不怕了。 不过其它root能做的事还是防不了,如利用tcpdump来 窃听该localnet中的通讯并取得在该localnet上其它 机器的账号密码,reboot机器,更改NIS的数据,更改 其它没有被CHROOT的账号的密码藉以取得一般账号(所 以root不可加入NIS中)等等。 (此时就必须藉由securetty或是login.access或是将 wheel group拿出NIS来防止其login as root) 2.已加载内存中的Daemon: 对于那些一开机就执行的程序如sendmail,httpd, gopherd,inetd等等,如果这些daemon有hole(如 sendmail),那hacker只要破解这些daemon还是可以取 得root权限。 * 结论: CHROOT可以增进系统的安全性,限制使用者能做的事, 但是CHROOT Is Not Everything,因为还是有其它的 漏洞等着hacker来找出来。 通过Chroot机制让服务器安全到底 所谓"监牢"就是指通过chroot机制来更改某个进程所能看到的根目录,即将某进程限制在指定目录中,保证该进程只能对该目录及其子目录的文件有所动作,从而保证整个服务器的安全。 创建chroot"监牢" 以前,Unix/Linux上的daemon都是以root权限启动的。当时,这似乎是一件理所当然的事情,因为像Apache这样的服务器软件需要绑定到"众所周知"的端口上(小于1024)来监听HTTP请求,而root是惟一有这种权限的用户。 但是,随着攻击者活动的日益频繁,尤其是缓冲区溢出漏洞数量的激增,使服务器安全受到了更大的威胁。一旦某个网络服务存在漏洞,攻击者就能够访问并控制整个系统。因此,为了减缓这种攻击所带来的负面影响,现在服务器软件通常设计为以root权限启动,然后服务器进程自行放弃root,再以某个低权限的系统账号来运行进程。这种方式的好处在于一旦该服务被攻击者利用漏洞入侵,由于进程权限很低,攻击者得到的访问权限又是基于这个较低权限的,对系统造成的危害比以前减轻了许多。 有些攻击者会试图找到系统其它的漏洞来提升权限,直至达到root。由于本地安全性远低于远程安全保护,因此攻击者很有可能在系统中找到可以提升权限的东西。即使没有找到本地漏洞,攻击者也可能会造成其它损害,如删除文件、涂改主页等。 为了进一步提高系统安全性,Linux内核引入了chroot机制。chroot是内核中的一个系统调用,软件可以通过调用库函数chroot,来更改某个进程所能见到的根目录。比如,Apache软件安装在/usr/local/httpd/目录下,以root用户(或具有相同权限的其它账号)启动Apache,这个root权限的父进程会派生数个以nobody权限运行的子进程,具体情况取决于个人设置。父进程监听请求自80端口的tcp数据流,然后根据内部算法将这个请求分配给某个子进程来处理。这时Apache子进程所处的目录继承自父进程,即/usr/local/httpd/。 但是,一旦目录权限设定失误,被攻击的Apache子进程可以访问/usr/local、/usr、/tmp,甚至整个文件系统,因为Apache 进程所处的根目录仍是整个文件系统的根。如果能够利用chroot将Apache限制在/usr/local/httpd/,那么,Apache所能存取的文件都是/usr/local/httpd/下的文件或其子目录下的文件。创建chroot"监牢"的作用就是将进程权限限制在文件系统目录树中的某一子树中。 为什么需要jail 将软件chroot化的一个问题是该软件运行时需要的所有程序、配置文件和库文件都必须事先安装到chroot目录中,通常称这个目录为 chroot jail(chroot"监牢")。如果要在"监牢"中运行/sbin/httpd,而事实上根本看不到文件系统中那个真正的/sbin目录。因此需要事先创建/sbin目录,并将httpd复制到其中。同时httpd需要几个库文件,执行如下命令可以看到这些库文件(在真实的文件系统下运行)。 #ldd /sbin/httpd libaprutil-0.so.0 => /usr/local/httpd/lib/libaprutil-0.so.0 (0x40017000) libgdbm.so.2 => /usr/lib/libgdbm.so.2 (0x4003c000) libdb-4.0.so => /lib/libdb-4.0.so (0x40043000) libpthread.so.0 => /lib/tls/libpthread.so.0 (0x400eb000) libexpat.so.0 => /usr/lib/libexpat.so.0 (0x400f8000) libapr-0.so.0 => /usr/local/httpd/lib/libapr-0.so.0 (0x40118000) librt.so.1 => /lib/librt.so.1 (0x40139000) lIBM.so.6 => /lib/tls/lIBM.so.6 (0x4014b000) libcrypt.so.1 => /lib/libcrypt.so.1 (0x4016d000) libnsl.so.1 => /lib/libnsl.so.1 (0x4019a000) libdl.so.2 => /lib/libdl.so.2 (0x401af000) libc.so.6 => /lib/tls/libc.so.6 (0x42000000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) 这意味着还需要在"监牢"中创建lib目录,并将库文件复制到其中。这一工作可以交由计算机完成,用jail等软件包来帮助简化chroot"监牢"建立的过程。 编译和安装jail 从http://www.jmcresearch.com/static/dwn/projects/jail/jail.tar.gz可以下载到jail的最新版本,它是由位于http://www.jmcresearch.com/projects/jail/的jail chroot项目小组开发的。该软件包包含了帮助自动创建chroot"监牢"的C程序、Perl程序和Bash脚本。 首先将jail.tar.gz置于任意目录,然后执行命令: #tar xzvf jail.tar.gz && cd jail/src 按照个人实际情况修改makefile文件,尤其是安装路径(默认安装路径是/usr/local)、体系结构(jail支持Linux、FreeBSD、IRIX和Solaris),以及编译选项等。最后执行命令: #make && make install 为jail创建chroot"监牢" 现在创建一个目录作为chroot"监牢",以/var/chroot/为例。执行下面的命令为chroot"监牢"创建环境: #/usr/local/bin/mkjailenv /var/chroot 这样"监牢"就建好了。jail软件包提供了几个Perl脚本作为其核心命令,包括mkjailenv、addjailuser和 addjailsw。如addjailsw会从真实文件系统中拷贝二进制可执行文件及其相关的其它文件(包括库文件、辅助性文件和设备文件)到该"监牢" 中。 为jail"监牢"添加软件 接下来需要为这个"监牢"增加一些软件,以便让它运行起来。执行以下命令安装一些基本的软件,包括ls、cat、cp等程序和ld-linux.so.2等库文件。 #/usr/local/bin/addjailsw /var/chroot 事实上仅有这些基本软件是不够的,还需要把一些真正有用的东西限制起来。下面的例子展示了为"监牢"添加arp程序的过程: #/usr/local/bin/addjailsw /var/chroot -P arp addjailsw A component of Jail (version 1.9 for linux) http://www.jmcresearch.com/projects/jail/ Juan M. Casillas <juanm.casillas@jmcresearch.com> Guessing arp args(0) Warning: file .//lib/tls/libc.so.6 exists. Overwritting it Warning: file .//lib/ld-linux.so.2 exists. Overwritting it Warning: file .//etc/ld.so.cache exists. Overwritting it Warning: file .//usr/lib/locale/locale-archive exists. Overwritting it Warning: file .//usr/share/locale/locale.alias exists. Overwritting it Warning: can't create /proc/net/arp from the /proc filesystem Done. 再以Apache服务器软件为例: #addjailsw /var/chroot/ -P /usr/local/httpd/bin/httpd addjailsw A component of Jail (version 1.9 for linux) http://www.jmcresearch.com/projects/jail/ Juan M. Casillas <juanm.casillas@jmcresearch.com> Guessing /usr/local/httpd/bin/httpd args(0) Warning: file /var/chroot//lib/libssl.so.4 exists. Overwritting it Warning: file /var/chroot//lib/libcrypto.so.4 exists. Overwritting it Warning: file /var/chroot//lib/libresolv.so.2 exists. Overwritting it …… Done. 不用在意那些警告信息,因为jail会调用ldd检查httpd用到的库文件。而几乎所有基于共享库的二进制可执行文件都需要上述的几个库文件。 接下来将Apache的相关文件拷贝到"监牢"中: #cp -a /usr/local/httpd/ /var/chroot/usr/local/ 可根据个人情况依次将Apache需要的文件复制到"监牢"中。 "监禁"囚犯 有时候需要为chroot"监牢"创建新的用户,比如Apache要求创建nobody用户作为子进程用户。鉴于可能有其它进程使用nobody,还可以使用另一用户——httpd。首先需要在真实系统中创建httpd用户: #useradd -d /var/chroot -s /usr/local/bin/jail httpd 然后执行以下命令在chroot"监牢"中创建httpd用户: #/usr/local/bin/addjailuser /var/chroot /usr/local/httpd /usr/sbin/httpd httpd 接下来修改/var/chroot/usr/local/httpd/conf/httpd.conf,将User nobody替换为User httpd。由于chroot后Apache将以httpd身份启动进程,只有root有权将Apache绑定在低端口上(通常为80),因此还需要修改端口值,该值必须大于1024(假设为8080)。这个修改要应用到Apache的所有配置文件中,包括虚拟主机的配置。至于Apache的其它设置,与在真实文件系统时一样配置即可。 接下来需要复制一些其它的文件。启动Apache最常见的方式就是调用apachectl,这是个Bash脚本。查看这个文件,会发现如下行: HTTPD='/usr/local/httpd/bin/httpd' LYNX="lynx -dump" ULIMIT_MAX_FILES="ulimit -S -n `ulimit -H -n`" ARGV="-h" $HTTPD -k $ARGV $HTTPD -k start -DSSL $HTTPD -t $LYNX $STATUSURL | awk ' /process$/ { print; exit } { print } ' 其中ulimit、lynx和awk是辅助性的程序。另外需要注意的是,程序使用不同的参数时,可能会使用不同的库文件,因此为了让Apache完整运行,使用下面的命令来跟踪所有可能的文件: #/usr/local/bin/addjailsw /var/chroot -P httpd "-k start -DSSL" 用上述的参数替换引号中的参数,完成全部的工作。 最后,让成功jail的Apache运行起来: #su - httpd & 打开浏览器进行测试,访问Web服务器时记住加上8080端口号。 jail高级应用 在前面的介绍中,使用了jail软件包中的三个Perl脚本。这里详细介绍这三个脚本的使用,以便高级用户使用。 mkjailenv 用法:mkjailenv chrootdir 作用:创建chroot"监牢"目录,并且从真实文件系统中拷贝基本的软件环境。 参数: chrootdir指定chroot"监牢"的路径。 addjailsw 用法:addjailsw chrootdir [-D] [-P program args] 作用:从真实文件系统中拷贝指定的文件及其相关文件。 参数: chrootdir指定chroot"监牢"的路径。 -D显示详细信息。 -P program args指定要添加到"监牢"中的软件。program可以是个文件名,也可以是文件的完整路径;args是参数。比如可以这样执行addjailsw: #addjailsw /var/chroot -P vi "-c q" addjailuser 用法:addjailuser chrootdir userdir usershell username 作用:创建新的chroot"监牢"用户。 参数: chrootdir指定chroot"监牢"的路径。 userdir指定新添加用户的主目录(相对于chroot"监牢"目录)。 usershell指定新用户使用的Shell的完整路径(比如/bin/bash)。 username为新添加的用户名。 比如: #addjailuser /var/chroot /home/ftp /bin/csh ftp 这个脚本会自动修改"监牢"中的/etc/passwd、/etc/group和/etc/shadow文件。 从上文看,如果仅使Apache一个软件运行在"监牢"中,mkjailenv似乎过于"热心"了,因此可以不运行mkjailenv /var/chroot命令,而只运行addjailsw /var/chroot -P httpd或在调试完chroot"监牢"后删除多余的文件,并修改/etc/passwd中多余的用户信息。由此想到,现在大多数流行的Web站点都采用Apache+PHP+MySQL+SSL的搭配(可能还会有FTP、Mail、Perl等组件),因此完全可以建立一个综合的Web"监牢"。系统管理员可以为这个"监牢"设置软件环境,当然这个环境只包括维护Apache+PHP+MySQL+SSL这些组件的必备工具,如使用Bash、SSH、编译软件或上传等。这可能是一个浩大的工程,但是却非常有意义。参考上面的方法,大家可以尝试jail出完美的服务器来。 2008年4月 文章选取的例子非常简单,上手容易,只是为了讲述静态与动态链接库的生成和链接过 程,还有他们之间的区别。以下例子在 gcc 4.1.1 下顺利通过。 文件预览文件目录树如下,如你所见,非常简单。 - libtest/
- |-- lt.c
- |-- lt.h
- `-- test.c
代码#lt.c - /* lt.c
- *
- */
-
- #include <stdio.h>
-
- void myprint(void)
- {
- printf("Linux library test!\n");
- }
# lt.h - /* lt.h
- *
- */
-
- void myprint(void);
#test.c - /* test.c
- *
- */
-
- #include "lt.h"
-
- int main(void)
- {
- myprint();
- return 0;
- }
先看静态库首先做成静态库 liblt.a 。 - $ gcc -c lt.c -o lt.o
- $ ar cqs liblt.a lt.o
再者,链接,这里指定了静态库的位置,注意文件顺序不可乱序。 - $ gcc test.o liblt.a -o test
这个时候再来看他的引用库情况。 - $ ldd test
- linux-gate.so.1 => (0xffffe000)
- libc.so.6 => /lib/libc.so.6 (0xb7e29000)
- /lib/ld-linux.so.2 (0xb7f6e000)
动态库做成动态库 liblt.so 。 - $ gcc -c lt.c -o lt.o
- gcc -c test.c
- $ gcc -shared -Wall -fPIC lt.o -o liblt.so
链接方法I,拷贝到系统库里再链接,让gcc自己查找 - $ sudo cp liblt.so /usr/lib
- $ gcc -o test test.o -llt
这里我们可以看到了 -llt 选项,-l[lib_name] 指定库名,他会主动搜索 lib[lib_name].so 。这个搜索的路径可以通过 gcc --print-search-dirs来查找。 链接方法II,手动指定库路径 - $ cc -o test test.o -llt -B /path/to/lib
这里的-B 选项就添加 /path/to/lib 到gcc搜索的路径之中。这样链接没有问题但是方法II 中手动链接好的程序在执行时候仍旧需要指定库路径(链接和执行是分开的)。需要添加系 统变量 LD_LIBRARY_PATH : - $ export LD_LIBRARY_PATH=/path/to/lib
这个时候再来检测一下test程序的库链接状况(方法I情况) - $ ldd test
- linux-gate.so.1 => (0xffffe000)
- liblt.so => /usr/lib/liblt.so (0xb7f58000)
- libc.so.6 => /lib/libc.so.6 (0xb7e28000)
- /lib/ld-linux.so.2 (0xb7f6f000)
恩,是不是比静态链接的程序多了一个 liblt.so ?恩,这就是静态与动态的最大区别,静 态情况下,他把库直接加载到程序里,而在动态链接的时候,他只是保留接口,将动态库与 程序代码独立。这样就可以提高代码的可复用度,和降低程序的耦合度。 2008年3月 Chord协议 Chord在2001年由麻省理工学院提出(参见0),其核心思想就是要解决在P2P应用中遇到的基本问题:如何在P2P网络中找到存有特定数据的节点。与前两种协议不同,Chord专门为P2P应用设计,因此考虑了在P2P应用中可能遇到的特殊问题,这些内容将在路由的部分进行讨论。 哈希算法 Chord使用一致性哈希作为哈希算法。在一致性哈希协议中并没有定义具体的算法,在Chord协议中将其规定为SHA-1。 路由算法 Chord在一致性哈希的基础上提供了优化的路由算法: 在Chord中,每个节点同样需要存储m个其他节点的信息,这些信息的集合被称为查询表(Finger Table)。一致性哈希中的节点同样具有这样的表格,但在Chord中,表格中的节点不再是直接相邻的节点,它们的间距(ID间隔)将成2i 的关系排列(i 表示表中的数组下标)。这样形成的节点之间路由关系实际上就是折半查找算法需要的排列关系。 在查询的过程中,查询节点将请求发送到与键值最接近的节点上。收到查询请求的节点如果发现自身存储了被查询的信息,可以直接回应查询节点(这与一致性哈希完全相同);如果被查询的信息不在本地,就根据查询表将请求转发到与键值最接近的节点上。这样的过程一直持续到找到相应的节点为止。不难看出,查询过程实际上就是折半查找的过程。 经过Chord的优化后,查询需要的跳数由O ( N)减少到O(log(N))。这样即使在大规模的P2P网络中(例如N=100,000,000),查询的跳数也仅为O(8),每个节点仅需存储27个(log2100000000)其他节点的信息。 Chord还考虑到多个节点同时加入系统的情况并对节点加入/退出算法作了优化。 讨论 Chord算法本身具有如下优点: 负载平衡 这一优点来自于一致性哈希,也就是一致性哈希中提到的平衡性。所有的节点以同等的概率分担系统负荷,从而可以避免某些节点负载过大的情况。 分布性 Chord是纯分布式系统,节点之间完全平等并完成同样的工作。这使得Chord具有很高的鲁棒性,可以抵御DoS攻击。 可扩展性 Chord协议的开销随着系统规模(结点总数N)的增加而按照O(logN)的比例增加。因此Chord可以用于大规模的系统。 可用性 Chord协议要求节点根据网络的变化动态的更新查询表,因此能够及时恢复路由关系,使得查询可以可靠地进行。 命名的灵活性 Chord并未限制查询内容的结构,因此应用层可以灵活的将内容映射到键值空间而不受协议的限制。 Chord在CFS系统中得到了应用,具体的介绍可参见[8] 内容寻址网络(Content-Addressable Network,CAN) CAN在2001年由加州大学伯克利分校提出(参见[3])。与Chord一样,CAN也是DHT的一个变种。 哈希算法 CAN的哈希算法与一致性哈希有所不同。Chord中,哈希得到的键值总是一维的,而在CAN中,哈希的结果由d维的笛卡尔空间来表示。d是一个由系统规模决定的常量。 路由算法 CAN的路由查询将在d维笛卡尔空间中进行。 在CAN中,每个节点自身的ID经由哈希后得到的d维向量。经过这样的映射后,整个P2P系统将被映射到一个d维笛卡尔空间中,每个节点的位置由其自身ID决定。CAN对邻居节点的定义并不要求成2i的关系排列,而是改为用在笛卡尔空间上相邻来表示:在d维笛卡尔空间中,2个节点的d维坐标中有d-1维是相等的,剩余的一维是相邻的节点称之为相邻节点。 CAN中的节点仅存储相邻节点表。由于在d维的空间中最多有2d个相邻的节点,因此节点的相邻节点表最多有2d个表项。 在查询的过程中,查询节点首先计算被查询内容的键值(d维向量),然后在节点列表中查找在笛卡尔空间中与该键值最为接近的相邻节点,找到后向该节点发送查询请求(这一策略被称为贪婪策略)。查询请求中将携带被查询内容的键值。收到查询请求的节点如果发现自身存储了被查询的信息,可以直接回应查询节点(这与一致性哈希完全相同);如果被查询的信息不在本地,就根据相邻节点表将请求转发到与键值最接近的节点上。这样的过程一直持续到找到相应的节点为止。在查询过程中,被查询节点到目标节点的笛卡尔空间距离单调地减少。 如果查询节点或转发节点发现邻居节点表中无法找到可用的下一跳节点,则采用非结构化P2P常用的扩展环搜索(Expanding Ring Search,使用无状态,受控的泛洪算法在重叠网中搜索)以找到合适的(符合贪婪策略)下一节点。 经过CAN的优化后,查询需要的跳数由O ( N)减少到均值为(d/4)(n1/d)的随机制,考虑到d为常数,这一值可以表示为O(n1/d)或O(dn1/d)。 讨论 CAN和Chord的主要区别在于路由算法不同。相比之下,在节点数量非常大时,CAN的平均查询跳数要比Chord增加得更快。而且 CAN查询过程中需要的运算量也要高于Chord。但CAN使用的d为预先设置的常量,因此并不假设系统节点数量。在节点总数动态变化范围很大的系统中, CAN的相邻节点表结构保持稳定,这在P2P的应用中也是很重要的优点。 2008年1月 在iPhone最新的1.1.2版本固件和iTunes 7.5下,已经再次可以简单的用修改文件名的方法加入自定义铃声。具体方法如下: 1. 使用其它软件(Audition等)截取出长度小于40秒的MP3文件 2. 将该MP3文件加入iTunes曲库 3. iTunes中右键点击该曲目,转换为AAC格式 4. 进入My Documents\My Music\iTunes\iTunes Music\目录,将转换后的.m4a文件重命名为.m4r 5. 如果该文件夹下不存在Ringtones文件夹,创建一个 6. 双击更名后的.mpr文件打开,它会自动加入iTunes的铃声列表,同步iPhone 2008年1月 --------------上下级之间保证执行的要点 ★ 每个人的本性中始终在重复着一个永恒的主题:回避风险。 ★ 所有的执行在管理层面上都可以总结为一点:责权利的对等。 ★ 为什么当领导的总是没时间,而下属总是没工作?原因就在于猴子都从下属身上,跳到了经理的肩上。 【责任是一只猴子】 我们每一个人的本性里面始终都在重复一个主题,这个主题就是“回避风险”。所有的执行在管理层面都可以归结成为一句话,叫做“责权利的对等”。我认为,这个原则是管理人员或者中层管理人员做管理的最基本要领。 之所以是最基本的要领,是因为我们每一天其实都处于“回避风险,逃避责任”的阴谋之中。为什么用“阴谋”这个词,是因为我们常常在不知不觉中,中了招,累得要死还自鸣得意。 闭上眼想想吧,我们是不是常常会碰到这样的情况。每天,当你推开公司大门时,经常会有员工迎面上来问你:经理,您昨天布置我们做的事,我们现在出了点问题,你看这个事情怎么解决? 在通常的情况下,你会如何回答?比较常用的回答方式是两种:一种是:“我想一想,一会儿我再告诉你”。另外一种是,你就直接跟他讲,这件事应当如何如何处理。 我们每天都会碰到大量这类事,但你想过没有,这是一场不知不觉的“阴谋”,这场阴谋的名字就叫“逃避责任”或“回避风险”。 在你进公司大门之前,你一定计划好了今天要做这样,或者做那样的事,你一天八小时应当足以完成这些事,但事实上你会发现,只要我们一当上经理或者管理人员,你就发现你的时间完全不够用。 问题是,为什么我们做经理的总是累得要死,总是发现没时间,可做下属的却总是没事干呢? 原因是我们常常陷入下属“逃避责任”或“回避风险”阴谋当中。我们可以分析一下,我们把责任比喻成一只猴子,在你进办公室之前,所有应当完成的工作都应当布置好的,各有各的工作,各有各的责任,但是在下属请示你之后,情况发生了什么变化呢? 你会发现在大多数情况下,猴子跳到了你的身上。比如下属请示了你的时候,如果你的回答是第二种:我想一想一会再告诉你。那么半小时后,你会发现这个下属就站在你的门口,敲你的门:“领导,这个问题您考虑的怎么样了?” 注意,本来这个问题是需要他解决的,是应当你去检查他完成得怎么样了,可现在是什么情况呢?现在是他来问你,你考虑得怎么样了?! 第二种情况下又如何呢?你当时的回答是这事应当如何如何做,几天之后你过去检查结果,发现出了问题,于是你很不高兴地问:“这个事情你怎么做成这个样子了?”员工也会很快回答你:“那不是你告诉我这样去做的吗?” 在这种情况下,是你领导他,还是他领导你呢?或者说是你管理他,还是他管理你呢? 所有这些,只是发生在下属请示你之后,一切就都改变了。经过分析现在你明白了:责任是一只猴子,如果你不懂如何回答下属的请示,一转眼之间,就会跳到你的身上。然后,你就不再有上下班时间,不再有周末,不再有假日。 一旦所有下属的猴子跳到了你的身上,你就要加班加点工作,累得要死。可悲的是,你还沉浸在一种虚荣之中:下属凡事请示我,说明我在他们心中重要,说明他们尊敬我呀! 现在你明白了吧,这是一场阴谋!为什么那么多下属喜欢凡事都请示你?因为那是一种“回避风险,逃避责任”最好办法。 可问题是,我们不可能在下属请示我们问题的时候不回答呀?怎么办? 猴子管理法,就是解决这类矛盾的最优方案。 【猴子管理第一法则:锁定责任】 经典执行借口:销售不好是因为俄罗斯的矿山爆炸 ² 始终让猴子在下属的肩上; ² 凡是计划的就一定要有结果,那怕是阶段性的结果; ² 责任变动时要让放猴子的人清楚:猴子已经跳到另一个人身上了。 所谓责权利对等,无非是讲猴子原来在谁的身上,它就应该在谁的身上,不允许它在组织内跳来跳去。如果领导者不懂得如何锁定责任,责任这只猴子就会在公司中跳来跳去,最后导致责、权、利的不对等。 为什么当领导的总是没时间,而下属总是没工作?原因就在于猴子都从下属身上,跳到了经理的肩上。谁应当对此负责?下属还是经理?我觉得主要的责任应当由经理承担。下属提问是正常的,猴子跳到了上司的肩上,是因为上司不懂责任的归宿。 如果我们说有一家公司的销售会议,得出一个结论,销售不好是因为俄罗斯的矿山爆炸了。你一定会觉得很荒唐,但下面这些对话我相信你一定不会陌生: 营销部门经理说:“最近销售做的不好,我们有一定责任,但是最主要的责任不在我们,竞争对手纷纷推出新产品,比我们的产品好,所以我们很不好做。” 研发部门经理说:“我们最近推出的新产品是少,但是我们也有困难呀,预算太少!” 财务部门经理:“是,我是削减了你的预算,但是你要知道,公司的成本在上升,我们当然没有多少钱。”这时,采购部门经理D说:“我们的采购成本是上升了10%,为什么,你们知道吗?俄罗斯的一个生产铬的矿山爆炸了,导致不锈钢价格上升。” “哦,原来如此呀,这样说,我们大家都没有多少责任了,哈哈哈哈!” 总经理:“这样说来,我只好去考核俄罗斯的矿山了!!” 结果是:销售不好是因为俄罗斯的矿山爆炸了。 这样一些荒唐的故事,之所以会发生在我们身边,原因就在于每个人的本性中,始终在重复着一个永恒的主题:回避风险。因此,管理责任这只猴子的第一法则,就是锁定责任:猴子原来在谁的身上,无论有什么变化,它都应该被锁定在原来责任人的身上。 就象这个故事中的情景一样,销售部销售不好,责任在产品;产品不好,责任在预算;预算之所以少,是因为成本高,赢利少,而成本之所以高,是因为采购,采购成本则是因为为俄罗斯矿山爆炸。 每个人都可以把理由往别人身上推,每个人都能够在别人那儿找到借口,可我们有没有想过一个基本的问题:如果我们的产品比任何竞争对手都强大,如果我们的产品没有任何问题,那么,这个世界还存在营销这个词吗? 为什么同样的产品,在不同人的手里会有不同的结果?比如我们能够楚地说清宝洁的洗发水,比索肤特的洗发水之间有什么明确的区别吗?为什么几乎差不多的产品在不同的公司,在不同的人手中会有不同的结果? 结论只有一句话:做得好的公司,猴子各就各位--员工勇于承担责任,。做得不好的公司,猴子上窜下跳――员工相互推卸责任。 【关键执行要点:始终让猴子在下属的肩上】 l 绝不允许等着做和问着做, l 海尔是如何在执行中锁定猴子的? 那么如何锁定猴子呢?入口在那里? 解铃还要系铃人。我们首先要懂得下属做事的方式,我们才能够防止他们的猴子跳到我们身上。一般说来,员工有五种工作方式: 1. 等着做 2. 问着做 3. 提出建议,等着结果再做 4. 主动做,边做边汇报 5. 主动做,然后按程序汇报 如果你想尽量地让猴子呆在下属身上,那么,你就要尽力消除第一种和第二种情况。在“等着做”或“问着做”的情况下,猴子最有可能跳回自己的肩上。 在这一点上,海尔是个榜样。 海尔电冰箱厂有一个五层楼的材料库,这个五层楼一共有2945块玻璃,如果你走到玻璃跟前仔细看,你一定会惊讶的发现这2945块玻璃每一块上都贴着一张小条! 小条上是什么?原来每个小条上印着两个编码,第一个编码上写着负责擦这个窗户的责任人,第二个编码上是谁负责检查这个窗户。 猴子在谁的身上?海尔在考核准则上规定:如果玻璃脏了,责任不是负责擦的人,而是负责检查的人! 如果玻璃脏了,责任这只猴子锁定于检查的人身上,那么,擦玻璃的行动责任,这保猴子就会被锁定在擦窗户这个员工身上,绝对不会发生猴子上窜下跳。 海尔OEC管理法的核心是,对工作的分解强调“三个一”,即分解量化到每一个人、每一天、每一项工作。在海尔大到机器设备,小到一块玻璃,都清楚标明事件的责任人与事件检查的监督人,有详细的工作内容及考核标准,如此形成环环相扣的责任链,做到了“奖有理、罚有据”。 这种管理的核心是,我们不再去想个人工作态度如何,我们要把责任锁定,即使是一个简单的擦玻璃的工作,也要明确制定两个责任人,各有各自的明确责任。 海尔冰箱总共有156道工序,海尔精细到把156道工序,分为545项责任,然后把这545项责任落实到每个人的身上。 凡事都要做到“责任到人”。“人人都管事,事事有人管”,这就是海尔能够成为中国企业榜样的重要原因。 哪怕是车间里一扇窗户的玻璃,其卫生清洁也有指定员工负责擦,也有指定的员工负责检查,更何况海尔的生产,销售? 责任锁定,首要的是锁定猴子的归宿-----这是上下级之间保证执行的要点。 比尔翁肯 (Bill Oncken)曾发明一个有趣的理论—「背上的猴子」。他所谓的「猴子」,是指「下一个动作」。回想一下,你是否有过这样的情形:在走道上碰到一位部属,他说:「我能不能和您谈一谈?我碰到了一个问题。」于是你便站在走道上专心听他细述问题的来龙去脉,一站便是半个小时,既耽搁了原先你要做的事,也发现所获得的信息只够让你决定要介入此事,但并不足以做出任何决策。于是你说:「我现在没时间和你讨论,让我考虑一下,回头再找你谈。」 在这样的案例中,猴子原本在部属的背上,谈话时彼此考虑,猴子的两脚就分别搭在两人背上,当你表示要考虑一下再谈时,猴子便移转到你背上。你接下了部属的角色,而部属则变成了监督者,他会三不五时跑来问你:「那件事办得怎样了?」如果你的解决方式他不满意,他会强迫你去做这件原本他该做的事。 当你一旦接收部属所该看养的猴子,他们就会以为是你自己要这些猴子的,因此,你收的愈多,他们给的就愈多。于是你饱受堆积如山、永远处理不完的问题所困扰,甚至没有时间照顾自己的猴子,努力将一些不该摆在第一位的事情做得更有效率,平白让自己的成效打了折扣。 经理人应该将时间投资在最重要的管理层面上,而不是养一大堆别人的猴子。身为经理人,如果你能让员工去抚养他们自己的猴子,他们就能真正地管理自己的工作,你也有足够的时间去做规划、协调、创新等重要工作,让整个单位持续良好的运作。 翁肯提出的猴子管理法则,目的在于帮助经理人确定已由适当人选在适当的时间,用正确的方法做正确的事。当然,这个法则只能运用在有生存价值的猴子身上,不该存活的猴子,就狠心把他杀了吧! 法则一:说明 除非下一个步骤已经明确界定,否则经理人和员工都不能离开。严守这项法则可以获得三种好处: 第一,如果员工知道要提出适当的下一步骤才能结束谈话,他就会在事前做更缜密的规划。 第二,它可以促使员工采取行动。 第三,对猴子做描述、把下一步骤说清楚,能提高员工的工作意愿,让他们跨出最具关键性的一步。 法则二:所有人 经理人和员工的对谈,要到每一只猴子都分配给一个人拥有之后才能结束。至于哪一只猴子该归谁管,原则上尽可能把猴子交给能照顾到他们福利的最低阶层人员去照料,因为部属总共可以投入的时间精力比高阶经理人来得多,而且经验也告诉我们,部属往往比我们所想的还要能干。 把猴子送错主人,有时候是经理人自己内心的需求,想要避开管理所带来的挑战,或是以为唯有自己才做得来,有时候却是组织政策使然。这时候要替猴子找到适当的主人,就必须技巧与自制力兼施,尤其是自制力最为重要。 要求部属尽力把工作做到最好,可能会遭遇部属反抗,这与为猴子找到适合主人的实务,变得有些相抵触,因为经理人会发现,有时候把猴子接过来,要比让猴子靠在适当主人背上要来得容易。但是请记住:要培养一个人的责任感,唯一的方法是给予他们责任。 法则三:保险单 在把每一只猴子放出去面对组织丛林之前,先为他们保个险。此一法则提供一系统性的方法,用以平衡员工所需的处理猴子的自由空间,与经理人对结果所负的责任。 赋予员工权限和自由,经理人和员工可以互蒙其利。经理人能自由裁量时间,花在监督员工的时间及精力减少;员工也可以享受到自我管理的好处,有更高的满足感、更高昂的士气。 但员工拥有自由空间时,就不免会犯错,这时候猴子的保险就要派上用场,保证他们犯的错是组织所能承担得起的。 猴子保单有两个层次。第一是给予建议,然后行动。当员工犯下承担不起的错误的可能性相当高时,希望有机会否决他们的行动计划,这是一种保护措施,但却得牺牲经理人的时间及员工的自由空间。第二是行动,然后给予建议。确信员工可以自行完善料理猴子,先行动后再来报告,这让员工有很大的运作空间,也省下经理人的监督时间,但风险也较大。至于该选择哪一份保单,则视情况而定,而双方都可以行使选择权。 法则四:照料与检查 企业的成功取决于猴子的健康,因此必须定期为 ?们做检查,维持?们的身体健康。检查猴子的目的有二:一是发现员工正在做正确的事,进而称赞他们;二是发掘问题,并在问题形成危机之前采取校正行动。 猴子生病,不是由于缺乏照料、营养不良,就是因为照料不当。有时候员工不想让经理人知道猴子生病了,因为他们大都想自行解决问题,经理人应该与员工建立默契,要他们尽力照顾病猴子,如果情况没有改善,就必须把猴子交给经理人检查。相对的,如果经理人发现猴子生病,就应该把下一次检查的时间提早到猴子身体状况所能允许的时间。 这里的检查着重在猴子的情况,而非员工本身,因此检查让经理人有机会发现员工正确地在进行某事、查明及纠正问题、指导员工、降低自己的焦虑,之后,就由员工自己去求表现了。 翁肯的理论让我们知道,如果我们真的想帮助他人,就必须教他们如何补鱼,而不是送他一条鱼。剥夺他人的主控权,去喂养他人的猴子,并无法帮他们解决问题,这只是为别人做他自己可以做的事。
2007年12月 ATM概述 ATM是一种大小固定的信元交换和多路复用技术,它是面向连接的,任何用户数据在两个更多ATM连接设备之间传输之前,都必须建立虚电路(SVC/PVC) ATM的数据链路层 l ATM适配层(AAL,ATM Adaptation Layer) l ATM层 上层将用户信息以比特的形式传下传给AAL层,用户信息被封装为AAL帧,然后ATM层将这些信息分段为ATM信元,接收端会执行反向操作,这种过程叫做分段和重组(SAR Segment and Reassembly) The IP to ATM CoS feature is supported on Cisco 2600, Cisco 3600, Cisco 7200, and Cisco 7500 series routers equipped with the following hardware: • Cisco 2600 and Cisco 3600 series: ATM OC-3, T1 IMA, or E1 IMA port adapter • Cisco 7200 series: – NPE-200 or higher (NPE-300 recommended for per-virtual circuit (VC) class-based weighted fair queueing (CBWFQ) – One of the following Enhanced ATM port adapters (PA-A3): T3, E3, DS3, or OC-3 • Cisco 7500 series: – VIP2-50 – One of the following Enhanced ATM port adapters (PA-A3): T3, E3, DS3, or OC-3 IP to ATM CoS supports configuration of the following features: • Single ATM VCs • VC bundles • Per-VC Low Latency Queueing (LLQ), WFQ, and CBWFQ ATM的应用方式 l RFC2684,针对多个上层协议的手工方式。 l RFC2225(经典的ip协议),针对IP应用的动态方式 l 采用局域网仿真(LANE)用于第2层的动态方式 l 采用ATM上的多协议(MPOA)针对所有 L2 L3的动态方式 ATM信元类型
ATM UNI 信元 ATM NNI信元 GFC:通用流量控制(只有在ATM UNI 信元包含)控制用户本地(端节点到网络)流量, 1. 未控制模式(未使用GFC字段)一般是这样使用 2. 控制模式(使用GFC字段) VPI:虚路径标识符(VP的标识),ATM具有本地标识的地址部分,UNI为8bit,NNI为12bit VCI:虚信道标识符(VC的标识),ATM具有本地标识的地址部分,ATM交换机通过PVC转换表来转发ATM信元(手动配置的) PT:有效负载类型(操作/管理/维护) CLP:信元丢弃优先级,在网络拥塞时,优先级低的信元被丢弃的可能性更大。(1/0) HEC:报头差错控制 l UNI(user to network interface) l NNI(network to node interface) ATM的服务类型 l AAL1:固定比特率(CBR constant bit rate)\ l AAL2:可变比特率(VBR variable bit rate) l AAL3:面向连接的数据通信 l AAL4:面向非连接的数据通信 l AAL5:未指定比特率(UBR unspecified bit rate) 信元丢弃策略 当信元超出VC的通信合同规定时,ATM的流量控制机制把信元的CLP位设置为1,当ATM网络的部分发生拥塞的时,CLP位为0和1的信元都有可能被丢弃,虽然CLP位被标记的信元首先被丢弃。 策略: l PPD(partial packet discard)部分分组丢弃 ATM交换机在缓冲区溢出的时候开始丢弃信元,当大型分组的一个信元被丢弃时,PPD便开始丢弃该分组的其他信元(直到交换机发现ATM信元头中设置的参数表明该信元时分组的最后一个信元,分组的最后一个信元本身不会被丢弃,由于AAL5不支持同一个VC上分组同时多路复用,所以可以用这个来划分分组边界),不再继续传送该组其他部分的信元了,因为这会在拥塞的链路上增加不必要的通信,在这种情况下,目的地无法将信元重组为原来的分组,因此必须通过高层的协议重传整个分组。 l EPD(early packet discard) 早期分组丢弃 分组在被允许进入输入队列的之前将执行EPD,当新的分组到达的时候,EPD检查输出缓冲区的使用情况,如果使用的缓冲区小于配置的门限值,ATM交换机就知道缓冲区空间不会被耗尽,分组的所有信元都将进入队列。否则,ATM交换机就会认为缓冲区空间要被耗尽,无法将整个分组加入队列中,这样,交换机就会丢弃整个分组。因此,EPD要么将分组的所有信元加入队列中,要么丢弃整个分组。 VP整形 l 与ATM VC的ATM的服务类似,ATM的整形也可以应用于VP,以限制整个VP的流量,在被整形的VP中,所有的VC仍然可以时UBR,她传输尽力而为通信,对流量没有任何限制。 案例研究:提供abr服务的pvc In atm0/0/0 Ip add 192.168.12..1 255.255.255.0 Pvc 0/34 Abr 10000 1000 (abr服务的pcr和mcr是10m/s 和1m/s,pcr和mcr的默认值是线路速率和0) Atm abr tate-factor 8 8(将接口配置为收到网络的rm控制信息时,提高或者降低信元传输速率,当rm信元从网络收到反馈后,abr速率的增加或者降低因子为8,默认的因子是16) 在ATM上维持IP QOS的两种方案 (1)单个PVC将所有的ip通信传送到目的地,在atm的网络入口处,超过atm pvc参数和服务的ip通信被加入队列中,当由于网络拥塞而建立队列的时候,诸如wred和wfq等ip qos技术将被应用于队列。 wred确保高优先级通信比低优先级通信的损失低,wfq确保高优先级通信获得更高的带宽,因为它更频繁的调度高优先级通信 PS:在pvc上运行CBWFQ时,可以根据通信类来分配带宽 committed access rate (CAR) or policy-based routing (PBR), to classify and mark different IP traffic by modifying the IP Precedence field in the IP version 4 (IPv4) packet header. Subsequently, Weighted Random Early Detection (WRED) or distributed WRED (DWRED) can be configured on a per-VC basis so that the IP traffic is subject to different drop probabilities (and therefore priorities) as IP traffic coming into a router competes for bandwidth on a particular VC。 Enhanced ATM port adapters provide the ability to shape traffic on each VC according to the ATM service category and traffic parameters employed. When you use the IP to ATM CoS feature, congestion is managed entirely at the IP layer by WRED running on the routers at the edge of the ATM network. (2)一个VC束(多个PVC组成)将ip通信传输到目的地,将OQS(实时,非实时,尽力而为)不同的通信传输到向他的目的地,使用多个PVC可以让每个IP通信类都用单独的PVC传输,每个VC都提供一种ATM服务,为关键通信类提供了通信隔离,使用PVC束前要先在网络中实施流量工程,才能使束中所有PVC的使用情况最佳,否则会出现在高优先级通信的PVC出现拥塞,而低优先级通信的PVC则相对空闲。当传输高优先级通信的PVC出现拥塞时,不能将高优先级的通信转到其他PVC中,使用单个PVC时,可以启用CBWFQ。 --------VC束中每个VC都用来传输具有特定IP优先级值的通信,一个VC可以对应一个或者多个IP优先级,但需要注意的是,每个PVC束只能有一个路由对等体(邻居) -------可以使用ATM OAM或 过渡本地管理接口(ILMI,Interim local management interface)来监视VC的完整性,如果一个束的高优先级VC出现故障,可以将其通信转发给束中的低优先级VC或宣布整个束出现故障。 -------束中每个VC都有一个单独的队列,可以在每个VC队列上应用IP QOS技术WRED和WFQ。 1. 根据分组的IP优先级值将他们映射到VC。 2. 每个VC都用于所有通信类的单ABR PVC,单个PVC的WRED和WFQ。 Config Task List • Single ATM VC with WRED Group and IP Precedence Example interface ATM1/1/0.46 multipoint ip address 200.126.186.2 255.255.255.0 no ip mroute-cache shutdown pvc cisco 46 encapsulation aal5nlpid random-detect attach sanjose ! random-detect-group sanjose precedence 0 200 1000 10 precedence 1 300 1000 10 precedence 2 400 1000 10 precedence 3 500 1000 10 precedence 4 600 1000 10 precedence 5 700 1000 10 precedence 6 800 1000 10 precedence 7 900 1000 10 • VC Bundle Configuration Using a VC Class Example router isis net 49.0000.0000.0000.1111.00 vc-class atm bundle-class encapsulation aal5snap broadcast protocol ip inarp oam-bundle manage 3 oam 4 3 10 vc-class atm control-class precedence 7 protect vc vbr-nrt 1000 5000 32 vc-class atm premium-class precedence 6-5 no bump traffic protect group bump explicitly 7 vbr-nrt 20000 10000 32 vc-class atm priority-class precedence 4-2 protect group ubr+ 10000 3000 vc-class atm basic-class precedence other protect group ubr 10000 interface a1/0.1 multipoint ip address 10.0.0.1 255.255.255.0 ip router isis bundle new-york protocol ip 1.1.1.2 broadcast protocol clns 49.0000.0000.2222.00 broadcast class-bundle bundle-class bundle san-francisco protocol clns 49.0000.0000.0000.333.00 broadcast inarp 1 class-bundle bundle-class pvc-bundle sf-control 307 class-vc control-class pvc-bundle sf-premium 306 class-vc premium-class pvc-bundle sf-priority 304 class-vc priority-class pvc-bundle sf-basic 301 class-vc basic-class bundle los-angeles protocol ip 1.1.1.4 broadcast protocol clns 49.0000.0000.4444.00 broadcast inarp 1 class-bundle bundle-class pvc-bundle la-high 407 precedence 7-5 protect vc class-vc premium-class pvc-bundle la-mid 404 precedence 4-2 protect group class-vc priority-class pvc-bundle la-low 401 precedence other protect group class-vc basic-class • Per-VC WFQ and CBWFQ on a Standalone VC Example class-map class1 match access-group 101 class-map class2 match access-group 102 policy-map policy1 class class1 bandwidth 500 queue-limit 30 class class2 bandwidth 1000 interface ATM1/1/0.46 multipoint ip address 200.126.186.2 255.255.255.0 pvc cisco 46 vbr-nrt 2000 2000 encap aal5snap service policy output policy1 • Per-VC WFQ and CBWFQ on Bundle-Member VCs Example bundle san-francisco protocol ip 1.0.2.20 broadcast encapsulation aal5snap pvc-bundle 0/35 service policy output policy1 vbr-nrt 5000 3000 500 precedence 4-7 pvc-bundle 0/34 service policy output policy2 vbr-nrt 5000 3000 500 precedence 2-3 pvc-bundle 0/33 vbr-nrt 4000 3000 500 precedence 2-3 service policy output policy4 pvc-bundle 0/31 service policy output policy2 2007年7月 这次回国时间很久,有3个月,不过没有在家里呆上几天就去单位里面学习了。
说说学习,其实和我专业没有关系。老爸看我在家里烦了,就要我去工作了。
到了单位在学习车床,一个月下来,感处很多。 2007年6月 时间过的真快,一转眼又过了一年在法国.
想当年还是一个什么都不懂的小毛孩,高中毕业就来了一个陌生的世界.
现在过来这么久了,变化还是很大的.不管是学业还是生活,以及人生观也有变化.
今年是来巴黎的第一年,6大的学习算是有惊无险的过来,明年就是我学习的最后一年了,
到时候就真的要进入社会了.
和以前一样,暑假就回国.去国内好好休息休息把,在法国的生活没有国内丰富.不多说,累了. 2007年2月 今天把上次有坏道的硬盘拿来修了修,还有FBDISK这个软件还不错,大概后来修了以后浪费了5G的空间,不过总算没有浪费一块硬盘.
加上有这块硬盘,我就装了Linux了,不过说真的,很好用.就是很多软件都要下载,
而且还有一个大问题,每次用LINUX都要换硬盘,下次在想个办法.
|