Linux Kernel

网络端口预留

最近这些天我在做内核的一个新功能:/proc/sys/net/ipv4/ip_local_reserved_ports,这个文件的作用是告诉内核保留一些指定的端口,这样以来对于那些使用固定端口号的第三方应用程序来说就能保证它们总是能成功使用这些端口,而不是被内核自动分配端口时抢占。

我们知道在调用比如 bind(2) 时指定端口0其实是告诉内核自动去分配一个可用的端口,这个端口是随机的,而且范围是在 /proc/sys/net/ipv4/ip_local_port_range 指定的之内。不光 bind(2),你调用 connect(2) 时也会自动获得一个端口,它也是这样得到的。所以,如果我们都使用端口0去让内核分配端口世界会很和谐,各个程序相安无事,但是很多服务器程序需要有个固定的端口的,随机分配的端口是不能接受的,于是就有了那些已知的固定端口号。问题还没解决,如果我们的服务器程序的端口号不在里面怎么办?去 IANA 申请吧,不至于,可能你的程序不够知名,或者 IANA 没有批准等等申请不上,怎么办?

一个方法是使用那个 ip_local_port_range ,把范围调整到不包含你的端口的范围。这样做有个明显的缺点,如果你的端口号正好在当前 ip_local_port_range 的正中间,那样会有一半的端口都要被排除掉了,而且很明显 ip_local_port_range 的本意也不让你做这种事的。于是就有了 ip_local_reserved_ports,引入它的目的就是为了让你在这种情况下预留端口。

我写的最初的补丁只支持像 ip_local_port_range 那样的输入格式,比如:”50000 50100”,但是 Octavian 觉得这种格式灵活性不高,如果能支持指定多个任意范围的端口号那就更好了。于是他接过我的补丁继续做,从我的 v2 一直到现在的 v6,期间经过了多次讨论和测试,现在基本上已经成熟了。如果不出意外,应该可以汇入到主线内核中。新的 ip_local_reserved_ports 可以支持“50000,50100-50200” 这种格式,可以非常方便地指定要预留的单个端口或者端口范围。

注意,ip_local_reserved_ports 和 ip_local_port_range 关系不大,前者里的端口完全可以在后者的范围之外,这么设计一是为了简单,二是为了防止竞争,你可以先把 ip_local_port_range 调整到你预留的端口范围之外,然后等写好预留端口的列表之后再把前面的范围调回去。比如我们可以这么写代码:
[bash]
from=cut -f1 /proc/sys/net/ipv4/ip_local_port_range
to=cut -f2 /proc/sys/net/ipv4/ip_local_port_range
if [ “$1” -lt $to ] && [ “$1” -gt $from ]; then
echo “$from $[$1-1]” > /proc/sys/net/ipv4/ip_local_port_range
fi
original=cat /proc/sys/net/ipv4/ip_local_reserved_ports
original=”$original, $1”
echo “$original” > /proc/sys/net/ipv4/ip_local_reserved_ports
echo “$from $to” > /proc/sys/net/ipv4/ip_local_port_range
[/bash]

archlinux 编译安装内核

一离开 Red Hat 的开发环境很多东西都不顺手,编译安装内核就是一个例子。在 Fedora/RHEL 上,直接 make install 就什么都装好了,到了 archlinux 下可就不行了,它上面没有 /sbin/installkernel 不说,制作 initrd 工具也不一样,内核 config 放置不一样,就连内核版本号命名方式也不一样。所以就得自己写脚本来搞定了。

首先,你要把 CONFIG_LOCALVERSION 和 CONFIG_LOCALVERSION_AUTO 关了,前者是加自己的版本标识,后者是使用 git 来决定当前编译内核的版本号,所以说你会得到类似 2.6.33-rc7-ARCH-00010-g6339204-dirty 的东东,很烦人,直接关掉。

然后就是内核 config 文件的放置,Fedora 上都是单独放到 /boot 下,而 archlinux 是编译进内核的,也就是说在 /proc/config.gz 里,是通过 CONFIG_IKCONFIG_PROC 来控制的。我觉得放置在哪没多大关系,咱又不差内存。;)

好了,下面就是脚本了,它可以帮你完成安装内核这个最后的步骤,有点像 installkernel。但你必须在内核源代码根目录下运行,而且假设你执行完了 make modules_install 这一步,当然了,假设你使用的是 grub。

有兴趣的同学可以把它改成 installkernel 一样的接口,这样就可以直接 make install 了。

[bash]

!/bin/bash

#

1. This must be run in the top kernel source directory.

2. Assume you already install the modules.

version=make kernelversion
cp System.map /boot/System.map.${version}

Maybe only works on x86

cp arch/uname -m/boot/bzImage /boot/vmlinuz-${version}
mkinitcpio -k “${version}” -c /etc/mkinitcpio.conf -g /boot/kernel.${version}.img

kernel_args=cat /proc/cmdline
root_device=awk '/^root/ { print $2; exit; }' /boot/grub/menu.lst
if ! grep -q “Test Kernel ${version}” /boot/grub/menu.lst
then
cat >> /boot/grub/menu.lst <<eof

title Test Kernel ${version}
root ${root_device}
kernel /boot/vmlinuz-${version} ${kernel_args}
initrd /boot/kernel.${version}.img
EOF
fi
[/bash]

kbuild 更换维护者

Sam Ravnborg 作为 Linux 内核 kbuild 子系统的维护者已经很长时间。因为提交过相关补丁的原因,和他有过电子邮件来往。如果我没记错的话,他应该是意大利人,这位老大英语不是一般差,他的英语我每次读都比较费劲。无论如何,他对内核社区的贡献是有目共睹的,感谢多年来他的努力,现在 kbuild 系统已经很完善了。

昨天晚上的一封电子邮件中,Sam 突然宣布放弃继续维护 kbuild,因为家庭和工作的原因,时间不够用了。他维护 kbuild 也都是在业余时间来做,确实比较辛苦。 kbuild 作为内核中比较特殊的一部分,它几乎纯粹是用户层的东西,但和最后生成的内核二进制文件又息息相关,总体上感觉有点儿“鸡肋”,而且一般人是不会乱碰那些很多人看不懂的 Makefile 的。而且编译不光涉及简单的直接编译,还涉及到各个平台之间的交叉编译,涉及二进制文件格式等等,维护这些东西确实很麻烦。不管谁接手这个活儿都不容易。

目前谁将继续维护 kbuild 还未确定,主要还是要看 lkml 社区的认可程度,毕竟开源社区就是靠声望运作的。在私下的讨论中,Debian 开发者 Aníbal Monsalve Salazar 表示对此感兴趣,希望能接手,但不知为何并没有抄送到 lkml。最后如何我们只能拭目以待了。

BTW:正是靠这么多业余爱好者的努力,Linux 才成长如此迅速。感谢那些默默无闻,常年奋战在自由软件开发第一线的 geek 们!他们是互联网上最可爱的人!geek 万岁!8-) 姑娘们,给你们十个理由去嫁给 geek 男!恩哼~!

夭折的一个补丁

这个补丁其实本来是我分配到的一个任务,不过在我们内部讨论时被拒了,所以放出来也无妨。再说了,在我们内部提交补丁的第一原则是必须首先在LKML上得到认可,没什么秘密。:-)

这个任务其实很简单,就是要把socket的一些选项给通过/proc导出,这些选项就是通过setsockopt(2)设置的那些。

初看这个任务感觉很简单,无怪乎就是一个/proc下的interface,然后就是通过某种方式把数据传输过来。但仔细一看其实不简单,因为setsockopt(2)并没有把这些数据集中存储起来,而是分散地存放到各个角落,然后通过switch/case的方式逐个判断。所以这里问题就来了,在某种程度上,其实我们要的就是一种“迭代”,而无奈的是,这些数据太过离散而不能迭代!这就是难点所在!

怎么解决呢?我首先想到的是用hash,把这些数据通过链表连起来,用其level和optname作关键字。后来想想其实这样也很麻烦,而且越往下想越麻烦。。。这时我就开始反思是不是该往简单里想想了?我盯住那switch/case,发现它既然能阻止我“迭代”,那我也能用它来“迭代”!对,我的主意就是用fall through!于是下面这个技巧就出来乐~~!

[c]

define BREAK(OPT, val)

   if (optname != SO_ALL)  
           break;          
   len += scnprintf(buf+len, *(int *)optlen - len, "%dt%dt%ldt%dn", 
           level, OPT, (long)(val), (int)sizeof(val))

define STOP() if (optname == SO_ALL) {

                   *(int *)optlen = len; 
                   return 0;       
           }

[/c]

其余的就很简单了,尤其是/proc的函数接口,感觉那块儿代码算是很整洁的,相当值得一读。

当然了,正如开头所说,现实是很残酷的,这个补丁被拒绝了,因为大牛David Miller不喜欢,他说这个是Solaris上有的东西,这并不代表Linux就一定要有,如果真的需要这个功能,我们还有systemtap,而且程序本身有义务自己打印socket的设置选项,如果调试的话。

如果你要有兴趣的话可以试试我这个补丁,但真要用的话还得自己完善一下,尤其是输出格式。:-)

__umoddi3()的问题

在编译内核时有人遇到下面这个问题:

kernel/built-in.o: In function `getnstimeofday':
(.text+0xb6ae): undefined reference to `__umoddi3'
kernel/built-in.o: In function `getnstimeofday':
(.text+0xb6ce): undefined reference to `__udivdi3'

这个问题可以在用户空间重现,不过不是很容易,我实验了一下,在i386上,并不是所有的64位整数操作都会被转化成调用__umoddi3,gcc bugzilla上有演示程序,如下:

[c]

define NSEC_PER_SEC 1000000000UL

int rmg(void);

int main(void)
{
/ int sec; /
return rmg();
}

int rmg(void)
{
static unsigned long long nsec = 0;
static int sec = 0;
while (sec = NSEC_PER_SEC, 0)) {
nsec -= NSEC_PER_SEC;
++sec;
}
}
return sec;
}
[/c]

这样编译它:% gcc -nostdlib -O2 -o umoddi3 umoddi3.c,就会得到:

/tmp/ccycM684.o: In function `rmg':
umoddi3.c:(.text+0x87): undefined reference to `__udivdi3'
collect2: ld returned 1 exit status

问题重现了。这里的问题是,对于nsec来说,内层的循环其实等价于求模运算,gcc在优化时发现了这一点,而且硬件本身也不支持对64位整数直接进行算术运算,所以gcc会把这一步优化成调用内部函数udivdi3()和umoddi3(),这两个函数在libgcc中(见gcc源代码gcc/libgcc2.c),libgcc默认和libc一样是要被加载的,但如果我们加了-nostdlib(Linux内核是更好的例子),这个问题就会出现了。

知道原因了,怎么解决?网上有两种方法,一种是像这个补丁那样,在循环中插入下面这条内联汇编:


asm(“” : “+r”(ns));

这句是告诉gcc把ns这个变量放到寄存器中,并且既有读操作也有写操作,所以后面再用它时必须重新读取,这样就消除了上面的优化。

另一种解决方法是添加新的编译选项:-fno-tree-scev-cprop,这个选项似乎没有文档,至少我没找到。说说它的大体意思。scev应该是SCalar EVolutions,什么意思不知道。:( cprop应该是Copy PROPagation,这个应该很容易理解,就是赋值的传播,比如:

i = 10;
a = i;
b = i;

其实就是:

a = 10;
b = 10;

可见,编译优化是门大学问,写个编译器丝毫不比写个内核容易。:-P

switch_to中的汇编

switch_to()的代码在arch/x86/include/asm/system.h中,如下:
[c]

define switch_to(prev, next, last)

do {
/*

 * Context-switching clobbers all registers, so we clobber    
 * them explicitly, via unused output variables.        
 * (EAX and EBP is not listed because EBP is saved/restored    
 * explicitly for wchan access and EAX is the return value of    
 * __switch_to())                        
 */                                
unsigned long ebx, ecx, edx, esi, edi;                

asm volatile("pushflnt"        /* save    flags */    
         "pushl %%ebpnt"        /* save    EBP   */    
         "movl %%esp,%[prev_sp]nt"    /* save    ESP   */ 
         "movl %[next_sp],%%espnt"    /* restore ESP   */ 
         "movl $1f,%[prev_ip]nt"    /* save    EIP   */    
         "pushl %[next_ip]nt"    /* restore EIP   */    
         "jmp __switch_ton"    /* regparm call  */    
         "1:t"                        
         "popl %%ebpnt"        /* restore EBP   */    
         "popfln"            /* restore flags */    

         /* output parameters */                
         : [prev_sp] "=m" (prev-&gt;thread.sp),        
           [prev_ip] "=m" (prev-&gt;thread.ip),        
           "=a" (last),                    

           /* clobbered output registers: */        
           "=b" (ebx), "=c" (ecx), "=d" (edx),        
           "=S" (esi), "=D" (edi)                

           /* input parameters: */                
         : [next_sp]  "m" (next-&gt;thread.sp),        
           [next_ip]  "m" (next-&gt;thread.ip),        

           /* regparm parameters for __switch_to(): */    
           [prev]     "a" (prev),                
           [next]     "d" (next)                

         : /* reloaded segment registers */            
        "memory");                    

} while (0)
[/c]

根据ABI约定和内联汇编,ebx, ecx, edx, esi, edi这几个寄存器是由编译器自动保存和恢复的。这一点可能不太好理解,举个例子,看下面的代码中的ecx:
[c]

include

void modify_ecx(void) {
unsigned long ecx;

asm (
“movl $1, %%ecxnt”
:”=c”(ecx)
:
);
}

void test(void) {
unsigned long ecx;

asm volatile(
“nopnt”
“call modify_ecxnt”
: “=c” (ecx)
:
: “memory”
);
printf(“ecx=%ldn”, ecx);
}

int main(void) {
test();
return 0;
}
[/c]
这里的test()就相当于内核源代码中“调用”switch_to()的context_switch(),我们来查看其对应的汇编代码(注意要加-O0):


main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ecx
subl $8, %esp
call test
movl $0, %eax
addl $8, %esp
popl %ecx
leal -4(%ecx), %esp
ret

可见,在调用test()之前,编译器已经自动完成了保存ecx的操作,而调用之后它又会恢复ecx的值。

而ebp,eflags是手工压入堆栈,并在switch回来后恢复出来的。esp和eip保存在相应的task_struct结构体里。

需要额外说明的是那个jmp,因为这里的参数传递是通过寄存器完成的,具体说是用了eax和edx这个两个寄存器,所以再jmp其实就和call一样了,不过call是要把ebp入栈的,而jmp不需要,这里也不需要。

另外一个可能的问题是:为什么switch_to()有三个参数?我们切换的进程明明是两个啊!这里问题的所在是进程切换时堆栈的切换,如果不使用三个参数,切换的堆栈中仍然保存的是切换前的参数,再切换回来时prev很可能不对了,所以需要一个参数来修正,这个参数又正好是__switch_to的返回值。这样问题就解决了。

尾部调用优化

gcc实在是太聪明了,有时候就会聪明过头,尾部调用优化就是一个很好的例子。:)

尾部调用其实很好理解,就是在函数的最后调用另外一个函数,一种大的可能就是这个函数和调用函数参数差别很小,那么这时候,gcc会对这个尾部调用进行优化,如果可能,完全可以把调用那个函数时的入栈操作给直接优化掉。这就是尾部调用的优化。我们可以看看下面的例子:

[c]
extern int callee(int a, int b);

static int c;

int call(int a, int b)
{
int ret = callee(c, b);
return ret;
}
[/c]

用”gcc -fomit-frame-pointer -fno-inline -O2 -S tail_call.c”编译它,得到的汇编代码截取如下:

call:
        movl    c, %eax
        movl    %eax, 4(%esp)
        jmp     callee

很明显,入栈操作被优化掉了,成了直接操作call()的参数!这会有问题,如果call()的调用函数不想看到堆栈被改变的话。

在Linux内核中有个很好的例子,那就是系统调用!系统调用的入口(arch/x86/kernel/entry_32.S)是用汇编写成的,在进入一个系统调用前存放参数的寄存器会被入栈,系统调用完毕后又会被恢复,所以,如果系统调用也被做了尾部调用优化的话,那么系统调用前后寄存器的值就会发生变化!这就可能会破坏用户空间的代码!

Linux内核采取了相应的办法来解决这个问题,那就是宏asmlinkage_protect(),它的定义在arch/x86/include/asm/linkage.h。我们通过个具体的例子来看,比如open(2),其定义是这样的:

[c]
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)
{
long ret;

    if (force_o_largefile())
            flags |= O_LARGEFILE;

    ret = do_sys_open(AT_FDCWD, filename, flags, mode);
    /* avoid REGPARM breakage on x86: */
    asmlinkage_protect(3, ret, filename, flags, mode);
    return ret;

}
[/c]

上面的asmlinkage_protect会被展开成:


asm volatile (“” : “=r” (ret) : “0” (ret), “g” (filename),
“g” (flags), “g” (mode));

这句不会直接生成任何汇编代码,但是它会强迫gcc把参数放到寄存器中,也就避免了上面展示的直接对栈进行操作。

为了完整,我们把最开始的那段代码修复一下:
[c]
extern int callee(int a, int b);

static int c;

int call(int a, int b)
{
int ret = callee(c, b);
asm(“”:”=r”(ret):”0”(ret), “g”(a), “g”(b));
return ret;
}
[/c]
再汇编就可以看到堆栈操作了:


call:
pushl %ebx
subl $8, %esp
movl c, %eax
movl 20(%esp), %ebx
movl %eax, (%esp)
movl %ebx, 4(%esp)
call callee
addl $8, %esp
popl %ebx
ret

这里展示的代码都很简单,实际中往往很复杂,情况不同gcc的优化操作也不同,比如,当上面的callee()是static的时候虽然也会被优化但不会直接操作堆栈(而是通过使用寄存器),因为gcc可以完整地看到两个函数,可以在“全局”进行优化。实际问题实际分析,但这个尾部调用优化带来的问题我们必须得小心才是。

Linux分区个数限制

在linux上,对于IDE硬盘来说,分区个数限制在63个;而对于SCSI硬盘限制在15个。

我们可以直接在linux内核源代码中找到证据:

include/linux/ide.h

define PARTN_BITS 6 / number of minor dev bits for partitions /

次设备号也就决定了可挂载的IDE设备不会超过63个(2^6-1,之所以减一是因为hdX本身也会占用一个)。

SCSI也差不多,见 drivers/scsi/sd.c

if !defined(CONFIG_DEBUG_BLOCK_EXT_DEVT)

define SD_MINORS 16

else

define SD_MINORS 0

endif

在内核文档中也可以看到,见Documentation/devices.txt :

For partitions, add to the whole disk device number:
0 = /dev/hd? Whole disk
1 = /dev/hd?1 First partition
2 = /dev/hd?2 Second partition

63 = /dev/hd?63 63rd partition

8 block SCSI disk devices (0-15)

Partitions are handled in the same way as for IDE
disks (see major number 3) except that the limit on
partitions is 15.

Linux PID namespace

一直对linux内核的namespace感到困惑,今天看了一下代码才知道,原来所谓的namespace其实就是给虚拟化用的,PID namespace其实就是建立一个新的PID空间,这样内部可以使用一套新的PID,而且不会和外部冲突。这也就是说某个进程其实会有两个PID,一个空间一个。

我写了段C代码来展示这个问题。
[c]

include

include

include

include

include

include

static int fork_child(void *arg)
{
int a = (int)arg;
int i;
pid_t pid;

printf("In the container, my pid is: %dn", getpid());
for (i = 0; i &lt; a; i++) {
    pid = fork();
    if (pid &lt; 0)
        return pid;
    else if (pid)
        printf(&quot;pid of my child is %dn&quot;, pid);
    else if (pid == 0) {
        sleep(3);
        exit(0);
    }
}
return 0;

}

int main(int argc, char argv[])
{
int cpid;
void
childstack, stack;
int flags;
int ret = 0;
int stacksize = getpagesize()
4;

if (argc != 2) {
    fprintf(stderr, &quot;Wrong usage.n&quot;);
    return -1;
}

stack = malloc(stacksize);
if (!stack) {
    perror(&quot;malloc&quot;);
    return -1;
}

printf(&quot;Out of the container, my pid is: %dn&quot;, getpid());

childstack = stack + stacksize;
flags = CLONE_NEWPID | CLONE_NEWNS;

cpid = clone(fork_child, childstack, flags, (void *)atoi(argv[1]));
printf(&quot;cpid: %dn&quot;, cpid);

if (cpid &lt; 0) {
    perror(&quot;clone&quot;);
    ret = -1;
    goto out;
}

fprintf(stderr, &quot;Parent sleeping 20 secondsn&quot;);
sleep(20);
ret = 0;

out:
free(stack);
return ret;
}
[/c]

运行结果:

$ sudo ./pid_container 3
Out of the container, my pid is: 7061
cpid: 7062
In the container, my pid is: 1
Parent sleeping 20 seconds
pid of my child is 2
pid of my child is 3
pid of my child is 4

其实被namespace化的不只是PID,还有很多东西,貌似它们合起来被称为container。可以看 include/linux/nsproxy.h:

struct nsproxy {
atomic_t count;
struct uts_namespace uts_ns;
struct ipc_namespace
ipc_ns;
struct mnt_namespace mnt_ns;
struct pid_namespace
pid_ns;
struct net *net_ns;
};

虚拟化的东西是越来越让人摸不清头脑了。。。。