Saturday, September 4, 2010

prefork服务器方式

WEB服务器一般都有两种模式处理动态服务:
1. prefork
2. thread

一种是使用多进程, 另一种是使用多线程.  它们的具体的实现方式自己其实并不清楚.
最近公司的python WEB服务使用较多的内存. 为了清楚其中的问题. 需要学习flup(django需要此库)代码. (  人家写的代码实在好看 )

很多软件都有prefork方式, 下面是prefork模型:

每个nginx子进程都分别调用accept得到用户发来的80端口请求.
之所以每个子进程都可以调用accept得到同一个socket的请求, 是因为fork出的子进程的file descriptor是指向同一个实体.

这样实现了多进程竟争得到socket请求.

每个子进程又可以使用epoll, 线程等方式并发处理众多来自80端口的请求. fast cgi server也是类似的方法.
nginx与fast cgi server之间使用socket通信, 使用fast cgi协议.


每个flup work某一时刻只服务一个页面请求. 完成请求后可以重新接受请求.

使用prefork模型的方式。 所以在PreforkServer类中。

父进程的主体是一个loop:

119         # Main loop.
120         while self._keepGoing:
121             # Maintain minimum number of children.
122             while len(self._children) < self._maxSpare:
123                 if not self._spawnChild(sock): break

可以看出, 父进程是永远期待子进程的数目为一个数值的。这个数值为maxSpare
粗粗看上去, 有些野。但是父进程是有回收子进程的策略的, 如下:

169             # See who and how many children are available.
170             availList = filter(lambda x: x[1]['avail'],
self._children.items())
171             avail = len(availList)

上面的代码可以知道, 父进程会一直收集可用的子进程。 所谓可用是没有在工作
的子进程, 可以从子进程的代码中看出, 如下:
370             # Notify parent we're no longer available.
371             self._notifyParent(parent, '\x00')
372
373             # Do the job.
374             self._jobClass(clientSock, addr, *self._jobArgs).run()

上面的代码是子进程在调用jobClass.run之前, 通知父进程自己是 no longer
available的。


父进程维护着“可用的子进程“数目是为了可以了解负载情况, 判断是否需要产生
更多的子进程, 如下:
172
173             if avail < self._minSpare:
174                 # Need to spawn more children.
175                 while avail < self._minSpare and \
176                       len(self._children) < self._maxChildren:
177                     if not self._spawnChild(sock): break
178                     avail += 1

上面的代码使用了两个数据: minSpare和maxChildren。

minSpare是表示最少“可用子进程“数
maxChildren是表示最大“子进程”数, 是空闲与工作的子进程总数

如果“可用子进程”数还剩一定数据,会被回收, 如下:

179             elif avail > self._maxSpare:
180                 # Too many spares, kill off the extras.
181                 pids = [x[0] for x in availList]
182                 pids.sort()
183                 pids = pids[self._maxSpare:]
184                 for pid in pids:
185                     d = self._children[pid]
186                     d['file'].close()
187                     d['file'] = None
188                     d['avail'] = False

从上面的代码可以看到, fast cgi在启动时的使用到的三个参数的作用应该是:
minspare: 最小“空闲进程数“
maxspare: 常规进程数,即fast cgi启动后, fastcgi进程数是大于等于此值
maxChildren: 最大进程数, 此值主要是用于防止内存被使用完的。

Friday, September 3, 2010

父类方法使用子类的方法与数据

对OO其实是不懂, 只是会一个class( 模式说白了不会. 接口是学了go语言才知道是什么. (-_-)! )

今天在看flup项目的代码时, 有一段发码看不懂. 其实是自己对OO的不懂:
 
  1 #coding:utf-8
  2
  3 class Parent(object):
  4     def __init__(self, name):
  5         self._name = "\t父类: %s" % name
  6     def whoami(self):
  7         print self._name
  8         print "在父类方面中调用子类的方法:"
  9         self._print()
 10         
 11         
 12 class Child(Parent):
 13     def __init__(self, name):
 14         self._nickname = "\t子类:%s" % name
 15         Parent.__init__(self, "大明")
 16         
 17     def _print(self):
 18         print self._nickname
 19         
 20     def run(self):
 21         # 下面的两种调用方法是一样的
 22         print "使用Parent.method(self)的方法调用父类方法"
 23         Parent.whoami(self)
 24         print "使用self.method()的方法调用父类方法"
 25         self.whoami()
 26         
 27         
 28         
 29 if __name__ == "__main__":
 30     c = Child("小明")
 31     c.run()

平时一般都是使用self.whoami(), 自为发父方法无法使用子类的东西. 但是如果使用Parent.whoami(self) 的方式, 就会明白自己的理解其实是错的.

基本问题是自己没有对书中的Parent.__init__(self, "大明") 一句理解到位

嗯.... 自己的水平太差了.

Sunday, August 15, 2010

python2.4与2.5的异常基类变化

工作中, 有一服务需要从freeBSD迁移到centOS上。其中有一python脚本有些鬼异。
在freeBSD上可以正常, 在centOS上不能正常。最后目标定位在脚本最后的代码上, 大概如下:

try:                           
    if 测试条件:           
        do_someting()
        sys.exit(0)       
    else:                     
        sys.exit(1)       
except Exception:  
        sys.exit(1)       

centOS上, python的版本是2.4的。 freeBSD下安装的是2.5.
旧版本的Exception是基类: 因此在2.4上脚本永远是返回1的。
到了新的版本BaseException才是基类: 因此SystemExit和Exception是同一级。脚本可以返回0

这是迁移后的主要问题。 其实, 应该是作者不知道sys.exit的后果。作者把sys.exit当成os._exit了。
如下是APUE里的一幅图:
标准C里的exit函数在调用后, 不是马上退出程序, 而是运行一些收尾工作。比如把buffer里的数据flush到磁盘等等工作。然后才是调用真正的退出函数:_exit

python的sys.exit是抛出一个异常:SystemExit, python会默认会catch这个异常, 大意为:

try:
    我们的python代码
exception SystemExit:
    做一些收尾工作, 如flush之类的



递归, 循环, 迭代

工作中, 需要有历遍目录进行一些操作. python自带的os.walk很强大, 但是没有maxdepth这种层数参数.

os.walk是原本是一个递归. 下面是它的代码:
273     try:
274         # Note that listdir and error are globals in this module due
275         # to earlier import-*.
276         names = listdir(top)
277     except error, err:
278         if onerror is not None:
279             onerror(err)
280         return
281
282     dirs, nondirs = [], []
283     for name in names:
284         if isdir(join(top, name)):
285             dirs.append(name)
286         else:
287             nondirs.append(name)
288
289     if topdown:
290         yield top, dirs, nondirs
291     for name in dirs:
292         path = join(top, name)
293         if followlinks or not islink(path):
294             for x in walk(path, topdown, onerror, followlinks):
295                 yield x
296     if not topdown:
297         yield top, dirs, nondirs

BTW:: 这里需要注意*nux的ENAMETOOLONG 错误.

递归无法按"层"历遍目录. 它只能一条一条路径走尽.

朋友说使用循环也可以实现递归, 这种代码从来没有写过. 也一直在迷惑。
但是受 http://www.devshed.com/c/a/Python/Basic-Threading-in-Python/2/ 的启发.
突然明白循环是怎么做到递归的。 于是自己使用循环自己写了一个历遍的函数:

def loop_walk(top, n):
    stack = Queue.Queue()
    sub_stack = Queue.Queue()
    dirs = []
    files = []
    error = None
    stack.put([top])
    
    while True:
        if stack.empty():
            # stack被取完了, 下一层队列
            stack, sub_stack = sub_stack, stack
            if n <= 1:
                yield [], [], None
                return
            else:
                n -= 1
            continue
        else:
            try:
                top_list = stack.get(False)
            except Queue.Empty:
                yield [], [], None
                return

        for top in top_list:
            try:
                for item in os.listdir(top):
                    item = os.path.join(top, item)
                    if os.path.isdir(item):
                        dirs.append(item)
                    else:
                        files.append(item)
            except:
                # 出错, 比如没有权限
                #traceback.print_exc()
                error = top
            yield dirs, files, error
            if dirs:
                sub_stack.put(dirs)
            dirs = []
            files = []
            error = None

代码长度double了.........

Sunday, July 18, 2010

IO笔记3

read(2)和write(2)都有一个特点: 期待被处理的数据与已经被处理的数据量可以不一致.
比如:
char buffer[5000];
size_t count = read(1, buffer, 4096);

调用read读4096个bytes, 但是是不是真的已经读了4096个bytes, 这是没有必然要求的.

socket的I/O是最明显的.

如果不一致, 就要求程序员去处理. 不过, 这都是常事了, 都有满足这样需求的函数.

但是, 这就需要程序员可以自己分清这两种情况的函数.

不要认为sys.stdin.read(4096)和os.read(sys.stdin.fileno(), 4096)是一样的.

$ (echo -e "12345\n"; sleep 10) | python -c '
import sys
print sys.stdin.read(4096)'

上面的python程序是被block了10秒才运行print的. 但是下面的是马上收到数据的:
$ (echo -e "12345\n"; sleep 10) | python -c '
import sys
import os
print os.read(sys.stdin.fileno(), 4096)'

C的standard I/O是一组保证可以返回指定数据(量)的函数

standard I/O增加了buffer, 以减少read, write的次数. 原则就是每次调用read的时候读入BUF_SIZE的数据到buffer空间中, 下面是read函数的原型:

ssize_t read(int fd, void *buf, size_t count);

count是会根据file descriptor的不同变化, 如:
1. terminate device时为1024
2. 不是terminate device时为是page size, 或者程序员使用setvbuf指定

standard I/O有三个种非格式化函数:
1. character-at-a-time I/O
2. line-at-a-time I/O
3. direct I/O

无论使用哪种函数, 它都是如下的方式:

read(fd, buffer_ptr, BUF_SIZE);

那怕你只是想使用fgetc得到一个字符. 都是调用上面的函数. 一般情况BUF_SIZE >= 1024.

standard I/O除了缓存外. 还保证了返回的数据是想得到的, 如:
1. character-at-a-time I/O时, 保证返回一个字符
2. line-at-a-time I/O时, 保证返回一行数据, 以NULL结尾.
3. direct I/O时, 保证返回指定的数据结构.

否则, 会多次调用上面的read函数, 直到取得的数据满足要求, 过程中会出来下面的情况:
1. block;
2. EOF;
3. Error

Friday, July 16, 2010

IO笔记2

两层buffer的存在

 23 #include <stdio.h>
 24
 25 int main(int argc, char *argv[]){
 26     int retval ;
 27     while(1){
 28         char c[BUFSIZ];
 29         retval = read(0, c, 1);
 30         printf("%c\n", *c);
 31     }
 32     return 0;
 33 }

上面的代码, 用户输入一次, read函数被调用N次(数据的长度)。也就是说用户输入的数据已经被缓存了。
再如下:

 23 #include <stdio.h>
 24
 25 int main(int argc, char *argv[]){
 26     int retval ;
 27     int i;
 28     int c;
 29     printf("%d\n", BUFSIZ);
 30     for(i = 0; i < BUFSIZ / 2; i++){
 31         c = fgetc(stdin);
 32     }
 33     return 0;
 34 }

上面的代码fgetc被调用N次(数据的长度), 但是read的调用情况如下:
1. 当stdin不是terminate device时, 被调用一次, read的count参数为4096
2. 当stdin是terminate device时, 被调用两次, read的count参数为1024
* 至少怎么得到这种数据, 答案是使用strace凶器。

libc会根据stdin是什么使用setvbuf对FILE对象设置buffer.

从上面两个例子可以知道:
1. 使用standard I/O从terminate device读数据时, 其实经过了两个buffer机制。
2. standard I/O读不同file descriptor时的buffer大小会变化。 加上从APUE中知道的,情况会是这样
 * 当stdin, stdout是terminate device时, standard I/O会使用行缓存, buffer大小为1024.
 * 当stdin, stdout不是terminate device时, standard I/O会使用full buffer, buffer大小为4096.

好, 知道这两种buffer机制与buffer大小了, 移步看这一篇文章会有很好的收获:
 * http://www.pixelbeat.org/programming/stdio_buffering/

这文章从一个例子开始, 如下:
# tail -f access.log | cut -d' '|uniq
为什么上面的命令没有输出? 呵呵, 其中就是IO缓存的机制.

预读数据, 减少read, write

使用standard I/O都是会预读一定数量的数据, 用于减少调用read, write的次数.
使用standard I/O的好处就是程序员无需操心这个预读数据的大小, 只需要知道它是预读的即可.
如果不知道会预读的话, 上面的URL的例子就无法明白.


Sunday, July 11, 2010

IO笔记

IO部分总是很乱。不知道是因为API多还是其它原因:
 0. 阻塞的存在
 1. glibc的section3手册和linux的section2有同名函数。
 2. posix, ISO都有要求
 3. terminal IO与其它IO有不同的表现
 4. IO缓存的机制
 5. 还有众多古老的名词....

EOF        
EOF是什么? 什么时候才会产生? 还是和ctrl_c一样是signal ?

如下代码的运行結果是 -1
 19 #include <stdio.h>
 20
 21 int
 22 main(int argc, char *argv[])
 23 {
 24     printf("%d\n", EOF);
 25     return 0;
 26     
 27 } 

表示EOF是 -1 ?

当某一次使用 ssize_t read(int fd, void *buf, size_t count); 函数从一个file descriptor中读数据时(count>0), 函数得到0个字节表示读到文件的尾部了. 这时read函数返回0.

可读端的pipe关闭后, 只读端pipe在读完后会产生EOF:
  2 import os
  3 import sys
  4 import time
  5
  6 read_end, write_end = os.pipe()
  7
  8 pid = os.fork()
  9 if pid < 0:
 10     print "Error"
 11     sys.exit(1)
 12 if pid == 0:
 13     # parent
 14     os.close(write_end)
 15     try_time = 1
 16     # 因为无法得到child process会发多少数据, 只能使用死循环去读数据
 17     while True:
 18         print "%s time(s) call read" % try_time
 19         content = os.read(read_end, 1)
 20         if not content:
 21             break
 22         os.write(1, content)
 23         try_time += 1
 24     sys.exit(0)
 25
 26 else:
 27     # child
 28     os.close(read_end)
 29     os.write(write_end, "#" * 100)
 30     # 关了parent就能读到EOF
 31     os.close(write_end)
 32     sys.exit(0)



文件系统中的文件是有固定大小(某一状态), 但是在terminal IO里,是没有一个固定的尾部的, 需要输入者指定什么时候才是end-of-file, 所以, ctrl_D出现了. ctrl_D是一个特殊控制符。被tty的驱动处理(默认情况下用户程序不能从read函数得到, 除非要求tty不处理)

在了解ctrl_D前, 应该先要了解IO的缓存机制.

standard IO缓存 与 底层IO缓存
在此这前, 我对getchar这个函数不解, 其实就是不解如下的代码:
 19 #include <stdio.h>
 20
 21 int main(int argc, char *argv[]){
 22     char c ;
 23     while(c = getchar() ){
 24        printf("%c\n", c);
 25     }  
 26 }  

不要以为简单调用getchar函数可以实现vim这种交互.
当调用read去读stdin的数据时, 是行缓存的.上面的代码在第一次调用getchar时把一行的数据读到buffer里(回车键返回), 下次再调用getchar时会从buffer里取. buffer没有数据后再等待用户输入.

标准IO与系统底层IO这两套IO需要分开. 不然会出现事与愿违的情况, 如:
使用setbuf对FILE对象进行了设置后, 然后使用调用printf操作. 結果没有出现想要的设置效果。例如:

 23 #include <stdio.h>
 24 #include <string.h>
 25 #include <unistd.h>
 26
 27
 28 int main(int argc, char *argv[]){
 29     //
 30     FILE *input =  fdopen(1, "r");
 31     FILE *output =  fdopen(1, "w");
 32     setbuf(input, NULL);
 33     setbuf(output, NULL);
 34     char c ;
 35
 36     printf("Hello World");
 37     sleep(10);
 38 }

上面的printf的内容是过了10秒后才打印的, 明显, setbuf没有起作用。 但是如果修改为如下就不同了:
 23 #include <stdio.h>
 24 #include <string.h>
 25 #include <unistd.h>
 26
 27
 28 int main(int argc, char *argv[]){
 29     //
 30     FILE *input =  fdopen(1, "r");
 31     FILE *output =  fdopen(1, "w");
 32     setbuf(input, NULL);
 33     setbuf(output, NULL);
 34     char c ;
 35
 36     fprintf(output, "Hello World");
 37     sleep(10);
 38 }
上面代码的区别在于:printf使用了stdin这个FILE对象(默认是line buffer), fprintf使用了指定的FILE对象。standard IO的缓存是通过修改FILE对象实现的。

read和write会不会缓存, 缓存机制是什么? 这与read, write操作的对象有很大的关系, 例如:

 28 int main(int argc, char *argv[]){
 29
 30     while(1){
 31         char c;
 32         (void *)read(0, &c, 1);
 33         (void *)write(1, &c, 1);
 34     }
 35 }
上面虽然使用了read, write, 还是用户输入还是行缓存的, 无法像vim一样交互操作。这时要想非行缓存, 需要操作到tty的缓存机制。 如下的一段python代码设置了tty的缓存方式:

  1 #coding:utf-8
  2 import os
  3 import sys
  4 import termios
  5
  6 STDIN_FILENO = sys.stdin.fileno()
  7
  8 old_attr = termios.tcgetattr(STDIN_FILENO)
  9 new_attr = termios.tcgetattr(STDIN_FILENO)
 10
 11 new_attr[3] &= ~ (termios.ICANON | termios.ECHO)
 12 termios.tcsetattr(STDIN_FILENO, termios.TCSADRAIN, new_attr)
 13 try:
 14     print '请使用vim的移动键'
 15     while True:
 16         c= os.read(STDIN_FILENO, 1)
 17         if c == 'j':
 18             print "下"
 19         elif c == 'k':
 20             print "上"
 21         elif c == 'h':
 22             print '左'
 23         elif c == 'l':
 24             print '右'
 25 except KeyboardInterrupt, e:
 26     termios.tcsetattr(STDIN_FILENO, termios.TCSADRAIN, old_attr)


回到ctrl_D的问题
在terminal IO里, 因为默认是使用缓存的, 赋于ctrl_D两种功能:
 1. 相当于flush操作.  即时把输入的数据返回给read函数, 不用等待'\n'的出现。
 2. 让tty返回0字节给read函数. 表示EOF。 在单独输入ctrl_D后产生这种效果

控制符        
控制符都是tty驱动提供的一种方便功能, 有少量的控制符会产生signal的:
^Z和^C
还有少量的是无法修改的:
\r和\n

如下代码把“a“作为backspace功能:

  1 #coding:utf-8
  2 import os
  3 import sys
  4 import termios
  5
  6 STDIN_FILENO = sys.stdin.fileno()
  7
  8 old_attr = termios.tcgetattr(STDIN_FILENO)
  9 new_attr = termios.tcgetattr(STDIN_FILENO)
 10
 11 new_attr[6][termios.VERASE] = 0x61
 12 termios.tcsetattr(STDIN_FILENO, termios.TCSADRAIN, new_attr)
 13 try:
 14     print "请使用a键删除数据"
 15     os.read(0,100)
 16 except KeyboardInterrupt, e:
 17     termios.tcsetattr(STDIN_FILENO, termios.TCSADRAIN, old_attr)