Archive

Posts Tagged ‘linux’

ip分片与重组

December 19th, 2014 No comments

最近在做udp pump, 突然手贱的想要知道一个udp包的最大负载的是多少.
于是写了一个简单的程序来测试了一下, client不断的发送payload size不断增加的包, 然后在server端收数据包. 当然了由于IP的最大payload只有65535-sizeof(IP Header).所以Udp的最大负载也就是只有大约65535-20-8 = 65507个. 如果超过这个大小以后,不是收不到, 而是client根本发不出去这个数据包

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket
import random
import string
import sys
import hashlib

def sendData(udp, ip,port, size):
    data = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(size))
    md5 = hashlib.new('md5')
    md5.update(data)
    udp.getsockname()
    udp.sendto(data,(ip,port))
    print "%s :: sending %08d bytes to %s:%d   --> %s" % ( udp.getsockname(), len(data), ip,port, md5.hexdigest())


def run(ip,port,begin,inc,end):
    udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_IP)
    udp.bind(("0.0.0.0",0))
    sendData(udp, ip, port, 65507)
    sendData(udp, ip, port, 65508)

用上面的代码发送可以很轻松的验证,发送65507个字节的时候是OK的,但是65508的数据就会得到Message Too Long的错误.

接下来呢, 我做了一件错误的事情,然后导致了后来一系列的事情的发生.
我在server端使用tcpdump来查看网络数据包, udp server listen在6666端口.所以我使用命令来查看数据包

sudo tcpdump -i any -nnn -vvv "port 6666"

注意我的 tcpdump的filter是”port 6666″,也就是采用过滤端口的方式.这个也就是我想当然的认为ip fragment以后的每一个udp packet的port应该和原始数据是一致.

拿到tcpdump的数据以后我下了一大跳

17:06:09.875525 IP (tos 0x0, ttl 61, id 28605, offset 0, flags [+], proto UDP (17), length 1500)
    192.168.80.233.52099 > 10.15.10.50.6666: UDP, length 65507

请看IP的段显示的数据大小为1500, 这个很容易理解,毕竟大部分的网络的MTU都是1500,这里也不例外.但是UDP段显示的length是65507, 这个也说得过去, 我发送的数据就是这么大的. 但是问题来了,为啥只有这么一个数据包. 于是我猜测抓包显示的数据错掉了.我把抓包数据存下来以后发现文件大小也就是1500多一点大.那么很明显这个数据包里面的数据真的只有1500 bytes. 这里的事情被我认为是非常诡异的, 因为我发送数据的时候对数据做了一个md5,udpserver收到数据以后也会做一个md5. 这两个md5是一模一样的,那么很明显证明了server端收到的数据和client发送的数据是一样的.
但是抓的包却只有1500 bytes啊,剩下的数据死到哪里去了呢?
我一度怀疑tcpdump有bug,于是换成了wireshark(tshark). 但是得到的结果是一样,见鬼了. 于是乎一阵google以后找到了使用raw socket来抓数据包的代码在这里.感谢作者的无私分享. 用这个程序跑了一下,只抓udp的包. 从抓包数据看,端口为6666的仍旧只有1500字节的ip数据, 但是很奇怪的是那些非6666端口的其他数据包里面的数据和我发送的数据十分相似–随机的字符数字组合. 然后我发现了一个东西,也是这个东西让我明白了这到底是怎么回事.

IP Header
   |-IP Version        : 4
   |-IP Header Length  : 5 DWORDS or 20 Bytes
   |-Type Of Service   : 0
   |-IP Total Length   : 1500  Bytes(Size of Packet)
   |-Identification    : 63208
   |-Dont Fragment Field   : 0
   |-More Fragment Field   : 0
   |-TTL      : 62
   |-Protocol : 17
   |-Checksum : 35759

这个让我感觉很奇妙的东西就是Identification. 因为抓包数据中的所有包头都含有相同的Identification. 这个不免让我觉得其实IP分片以后实际上是靠Identification来决定是不是同一个上层数据包的内容的. 于是我查看了一下ip分片的实现

/* Find the correct entry in the "incomplete datagrams" queue for
 * this IP datagram, and create new one, if nothing is found.
 */
static inline struct ipq *ip_find(struct net *net, struct iphdr *iph, u32 user)
{
        struct inet_frag_queue *q;
        struct ip4_create_arg arg;
        unsigned int hash;

        arg.iph = iph;
        arg.user = user;

        read_lock(&ip4_frags.lock);
        hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);

        q = inet_frag_find(&net->ipv4.frags, &ip4_frags, &arg, hash);
        if (q == NULL)
                goto out_nomem;

        return container_of(q, struct ipq, q);

out_nomem:
        LIMIT_NETDEBUG(KERN_ERR "ip_frag_create: no memory left !\n");
        return NULL;
}

上面的ip_find是从ip_local_deliver => ip_defrag => ip_find这个调用链过来的. 我们可以看到这里使用ip header里面的id, source addr, dest addr和protocol来做一个hash, 远没有port啥事.
到这里已经很清楚了, udp发的数据的最大负载是65535-20 -8 = 65507. 但是到了ip层的话还是需要被分片的, 否则过不了交换机. ip分片的依据的ip header中的id, 当然还需要由frags来指明fragment的offset之类的信息.
这个时候再回头来想想当初为何就没有想到, port是udp/tcp层的东西, ip层是没有这个玩意儿的. 那么分片自然就是和port无关的了.

Categories: programming Tags: , , ,

为何多线程程序占用这么多内存(linux)(续)

January 26th, 2014 2 comments

上一篇文章指出了在Centos6.4 x86_64下面多线程程序会相当占用内存资源。经过一番google和代码查看。终于知道了原来是glibc的malloc在这里捣鬼。请看developerworks该文章指出在glibc 2.10以上的版本会有这个问题,我的glibc版本是2.12

lrwxrwxrwx 1 root root 12 Oct 21 21:29 /lib64/libc.so.6 -> libc-2.12.so

glibc为了分配内存的性能的问题,使用了很多叫做arena的memory pool,缺省配置在64bit下面是每一个arena为64M,一个进程可以最多有 cores * 8个arena。假设你的机器是4核的,那么最多可以有4 * 8 = 32个arena,也就是使用32 * 64 = 2048M内存。 当然你也可以通过设置环境变量来改变arena的数量.例如export MALLOC_ARENA_MAX=1
hadoop推荐把这个值设置为4。当然了,既然是多核的机器,而arena的引进是为了解决多线程内存分配竞争的问题,那么设置为cpu核的数量估计也是一个不错的选择。设置这个值以后最好能对你的程序做一下压力测试,用以看看改变arena的数量是否会对程序的性能有影响。

后记:如果你打算在程序代码中来设置这个东西,那么可以调用mallopt(M_ARENA_MAX, xxx)来实现,不过很奇怪的是我在centos6.4上面居然看不到mallopt的man page,最后实在我的ubuntu 12.04的虚拟机上看到这个函数解释,而且里面并没有M_ARENA_MAX这个宏的解释。最后我实在glibc2.12的malloc/malloc.c的mALLOPt函数实现中才看到M_ARENA_MAX的。

Categories: programming Tags: , ,

为何多线程程序占用这么多内存(linux)

January 16th, 2014 No comments

我们的应用基本都是多线程的程序,程序启动以后如果在client发起命令之前内存占用量并不大,大概在几百兆左右,但是经过一段时间的运行呢,有的进程会占用到3~4G的内存。但是继续运行下去的话也不会继续增加了。由于增长到最高点以后内存使用量没有明显的大幅攀升,所以一直也没有特别的关注这个问题。最近呢,老大要看看这个问题究竟是咋回事,于是呢,有了本文。
一开始,我认为是程序逻辑分配了这么多内存。为了证明我的观点,我首先使用了ltrace。
ltrace -f -e “malloc,realloc,calloc,free” -p pid > trace 2>&1
上面是利用ltrace来检测程序对于malloc,realloc,calloc,free的调用情况。加上-f参数是因为是多线程的程序。当然,为了查看方便,我将上述命令的结果重定向到了一个文件。
当程序运行到了一个内存高点,例如3G的时候,取出trace文件,这个时候并没有让程序退出(否则可能会有很多free的调用)。trace文件大致是这个样子的:

[pid 21161] malloc(9) = 0x1b55a40
[pid 21161] malloc(9) = 0x1b58410
[pid 21161] free(0x1b55a40) =  <void>
于是,写了一个很简单的python脚本来分析程序的内存分配释放情况。最后得到的答案出乎我的意料。程序从启动稳定在300M左右到内存最高点期间的内存分配而没有释放的总量只有200k+,远小于1M。于是乎我怀疑自己统计错了。有重复做了几遍相同的操作,得到的答案都是一样的。

在多次使用ltrace无果以后,我开始怀疑程序内部是不是用到mmap之类的调用,于是使用strace对mmap,brk等系统函数的检测:
strace -f -e “brk,mmap,munmap” -p pid > trace 2>&1
测试方法同上,运行到内存高点以后查看strace文件。文件内容大致如下:
brk(0) = 0x6fd000
mmap(NULL, 4096, PROTREAD|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7f77f9853000
mmap(NULL, 82462, PROTREAD, MAPPRIVATE, 3, 0) = 0x7f77f9604000
我检查了一下trace文件也没有发现大量内存mmap动作,即便是brk动作引起的内存增长也不大。
于是乎我开始没有方向了 …
后来,我开始减少thread的数量开始测试,在测试的时候偶然发现一个很奇怪的现象。那就是如果进程创建了一个线程并且该线程运行以后内存使用量就会激增。我写了一个简单的程序,大致内容如下:

void* thread_run( void* ) {
    cout << "thread running,    VM SIZE:" << memusage() << " MB" << endl;
}

int main()
{
    cout << "process running,   VM SIZE:" << memusage() << " MB" << endl;

    pthread_t th;
    pthread_create(&th, 0, thread_run, 0);
    cout << "thread created,    VM SIZE:" << memusage() << " MB" << endl;

    pthread_join( th, 0 );

    cout << "after join thread, VM SIZE:" << memusage() << " MB" << endl;

    return 0;
}

以上调用的memusage()是用来获取当前进程的virtual memory usage的函数。
在我的机器上(CENTOS 6.4,X86-64)上运行结果如下:
process running, VM SIZE:13 MB
thread created, VM SIZE:23 MB
thread running, VM SIZE:87 MB
after join thread, VM SIZE:87 MB
也就是说,程序启动以后用了13M内存,创建了一个线程以后内存使用量增加了10M(这是因为64bit上缺省的线程stack size是10M,这个可以在创建线程的时候指定)。此时线程应该还没有运行,因为当前的主线程还没有被切换掉。当线程运行起来了以后,整个process的内存使用量立马又增加了64M。而且这个增加的64M内存在线程被join以后也没有被回收。

很神奇吧,反正我是没有反应过来是咋回事。接下来,让程序停在threadrun函数里面,然后使用pmap查看进程的内存情况。pmap -x的输出情况如下:


12346: ./test
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 8 8 8 r-x-- test
0000000000602000 4 4 4 rw--- test
000000000104e000 132 8 8 rw--- [ anon ]
0000003f71800000 128 104 0 r-x-- ld-2.12.so
0000003f71a1f000 4 4 4 r---- ld-2.12.so
0000003f71a20000 4 4 4 rw--- ld-2.12.so
0000003f71a21000 4 4 4 rw--- [ anon ]
0000003f72000000 1576 324 0 r-x-- libc-2.12.so
0000003f7218a000 2044 0 0 ----- libc-2.12.so
0000003f72389000 16 16 8 r---- libc-2.12.so
0000003f7238d000 4 4 4 rw--- libc-2.12.so
0000003f7238e000 20 16 16 rw--- [ anon ]
0000003f72400000 92 68 0 r-x-- libpthread-2.12.so
0000003f72417000 2048 0 0 ----- libpthread-2.12.so
0000003f72617000 4 4 4 r---- libpthread-2.12.so
0000003f72618000 4 4 4 rw--- libpthread-2.12.so
0000003f72619000 16 4 4 rw--- [ anon ]
0000003f72c00000 524 20 0 r-x-- libm-2.12.so
0000003f72c83000 2044 0 0 ----- libm-2.12.so
0000003f72e82000 4 4 4 r---- libm-2.12.so
0000003f72e83000 4 4 4 rw--- libm-2.12.so
0000003f7f000000 88 16 0 r-x-- libgccs-4.4.7-20120601.so.1
0000003f7f016000 2044 0 0 ----- libgccs-4.4.7-20120601.so.1
0000003f7f215000 4 4 4 rw--- libgccs-4.4.7-20120601.so.1
0000003f7fc00000 928 528 0 r-x-- libstdc++.so.6.0.13
0000003f7fce8000 2048 0 0 ----- libstdc++.so.6.0.13
0000003f7fee8000 28 28 28 r---- libstdc++.so.6.0.13
0000003f7feef000 8 8 8 rw--- libstdc++.so.6.0.13
0000003f7fef1000 84 12 12 rw--- [ anon ]
00007f7e4c000000 132 8 8 rw--- [ anon ]
00007f7e4c021000 65404 0 0 ----- [ anon ]
00007f7e50c2c000 4 0 0 ----- [ anon ]
00007f7e50c2d000 10260 28 28 rw--- [ anon ]
00007f7e51645000 12 8 8 rw--- [ anon ]
00007fffbb182000 84 12 12 rw--- [ stack ]
00007fffbb1ff000 4 4 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 r-x-- [ anon ]
total kB 89820 1260 188

请注意65404这一行,种种迹象表明,这个再加上它上面那一行(在这里是132)就是增加的那个64M)。后来增加thread的数量,就会有新增thread数量相应的65404的内存块。(后来发现可能不全是65404,我看到过65400,但是加上该行之上的哪一行数据都是64M)
而且我们看,65404这一行里面的该内存区域的权限,全部turn off,也就是该内存是不可读,不可写,不可执行。实在是不明白这个内存块有啥用处。

还有一点,就是估计到了一定thread的数量以后,增加量就不是64M了。我测试的时候发现创建了一个100个thread,所有的线程都不退出的话进程的内存总量是2999M。当然了如果线程一个跑完了再创建另外一个的话那么只有开始这个64M的增长量了。

不过到这里,我们已经知道了这个多出来64M内存并不是我们的程序本身的行为(当然我现在还并不知道到底是什么地方产生出来的)。也许这个ntpl的行为,也许这个是内核的行为,现在我还不清楚而已。这个留作以后的作业吧。

后记:
我今天对测试程序做了一个strace,得到了如下结果:
strace -f -e “brk,mmap,munmap,mprotect” test
brk(0) = 0x228f000
mmap(NULL, 4096, PROTREAD|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e30000
mmap(NULL, 82462, PROTREAD, MAPPRIVATE, 3, 0) = 0x7fab81e1b000
mmap(0x3f72000000, 3745960, PROTREAD|PROTEXEC, MAPPRIVATE|MAPDENYWRITE, 3, 0) = 0x3f72000000
mprotect(0x3f7218a000, 2093056, PROTNONE) = 0
mmap(0x3f72389000, 20480, PROT
READ|PROTWRITE, MAPPRIVATE|MAPFIXED|MAPDENYWRITE, 3, 0x189000) = 0x3f72389000
mmap(0x3f7238e000, 18600, PROTREAD|PROTWRITE, MAPPRIVATE|MAPFIXED|MAPANONYMOUS, -1, 0) = 0x3f7238e000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e1a000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e19000
mmap(NULL, 4096, PROT
READ|PROTWRITE, MAPPRIVATE|MAPANONYMOUS, -1, 0) = 0x7fab81e18000
mprotect(0x3f72389000, 16384, PROT
READ) = 0
mprotect(0x3f71a1f000, 4096, PROTREAD) = 0
munmap(0x7fab81e1b000, 82462) = 0
brk(0) = 0x228f000
brk(0x22b0000) = 0x22b0000
mmap(NULL, 99158576, PROT
READ, MAP_PRIVATE, 3, 0) = 0x7fab7bf87000

从上面没有看出应用程序有调用分配出64M左右的空间且权限是PROTNONE,或者更改某个约64M大小的区域的权限到PROTNONE的请求。难道这个6神秘的64M真的来自内核?看来这个问题只能说 “未完,待续 …”

Categories: programming Tags: , ,

putty无密码登陆linux box

January 16th, 2014 No comments

工作的时候很多时间都是使用putty登陆到linux机器进行操作,由于需要每次登陆都要输入密码,这多多少少有点烦人,况且又在局域网内部。所以自然想到了不输入密码使用putty登陆ssh server。
网上的方法很多,基本上都是一致的。
首先,使用putty的keygen工具puttygen.exe生成公钥和密钥。点击Generate然后在空白区域瞎乱动鼠标就行了。在生成公钥和密钥的时候我选择的类型是ssh-2 RSA,key长度是1024bits.生成以后保存private key文件。保存private key文件的时候不要输入密码。其实这个文件里面既有公钥又有密钥。当然你还可以单独保存一个公钥文件。
然后将上一步保存的公钥文件上传到你需要登陆的机器的用户目录下的~/.ssh目录。假设你的公钥文件叫做rsa_pub.现在使用命令
ssh-keygen -i -f rsa_
pub > rsa.pub
接着保存rsa.pub到authorized_keys文件就可以了。
cat rsa.pub >> authorized_
keys
然后启动putty,找到你登陆目标机器的那个保存的session,如果没有的话那么现在新建一个。找到connection->ssh->auth
在右边的Private key file authentication里面输入在第一步生成的private key的全路径。
照理,这个时候你使用putty登陆该linux机器的时候就无需输入密码了。但是我在使用的时候发现无法自动登陆成功。
putty上显示 “server refused our key”,于是按照askubuntu上的方法,解决了。(虽然我使用的linux server是centos的,但是大体上在这件事情上没有太大差别)该方法大致如下:
修改.ssh目录权限为700,也就是当前用户可读,写,执行
修改authorizedkeys的权限是600,也就是当前用户可读,写
修改/etc/ssh/sshdconfig,是下面这一句生效,也就是去掉行首的’#’
AuthorizedKeysFile .ssh/authorized_
keys
然后重启sshd
service sshd restart
就可以了。

其实linux下面需要无密码登陆到其他机器的做法也是一样的,首先用ssh-keygen生成一个公钥密钥对,然后上传公钥到目标机器的目标用户目录,后面的工作和上面一样的了。
ssh-keygen -t rsa.
同样的,在生成文件的时候不要输入密码

Categories: programming Tags: ,

组播的时候到底该如何绑定网卡

August 2nd, 2012 No comments

  以前在代码中,遇到组播都是一直绑定0.0.0.0然后去把自己加入到组播组里面去。但是最近的一次改动却让我发现了一个问题。

因为在显示的环境中机器上会有很多网卡(>3)。 而我们指向接收其中一个子网中的组播中的数据,那么很明显的想法就是接收端绑定相应子网的IP。在windows环境中,我们绑定了机器上该子网的IP(例如:172.16.20.40)。实际测试的发现一切良好。然后在Linux环境中沿用了这个code。结果却发现无论如何都收不到组播消息。于是一阵瞎倒腾,发现在linux上绑定在组播组的地址上就可以收到消息。晕,系统不一样表现就是不一样啊。但是事情并没有结束,我并没有弄清楚这是咋回事。查看了公司的包装的代码发现在mutilcast client的实现上是按照Windows的设定来的。

  1. 创建UDP socket,绑定在一个网卡地址上
  2. 指定multiast group address,并且取出socket的本地地址,填入ip_mreq.imr_multiaddr和 ip_mreq.imr_interface。

而google了一番发现了一个问题,在linux上是不能绑定在物理网卡地址上的,这样做会什么信息都收不到,要么绑定0.0.0.0以收取所有的组播消息,或者绑定一个具体的组播地址以收取发到这个组播地址的消息。而具体要收取哪一个子网的组播信息的时候用ip_mreq.imr_interface来指定。详见(http://stackoverflow.com/questions/11234671/multiple-multicast-on-multiple-interfaces 和http://www.kohala.com/start/mcast.api.txt

  看起来一样的东西在不同的系统下面可能是不一样的,谨记,谨记.

Categories: programming Tags: ,

lthread代码分析(四)

March 20th, 2012 No comments

 我们知道,一个调用阻塞socket的recv操作会一直等到有数据到达以后才会返回(采用了MSG_DONTWAIT标志的不算)。如果通过异步socket来实现,那么需要使用select或者epoll,然后通过类似回调的方式来告知有数据到达。有了lthread以后呢,这个步骤就可以由lthread来实现,而用户代码只需要像以前使用阻塞socket那样直接调用recv(这里要替换成lthread的recv函数了)就可以了。在代码逻辑上会比直接使用select/epoll来的简洁得多。我们来看看lthread里面关于socket部分的实现。

#define LTHREAD_RECV(x, y)                                  
x {                                                         
    ssize_t ret = 0;                                        
    lthread_t *lt = lthread_get_sched()->current_lthread;   
    while (1) {                                             
        if (lt->state & bit(LT_FDEOF))                      
            return -1;                                      
        ret = y;                                            
        if (ret == -1 && errno != EAGAIN)                   
            return -1;                                      
        if ((ret == -1 && errno == EAGAIN)) {               
            if (timeout)                                    
                _sched_lthread(lt, timeout);                
            _lthread_wait_for(lt, fd, LT_READ);             
            if (lt->state & bit(LT_EXPIRED))                
                return -2;                                  
        }                                                   
        if (ret >= 0)                                       
            return ret;                                     
    }                                                       
}

上面的宏定义里面,我们可以看出当一个接收数据操作(可能是recv也可能是recvfrom)被调用以后,如果遇到返回值为EAGAIN,那么就会调用_sched_lthread等待一个timeout时间。在等待的过程中如果该相关的fd上有可读事件发生,那么相关的_lthread就会被调用到。如果超时以后还没有事件到达,这个recv操作就会返回-2.

#define LTHREAD_SEND(x, y)                                  
x {                                                         
    ssize_t ret = 0;                                        
    ssize_t sent = 0;                                       
    lthread_t *lt = lthread_get_sched()->current_lthread;   
    while (sent != length) {                                
        if (lt->state & bit(LT_FDEOF))                      
            return -1;                                      
        ret = y;                                            
        if (ret == 0)                                       
            return sent;                                    
        if (ret > 0)                                        
            sent += ret;                                    
        if (ret == -1 && errno != EAGAIN)                   
            return -1;                                      
        if (ret == -1 && errno == EAGAIN)                   
            _lthread_wait_for(lt, fd, LT_WRITE);            
    }                                                       
    return sent;                                            
}                                                           

同理,发送数据也是一样的,当系统缓冲区能够容纳数据的时候,返回成功。否则等待相关fd的可写事件发生以后再向其缓冲区写入数据。

当然,上面的发送和接收表明,相关的socket必须是非阻塞的,否则无法达到这些效果。下面的lthread_socket函数就说明了这个问题

int
lthread_socket(int domain, int type, int protocol)
{
    int fd;
#if defined(__FreeBSD__) || defined(__APPLE__)
    int set = 1;
#endif

    if ((fd = socket(domain, type, protocol)) == -1) {
        perror("Failed to create a new socket");
        return -1;
    }
    /*把socket设置被非阻塞的*/
    if ((fcntl(fd, F_SETFL, O_NONBLOCK)) == -1) {
        close(fd);
        perror("Failed to set socket properties");
        return -1;
    }

#if defined(__FreeBSD__) || defined(__APPLE__)
    if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &set, sizeof(int)) == -1) {
        close(fd);
        perror("Failed to set socket properties");
        return -1;
    }
#endif

    return fd;
}

当然其他的还有accept和connect之类的操作基本上也是差不多的,只要遇到EAGAIN就会调用lthread调度器切换到其他的lthread上,同时监听fd感性却的事件。
有了这个lthread和socket的绑定,撰写concurrent的socket程序应该会简单很多的

Categories: programming Tags: ,

lthread代码分析(三)

March 16th, 2012 No comments

前面我们查看了lthread的创建以及调度器的运行代码,那么在不同的lthread中到底是如何切换的呢,我们现在来看一下lthread的切换代码lthread_resume函数。

int
_lthread_resume(lthread_t *lt)
{
    if (lt->state & bit(LT_NEW))/*如果是用lthread_create新创建的lthread,那么就初始化一下一些必要的数据*/
        _lthread_init(lt);

    _restore_exec_state(lt);/*这里是从lt中回复出当前的一些必要的执行环境数据,主要的是栈空间数据*/

    lthread_get_sched()->current_lthread = lt; /*设置调度器的当前运行lthread为lt*/
    _switch(&lt->st, &lt->sched->st);/*切换cpu执行代码位置,这里已经开始执行lt的代码了*/
    lthread_get_sched()->current_lthread = NULL;/*lt的代码执行完毕,或者是lt的执行函数中主动让出了执行权*/

    if (lt->state & bit(LT_EXITED)) {/*lthread执行完毕,做一些清理工作*/
        if (lt->lt_join) {
            /* if lthread was sleeping, deschedule it so that it doesn't expire. */
            _desched_lthread(lt->lt_join);
            LIST_INSERT_HEAD(&lthread_get_sched()->new, lt->lt_join, new_next);
            lt->lt_join = NULL;
        }

        /* if lthread is detached, free it, otherwise lthread_join() will */
        if (lt->state & bit(LT_DETACH))
            _lthread_free(lt);
        return -1;
    } else {
        _save_exec_state(lt);/*lt只会暂时交出了cpu的控制权,所以需要保存当前的运行环境,主要是栈空间数据*/
        /* place it in a compute scheduler if needed.  */
        if (lt->state & bit(LT_PENDING_RUNCOMPUTE)) {
            _lthread_compute_add(lt);
            lthread_get_sched()->sleeping_state++;
        }
    }

    return 0;
}

以上的部分就是如何运行一个lthread的代码,那么里面的函数saveexecstate和restoreexecstate分别是用来保存和回复运行环境的栈空间数据的。

inline int
_restore_exec_state(lthread_t *lt)
{
    if (lt->stack_size)
        memcpy(lt->st.esp, lt->stack, lt->stack_size);/*把保存下来的占空间数据copy到lt对应的sched的申请的栈空间*/

    return 0;
}

int
_save_exec_state(lthread_t *lt)
{
    void *stack_top = NULL;
    size_t size = 0;
    /*计算当前使用了多少占空间*/
    stack_top = lt->sched->stack + lt->sched->stack_size;
    size = stack_top - lt->st.esp;

    if (size && lt->stack_size != size) {
        if (lt->stack)
            free(lt->stack);
        if ((lt->stack = calloc(1, size)) == NULL) {
            perror("Failed to allocate memory to save stackn");
            abort();
        }
    }

    lt->stack_size = size;
    if (size)
        memcpy(lt->stack, lt->st.esp, size);/*把当前使用的栈上的数据保存下来,这样resume lthread的时候就可以使用了*/

    return 0;
}

这里我们可以看出,在lthread切换的时候的栈上的数据是直接保存在一个申请的额外空间里面的。最开始我也认为这样非常不好。但是如果不这样的话,就需要为每一个lthread申请一个很大的内存区域用作栈空间,这个明显是不现实的,特别是lthread的实例比较多的情况下。当然,现在这种处理方式也有一个问题。那就是lthread在执行的过程中使用了很多stack上的内存,例如char tmpBuffer[1024*1024]。这样会导致每次lthread切换的时候会有很多数据被copy过来,copy过去。如果在lthread的执行过程中需要一个大内存数据块,最好在堆上面分配内存,而不要使用栈上的内存。

Categories: programming Tags: ,

lthread代码分析(二)

March 16th, 2012 No comments

上回介绍了一些lthread的重要的数据结构,还有lthread是如何切换cpu执行指令的位置来达到任务切换的,现在来看看lthread的一些基本操作。不过说了这么多lthread到底是一个什么东西呢? 简单的说,lthread是一个用户态的轻量级线程,并且是不可比强制切换的哪一种。只有在lthread的执行过程中主动调用了一些函数,例如lthread_sleeping之类的函数,才会由sched来进行任务切换。也就是说lthread是一个non-preempty thread。

上回已经说过了一个lthread的数据结构是_lthread,当我们需要使用lthread的时候,首先需要创建一个lthread的实例 _lthread_create就是创建这个lthread实例的,类似于pthread_create这种函数.

int
lthread_create(lthread_t **new_lt, void *fun, void *arg)
{
    lthread_t *lt = NULL;
    pthread_once(&key_once, _lthread_key_create);
    sched_t *sched = lthread_get_sched();
    /*
    new_lt 这个是被创建的lthread的指针,成功以后就可以使用*new_lt进行lthread操作
    fun    和pthread的意思一样,就是lthread的执行函数
    arg    和pthread的意思一样,这个值将会传入你的提供的lthread执行函数
    */
    if (sched == NULL) {
        sched_create(0);/*创建一个调度器,这个就会在以后有所说明*/
        sched = lthread_get_sched();
        if (sched == NULL) {
            perror("Failed to create scheduler");
            return -1;
        }
    }

    if ((lt = calloc(1, sizeof(lthread_t))) == NULL) {
        perror("Failed to allocate memory for new lthread");
        return errno;
    }
    /*
    如果没有调度器话那么就创建一个,一个系统线程只会有一个调度器,所有在这个系统线程上运行的lthread都由这个调度器进行调度操作
    */
    lt->sched = sched;
    lt->stack_size = 0;
    lt->state = bit(LT_NEW);
    lt->id = sched->total_lthreads++;
    lt->fun = fun;
    lt->fd_wait = -1;
    lt->arg = arg;
    lt->birth = rdtsc();
    lt->timeout = -1;
    *new_lt = lt;
    LIST_INSERT_HEAD(&lt->sched->new, lt, new_next);
     /*
     为lthread分配内存,并且初始化一些参数。
     将当前新建的lthread的调度器指针指向当前系统线程拥有的调度器上, 初始化stack_size为0,这个时候lthread并未执行,并且将状态置为new
     记录下线程创建时间。然后把新建的线程记录到调度器的new lthread list中去,以便调度器操作
    */
    return 0;
}

上面的lthreadcreate中调用了sched_create用于创建lthread的调度器,现在我们来看看调度器是如何创建的

int
sched_create(size_t stack_size)
{
    sched_t *new_sched;
    size_t sched_stack_size = 0;
    /*选择一个栈空间大小,不设置的话就用8M这个缺省值*/
    sched_stack_size = stack_size ? stack_size : MAX_STACK_SIZE;

    if ((new_sched = calloc(1, sizeof(sched_t))) == NULL) {
        perror("Failed to initialize schedulern");
        return errno;
    }
    /*将调度器设置到系统线程的TLS中,以便在当前的系统线程的任何一个地方都可以使用*/
    pthread_setspecific(lthread_sched_key, new_sched);
    /*为调度器分配栈空间大小,其实所有这个调度器运行的的lthread都会使用这个栈*/
    if ((new_sched->stack = calloc(1, sched_stack_size)) == NULL) {
        free(new_sched);
        perror("Failed to initialize schedulern");
        return errno;
    }

    bzero(new_sched->stack, sched_stack_size);/*不知道为何要执行这个操作,感觉没有什么必要*/
    /*
     创建一个poller用于获取网络IO的事件,以后估计会有File IO的事件。
    */
    if ((new_sched->poller = create_poller()) == -1) {
        perror("Failed to initialize pollern");
        _sched_free(new_sched);
        return errno;
    }

    if (pthread_mutex_init(&new_sched->compute_mutex, NULL) != 0) {
        perror("Failed to initialize compute_mutexn");
        _sched_free(new_sched);
        return errno;
    } 
    /*创建pipe,这样的好处是即便当前没有任何网络事件,也可以用这个pipe来唤醒调度器*/
    if (pipe(new_sched->compute_pipes) == -1) {
        perror("Failed to initialize pipen");
        _sched_free(new_sched);
        return errno;
    }

    new_sched->stack_size = sched_stack_size;

    new_sched->total_lthreads = 0;
    new_sched->default_timeout = 3000000u;
    new_sched->waiting_state = 0;
    new_sched->sleeping_state = 0;
    new_sched->sleeping = RB_ROOT;
    new_sched->birth = rdtsc();
    LIST_INIT(&new_sched->new);
    *将调度器的cpu state清空,调用lthread_run并且有lthread需要运行的话,这个st将会被设置*/
    bzero(&new_sched->st, sizeof(struct _cpu_state));

    return 0;
}

当lthread以及相关的sched的实例都创建好了,我们就可以使用lthread_run来执行了

void
lthread_run(void)
{
    sched_t *sched;
    lthread_t *lt = NULL, *lttmp = NULL;
    int p = 0;
    int fd = 0;
    int ret = 0;
    (void)ret; /* silence compiler */

    sched = lthread_get_sched();/*从系统线程中取得设置的调度器,每一个系统线程至多有一个调度器*/

    while (sched->sleeping_state ||
        !LIST_EMPTY(&sched->new) ||
        sched->waiting_state) {/*如果有lthread处于sleep状态,等待网络事件状态,或者有新建的lthread就一直执行,否则退出*/

        /*
         运行那些过期的lthread,例如某一个lthread设置10s以后再次被执行,那么如果现在已经过了10s,那么这个
         lthread就需要被执行。这个是一种设置timer的好方法
        */
        _resume_expired_lthreads(sched);

        /*运行那些被lthread_create创建的lthread实例*/
        while (!LIST_EMPTY(&sched->new)) {
            LIST_FOREACH_SAFE(lt, &sched->new, new_next, lttmp) {
                LIST_REMOVE(lt, new_next);
                _lthread_resume(lt);
            }
        }

        /* 3. resume lthreads we received from lthread_compute, if any */
        while (1) {
            pthread_mutex_lock(&sched->compute_mutex);
            lt = LIST_FIRST(&sched->compute);
            if (lt == NULL) {
                pthread_mutex_unlock(&sched->compute_mutex);
                break;
            }
            LIST_REMOVE(lt, compute_sched_next);
            pthread_mutex_unlock(&sched->compute_mutex);
            sched->sleeping_state--;
            _lthread_resume(lt);
        }
        /*向poller注册感兴趣的事件,这样当有事件发生的时候,就会得到通知*/
        register_rd_interest(sched->compute_pipes[0]);
        _lthread_poll();/*等待事件发生或者timeout*/

        /* 5. fire up lthreads that are ready to run */
        while (sched->total_new_events) {/*如果有网络事件发生就一一找到对应的lthread执行*/
            p = --sched->total_new_events;

            /* We got signaled via pipe to wakeup from polling & rusume compute.
             * Those lthreads will get handled in step 3.
             */
            fd = get_fd(&sched->eventlist[p]);
            if (fd == sched->compute_pipes[0]) {
                ret = read(fd, &tmp, sizeof(tmp));
                continue;
            }

            lt = (lthread_t *)get_data(&sched->eventlist[p]);
            if (lt == NULL)
                assert(0);

            if (is_eof(&sched->eventlist[p]))
                lt->state |= bit(LT_FDEOF);

            _desched_lthread(lt);
            _lthread_resume(lt);
        }
    }

    _sched_free(sched);

    return;
}

以上部分中,cpustate在执行过程中起到了关键的作用,当任务调度器发现有lthread需要执行的时候将会将CPU切换到需要执行Lthread的cpustate上,这个lthread执行完毕以后又会将CPU切换到调度器的cpustate上。于是调度器就继续执行剩下的事情,找寻下一个需要执行的lthread或者等待事件发生。lthread 运行示意图当没有任何需要执行的lthread存在以后调度器的运行代码就会退出(实际上是lthreadrun这个函数退出)。

Categories: programming Tags: ,

lthread代码分析(一)

March 11th, 2012 No comments

  我们的服务器程序基本上都是基于多线程的。理由很简单,需要同时处理很多请求。但是随之而来的问题也不少,有的是设计上的失误。,有的是代码上的欠缺。例如:有很多的RPC调用都是同步调用,这个就意味着调用者的当前的线程就会被一直阻塞到调用返回或者超时。一旦被调用端出现了什么问题,调用者的当前线程就不能去做其他的事情。一个最简单的解决办法就是增加线程数量,但是这并不是一个非常完美的解决方案。线程越多,系统在线程切换上面所消耗的时间也越多,而且一个系统上的线程数量也不能无休止的往上增长。

  有一天,我发现有一个python的package叫做gevent,就是在python利用coroutine实现网络框架,在网络方面主要应用了libevent(epoll),在coroutine方面使用了greenlet

  于是乎我自然想到了是否可以在c/c++上面应用这个特性呢?epoll是的有的,缺的就是一个coroutine类似的轻量级线程了。后来就在github上面发现了lthread这个东西,下面记录一下我看这个库代码的一些过程和所想所得。

int _switch(struct _cpu_state *new_state, struct _cpu_state *cur_state);
__asm__ (
"    .text                                  n"
"    .p2align 2,,3                          n"
".globl _switch                             n"
"_switch:                                   n"
"__switch:                                  n"
"movl 8(%esp), %edx      # fs->%edx         n"
"movl %esp, 0(%edx)      # save esp         n"
"movl %ebp, 4(%edx)      # save ebp         n"
"movl (%esp), %eax       # save eip         n"
"movl %eax, 8(%edx)                         n"
"movl %ebx, 12(%edx)     # save ebx,esi,edi n"
"movl %esi, 16(%edx)                        n"
"movl %edi, 20(%edx)                        n"
"movl 4(%esp), %edx      # ts->%edx         n"
"movl 20(%edx), %edi     # restore ebx,esi,edi      n"
"movl 16(%edx), %esi                                n"
"movl 12(%edx), %ebx                                n"
"movl 0(%edx), %esp      # restore esp              n"
"movl 4(%edx), %ebp      # restore ebp              n"
"movl 8(%edx), %eax      # restore eip              n"
"movl %eax, (%esp)                                  n"
"ret                                                n"
);

以上是这部分呢,就是lthread里面切换不同轻量级线程的代码了,代码本身就很好的说明了这个函数是干什么的。倒是第一次看到注释时候被这个fs->%edx  ts->%edx给弄晕了。其实前面的8 个movl就是把当前的寄存器的值保存到curstate指向的数据够中,后面的8个movl从newstate中恢复执行环境.  这个函数执行完毕以后,CPU就会执行new_state里面所指定的环境的内容。其中,执行环境主要包括 esp ebp eip(这个不能直接赋值) 。

这里罗列一下lthread里面我认为比较重要的两个数据结构_lthread和_sched

 

struct _lthread {
    struct              _cpu_state st;/*这个是当前的_lthread被切换以后保留下来的cpu执行环境,下次执行的时候需要*/
    void                (*fun)(lthread_t *lt, void *);/*_lthread的执行函数*/
    void                *arg;
    void                *data;
    size_t              stack_size;/*这个是用于lthread切换的时候指明栈空间使用的大小*/
    lt_state_t          state;
    sched_t             *sched;
    compute_sched_t     *compute_sched;
    uint64_t            timeout;
    uint64_t            ticks;
    uint64_t            birth;
    uint64_t            id;
    int                 fd_wait; /* fd we are waiting on */
    char                funcname[64];
    lthread_t           *lt_join;
    void                **lt_exit_ptr;
    LIST_ENTRY(_lthread)    new_next;
    LIST_ENTRY(_lthread)    sleep_next;
    LIST_ENTRY(_lthread)    compute_next;
    LIST_ENTRY(_lthread)    compute_sched_next;
    sched_node_t        *sched_node;
    lthread_l_t         *sleep_list;
    void                *stack;
    void                *ebp;
};
struct _sched {
    size_t              stack_size;/*调度器对应的栈大小,lthread运行的时候实际使用的是调度器所拥有的栈空间*/
    int                 total_lthreads;
    int                 waiting_state;
    int                 sleeping_state;
    int                 poller;
    int                 nevents;
    uint64_t            default_timeout;
    int                 total_new_events;
    /* lists to save an lthread depending on its state */
    lthread_l_t         new;
    lthread_l_t         compute;
    struct rb_root      sleeping;
    uint64_t            birth;
    void                *stack;
    lthread_t           *current_lthread;
    struct _cpu_state   st;/*调度器被切换以后的cpu执行环境,以便恢复以后继续进行调度工作*/
    struct epoll_event  eventlist[LT_MAX_EVENTS];
    int                 compute_pipes[2];
    pthread_mutex_t     compute_mutex;
};

[to be continue …]

Categories: programming Tags: ,

ubuntu/mint 中修改显示器的分辨率

January 14th, 2012 Comments off

我在用virtual box新安装了一个linux mint 12, 但是最近在使用的时候发现一个问题。当我用Right_Ctrl + F键全屏进入mint的时候,发现整个屏幕明显没有被占满。上下留出一大段的黑色。看起来实在不爽。于是看了一下当前的屏幕分辨率,居然是1440X793. 这个太没有道理了,我的显示器是分辨率是1440×900的,不知道从哪个地方跑出这么一个乱七八糟的分辨率来。

我开始认为应该是virtual box中设置错误。但是我怎么都没有找到在什么地方可以设置这个分辨率。于是我打算在mint里面修改以下这个分辨率。但是12(也就是ubuntu 11.10)里面已经找不到/etc/X11/xorg.conf这个文件了。在google之后我找到了方法。

首先使用xrandr命令列出当前的模式

RoyM1 ~ # xrandr
Screen 0: minimum 64 x 64, current 1440 x 900, maximum 32000 x 32000
VBOX0 connected 1440x793+0+0 0mm x 0mm
   1440x793       60.0 +
   1024x768       60.0
   800x600        60.0
   640x480        60.0

发现里面没有1440×900这个模式。于是使用xrandr添加一个新的模式进去。在添加之前,首使用cvt列出所需的模式的完整配置。然后添加这个新的模式并且应用。如下

RoyM1 ~ # cvt 1440 900
# 1440x900 59.89 Hz (CVT 1.30MA) hsync: 55.93 kHz; pclk: 106.50 MHz
Modeline "1440x900_60.00"  106.50  1440 1528 1672 1904  900 903 909 934 -hsync +vsync
RoyM1 ~ # xrandr --newmode "1440x900_60.00"  106.50  1440 1528 1672 1904  900 903 909 934 -hsync +vsync
RoyM1 ~ # xrandr --addmode VBOX0 "1440x900_60.00" 
RoyM1 ~ # xrandr --output VBOX0 --mode "1440x900_60.00"
RoyM1 ~ #

这样以来,整个屏幕空间都被利用上了。感觉明显好了许多

Categories: mutter Tags: