Socket与系统调用深度分析(基于5.0.1/32,其实系统调用并不是int80,而是VDSO,另一种快速的系统调用方式

Socket与系统调用深度分析

em ,理论是INT80,但实际调试最后才发现并不是的,所以我的过程中有很多的疑问,到最后我才发现。因此本博文思路并不一定很清晰,但如果你想知道5.0.1的系统调用方式到底是什么,请你继续观看

系统linux-5.0.1 32位
为加快大家查看源码的调用关系 提供 https://elixir.bootlin.com/linux/v5.0.1/source/net/ipv4/tcp_ipv4.c#L202
以下调试都是基于下图的理解进行的,针对图中1,2两个点,博文主要解决四个问题

1 int 0x80中断向量是如何与中断向量表绑定的?
2 socketAPI是如何进入内核调用socket 接口的?
3 socket接口是如何与传输层协议绑定的?
4 sokcet接口是如何调用具体协议的接口的?

Socket与系统调用深度分析(基于5.0.1/32,其实系统调用并不是int80,而是VDSO,另一种快速的系统调用方式_第1张图片

Linux 引导过程综述

BIOS->Bootloader->内核初始化:体系结构相关部分-><内核初始化:体系结构无关部分>

内核初始化:体系结构相关部分

1 内核映像结构
2 初始化与保护模式 
3 自解压内核 
<4 startup_32(head_32.c)>

startup_32(head_32.c)

1 初始化参数(设置段的值,清楚BSS,初始化栈)
2 开启分页机制
3 初始化 Eflags
4 检查处理器类型
5 载入 GDT、IDT
<6 i386_start_kernel>

i386_start_kernel 执行与体系结构无关部分的内核初始化

1 检查中断向量表(IDT)是否已经启动,em,IDT要被初始化第一次
<2 调用start_kernel执行与体系结构无关部分的内核初始化>

start_kernel

使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init())IDT初始化第二次

以上过程只是简单分析从linux启动到终端向量表的初始化的过程,下面我们来看IDT第二次初始化的细节。

中断向量int0x80是如何与中断处理例程绑定?

中断向量表的初始化分有三次:
(1)setup_once: early_idt_handler_array //./arch/x86/kernel/head_32.S line 377
(2)对0~19号的一些和0x80号系统保留中断向量的初始化,在trap_init中完成
(3)对其它中断向量的初始化,在init_IRQ中完成

第二次初始化中断向量表,绑定0x80和SYS_INT80_32

先给出答案:startup_32_smp-->i386_start_kernel -->start_kernel --> trap_init --> idt_setup_traps-->idt_setup_from_table
 gdb bt 调试结果如下:
 #0   (t=0xc1d99b10 , size=, sys=true,
     idt=) at arch/x86/kernel/idt.c:225
 #1  0xc1d291d9 in  () at arch/x86/kernel/idt.c:267
 #2  0xc1d29155 in  at arch/x86/kernel/traps.c:934
 #3  0xc1d23a5b in  () at init/main.c:595
 #4  0xc1d2327c in  () at arch/x86/kernel/head32.c:56
 #5  0xc10001ec in  () at arch/x86/kernel/head_32.S:363
 #6  0x00000000 in ?? ()
 

下面我们来具体分析一下。

idt_setup_traps

void __init idt_setup_traps(void)
{
    idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}

先不管idt_setup_from_table的作用,先来看看结构体数组def_idts的最后一行SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),SYSG是一个宏,代表系统中断门,作用就是将中断向量IA32_SYSCALL_VECTOR和中断处理例程entry_INT80_32绑定,相信你现在已经明白idt_setup_from_table函数的作用了,就是在填一个table,包括中断向量号及其处理程序。

#define IA32_SYSCALL_VECTOR     0x80 // ./arch/x86/include/asm/irq_vectors.h line 45

def_idts

static const __initconst struct idt_data def_idts[] = {
    ...
    SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_32)  /*int 0x80*/
};

idt_setup_from_table

idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
{
    gate_desc desc;
    for (; size > 0; t++, size--) {
        idt_init_desc(&desc, t);//初始化描述符对应的门:门类型有任务门、中断门、陷阱门和调用门
        write_idt_entry(idt, t->vector, &desc);//将对应的门和中断向量写入中断描述符表(IDT)
        if (sys)
            set_bit(t->vector, system_vectors);//系统设置了一个位图system_vectors,来表示每个中断向量表的使用情况,可以看到,这里是将size个向量表项对应的位图设置为1,表示已经被占用了。
    }
}
//1 中断向量表的每个表项叫做一个门描述符(gate descriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。

关于门的描述参考:

https://www.cnblogs.com/qintangtao/p/3325985.html

https://blog.csdn.net/cwcmcw/article/details/21640363

如果你想看write_idt_entry,如下,就是一个简单的内存拷贝。

static inline void native_write_ldt_entry(struct desc_struct *ldt, int entry, const void *desc)
{
    memcpy(&ldt[entry], desc, 8);
}

到此我们已经理清楚了中断向量int80绑定了entry_INT80_32,下面我们看第二个问题。

系统调用如何关联系统调用号,系统调用号如何绑定socket接口。

直接查看:linux-5.0.1/arch/x86/entry/syscalls/syscall_32.tbl,以下列出socket相关的系统调用接口,以及对应的系统调用号和内核的socket接口。

102 i386    socketcall      sys_socketcall          __ia32_compat_sys_socketcall

359 i386    socket          sys_socket          __ia32_sys_socket
360 i386    socketpair      sys_socketpair          __ia32_sys_socketpair
361 i386    bind            sys_bind            __ia32_sys_bind
362 i386    connect         sys_connect         __ia32_sys_connect
363 i386    listen          sys_listen          __ia32_sys_listen
364 i386    accept4         sys_accept4         __ia32_sys_accept4
365 i386    getsockopt      sys_getsockopt          __ia32_compat_sys_getsockopt
366 i386    setsockopt      sys_setsockopt          __ia32_compat_sys_setsockopt
367 i386    getsockname     sys_getsockname         __ia32_sys_getsockname
368 i386    getpeername     sys_getpeername         __ia32_sys_getpeername
369 i386    sendto          sys_sendto          __ia32_sys_sendto
370 i386    sendmsg         sys_sendmsg         __ia32_compat_sys_sendmsg
371 i386    recvfrom        sys_recvfrom            __ia32_compat_sys_recvfrom
372 i386    recvmsg         sys_recvmsg         __ia32_compat_sys_recvmsg

两种调用方式,一种是以系统调用号102 socketcall 进入sys_socketcall,然后分支进入sys_socket,sys_bind 等,另一种是直接通过自身的系统调用号比如359(socket)找到sys_socket。

我们可以通过调试来查看到底系统是使用哪一种?

用户态调试

先说明一下:由于我的内核无法完成升级,搞了两次暂时没有成功,所以以内核 4.0.5版本的linux系统在用户态调试下调试,如果以后升级成功了再来验证5.0.1.

linux-4.0.5和linux-5.0.1关于socket接口调用的区别:

linux-4.0.5(64) linux-5.0.1(32)
int80绑定中断向量入口地址 syscall entry_INT80_32
socketcall对应的系统调用号 102
socketd对应的系统调用号 41 359

源代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    return 0;
}

静态编译

gcc  -g -o static  test.c  -static
gdb ./static

Socket与系统调用深度分析(基于5.0.1/32,其实系统调用并不是int80,而是VDSO,另一种快速的系统调用方式_第2张图片

用户态系统调用过程:main->socked->mov $0x29 $eax->syscall

mov $0x29 $eax  这个语句相当重要,0x29恰好是十进制的41,对用socket的系统调用编号,然后陷入内核执行系统调用。

第二点说明:如果是linux-5.0.1 我认为上面两条命令应该变为,保存的系统调用编号为102.
mov $0x66 $eax
entry_INT80_32

由于知识水平有限,在用户态我也无法跟踪下去找到int 0x80指令的执行,当然我有想到反汇编static文件,然后查看out.txtx文件的结果,是否有int 0x80指令的调用,但是我没找到!!!是到底是哪儿的问题,是我看不懂汇编吗?不可能,看不懂汇编,我还看不懂 int 0x80 ?????????????好吧,还是我太。。。。菜了。是的,最后我才知道什么原因,请继续看。

objdump -d static > out.txt

用户态看不到,我们去内核态下看吧,注意注意,我的内核态切回5.0.1了。

内核态调试,基于linux-5.0.1

1 在lab3 目录下启动menu终端:

qemu -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append  nokaslr -s

2 在另一个终端输入

gdb
file vmlinux
break __sys_socket
target remote:1234
c

3 在menu系统中输入replyhi,另一个终端自然进入了内核的断点。

如图 Socket与系统调用深度分析(基于5.0.1/32,其实系统调用并不是int80,而是VDSO,另一种快速的系统调用方式_第3张图片

为什么我输入了两次replyhi呢?因为我第一次的断点打在sys_socket处,并没有停下,所以5.0.1系统中的socket系统调用并不是按照第二种方式,而是按照sys_socketcall分发的方式,这儿的细节我已经在上一篇博客最后写过了,现在我们来分析一下:enter_sysenter_32是如何通过系统调用号来找到__sys_socketcall的。你有发现点什么么?为什么进入内核的不是entry_INT80_32 ?

enter_SYSENTER_32中断处理例程到底发生了什么

通过跟踪栈的调用情况,我们来看一看,进入内核之后如何找到__sys_socket的。

(gdb) bt
#0  <__sys_socket >(family=2, type=1, protocol=0) at net/socket.c:1327
#1  0xc1757b98 in __do_sys_socketcall (args=,
    call=) at net/socket.c:2555
#2  <__se_sys_socketcall> (call=1, args=-1077721504) at net/socket.c:2527
#3  0xc1002095 in  (regs=)
    at arch/x86/entry/common.c:334
#4   (regs=0xc7191fb4) at arch/x86/entry/common.c:397
#5  0xc199141b in  () at arch/x86/entry/entry_32.S:887
#6  0x00000001 in ?? ()
#7  0xbfc34660 in ?? ()

其中涉及:entry_SYSENTER_32 -> do_fast_syscall_32 -> do_syscall_32_irqs_on -> __do_sys_socketcall-> sys_socket,下面我们一个一个看

entry_SYSENTER_32

ENTRY(entry_SYSENTER_32) 截取重要部分
    movl    TSS_entry2task_stack(%esp), %esp //保存当进程内核栈
.Lsysenter_past_esp:  //保存当前的一些重要寄存器到结构体 pt_regs中
    pushl   $__USER_DS      /* pt_regs->ss */
    pushl   %ebp            /* pt_regs->sp (stashed in bp) */
    pushfl              /* pt_regs->flags (except IF = 0) */
    orl $X86_EFLAGS_IF, (%esp)  /* Fix IF */
    pushl   $__USER_CS      /* pt_regs->cs */
    pushl   $0          /* pt_regs->ip = 0 (placeholder) */
    pushl   %eax     //压栈保存eax!!!还记得我们在用户态下保存的系统调用号吗?5.0.1下应该是102
    SAVE_ALL pt_regs_ax=$-ENOSYS    /* 保存其他寄存器保在 pt_regs 结构中 */
    movl    %esp, %eax
    call    do_fast_syscall_32

在内核启动时,其中会有一个软中断的陷入门,当接收到一个系统调用的时候, 相应的文件就会被调用,然后通过 push 和 SAVE_ALL 将当前用户态的寄存器,保存在 pt_regs 结构中,而结构体pt_regs的内容将在do_syscall_32_irqs_on里面取值。

调试验证,如下图,此时的eax正好是102

Socket与系统调用深度分析(基于5.0.1/32,其实系统调用并不是int80,而是VDSO,另一种快速的系统调用方式_第4张图片

do_fast_syscall_32

__visible long do_fast_syscall_32(struct pt_regs *regs)
{
    /*
     * Called using the internal vDSO SYSENTER/SYSCALL32 calling
     * convention.  Adjust regs so it looks like we entered using int80.
     */

    unsigned long landing_pad = (unsigned long)current->mm->context.vdso +
        vdso_image_32.sym_int80_landing_pad;
    regs->ip = landing_pad;
    enter_from_user_mode();//进入用户态
    local_irq_enable();//
    if (
#ifdef CONFIG_X86_64
        __get_user(*(u32 *)®s->bp,
                (u32 __user __force *)(unsigned long)(u32)regs->sp)
#else
        get_user(*(u32 *)®s->bp,
             (u32 __user __force *)(unsigned long)(u32)regs->sp)
#endif
        ) {

        /* User code screwed up. */
        local_irq_disable();
        regs->ax = -EFAULT;
        prepare_exit_to_usermode(regs);//推出用户态
        return 0;

Called using the internal vDSO SYSENTER/SYSCALL32 calling convention. Adjust regs so it looks like we entered using int80.

这句话特别有意思,使其看起来像我们使用了INT80进入内核!!!!!!!!!事实证明它确实不是通过:entry_INT80_32->do_fast_INT80_32->do_syscall_32_irqs_on->__do_sys_socketcall-> sys_socket,有一种挂羊头卖狗肉的感觉,感觉被骗了

实际上linux为了减少系统调用的开销,采取了一种比通过0x80更快的方式,即通过vDSO SYSENTER/SYSCALL32,具体请参考: http://blog.chinaunix.net/uid-27717694-id-4233173.html ,对不起,前面我瞎bb一堆,人家根本就没有通过INT80进入内核。

do_syscall_32_irqs_on

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
    struct thread_info *ti = current_thread_info();
    unsigned int nr = (unsigned int)regs->orig_ax;  //取系统调用号102
    nr = array_index_nospec(nr, IA32_NR_syscalls);//检查nr是否越界
    regs->ax = ia32_sys_call_table[nr](regs);//取对调用号对应的函数地址和参数
    }

regs的定义

struct pt_regs {
//重点是:orig_ax
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long bp;
    unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long ax;
    unsigned long cx;
    unsigned long dx;
    unsigned long si;
    unsigned long di;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_ax;
/* Return frame for iretq */
    unsigned long ip;
    unsigned long cs;
    unsigned long flags;
    unsigned long sp;
    unsigned long ss;
/* top of stack page */
};

调试验证,hh了,在编译内核的时候应该禁止编译优化的

Socket与系统调用深度分析(基于5.0.1/32,其实系统调用并不是int80,而是VDSO,另一种快速的系统调用方式_第5张图片

虽然我看不到,但是从我们之前的程序来看,regs->orig_ax就是102,从而调用了sys_socketcall.

sys_socketcall

switch (call) {
   case SYS_SOCKET:
       err = __sys_socket(a0, a1, a[2]);
       break;
   case SYS_BIND:
       err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
       break;
   case SYS_CONNECT:
       err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
       break;
   case SYS_LISTEN:
       err = __sys_listen(a0, a1);
       break;
   case SYS_ACCEPT:
       err = __sys_accept4(a0, (struct sockaddr __user *)a1,
                   (int __user *)a[2], 0);
       break;
   case SYS_GETSOCKNAME:
       err =
           __sys_getsockname(a0, (struct sockaddr __user *)a1,
                     (int __user *)a[2]);
       break;
   case SYS_GETPEERNAME:
...

通过socket的总接口sys_socketcall进入socket

__sys_socket

int __sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    int flags;

    flags = type & ~SOCK_TYPE_MASK;
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;
    type &= SOCK_TYPE_MASK;
    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        return retval;
    return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    return __sys_socket(family, type, protocol);
}

到此我们的前面两个问题已经解决了。

socket接口是如何与传输层协议绑定的?

由于socket接口对应传输层的协议,包括: TCP,UDP,PING RAW等,那么一个socket接口是如何与这些协议绑定的呢?即linux内核是如何初始化的传输层协议的。

lab3目录下输入调式

qemu -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append  nokaslr -s -S

另一个终端

gdb
file vmlinux
break inet_init
target remote:1234
c
bt  //查看断点的堆栈情况

#0  inet_init () at net/ipv4/af_inet.c:1900
#1  0xc1000a2d in do_one_initcall (fn=0xc1d6cea6 )
    at init/main.c:887
#2  0xc1d23da2 in do_initcall_level (level=) at init/main.c:955
#3  do_initcalls () at init/main.c:963
#4  do_basic_setup () at init/main.c:981
#5  kernel_init_freeable () at init/main.c:1136
#6  0xc198af98 in kernel_init (unused=) at init/main.c:1054
#7  0xc1991386 in ret_from_fork () at arch/x86/entry/entry_32.S:722

如上TCP/IP协议的初始化是从Linux内核初始化过程中加载TCP/IP协议栈的,从start_kernel、kernel_init、do_initcalls、inet_init,中间过程过

inet_init

static int __init inet_init(void)
{
    struct inet_protosw *q;
    struct list_head *r;
    int rc = -EINVAL;
    sock_skb_cb_check_size(sizeof(struct inet_skb_parm));
    rc = proto_register(&tcp_prot, 1);  //注册tcp
    if (rc)
        goto out;
    rc = proto_register(&udp_prot, 1);//注册udp
    if (rc)
        goto out_unregister_tcp_proto;
    rc = proto_register(&raw_prot, 1); //注册raw
    if (rc)
        goto out_unregister_udp_proto;
    rc = proto_register(&ping_prot, 1);//注册ping
    if (rc)
        goto out_unregister_raw_proto;
    /*
     *  Tell SOCKET that we are alive...
     */
    (void)sock_register(&inet_family_ops);
    }

我们可能到注册不同的协议是通过结构体 struct inet_protosw来实现的,我们来看一下结构体的内容,大部分主要内容都是函数指针对应函数指针,即通过结构体包含协议类型协议的处理函数对应,将socket接口和不同的协议关联起来了,所以我们写应用程序socket时必须要指明协议族类型比如 tcp对应AF_INET。

struct proto tcp_prot = {
    .name           = "TCP",              //重要  类型
    .owner          = THIS_MODULE,
    .close          = tcp_close,          //函数close->tcp_close函数
    .pre_connect        = tcp_v4_pre_connect,
    .connect        = tcp_v4_connect,    //函数connect->tcp_v4_connect函数
    .disconnect     = tcp_disconnect,
    .accept         = inet_csk_accept,   //accept
    .ioctl          = tcp_ioctl,
    .init           = tcp_v4_init_sock,
    .destroy        = tcp_v4_destroy_sock,
    .shutdown       = tcp_shutdown,
    .setsockopt     = tcp_setsockopt,
    .getsockopt     = tcp_getsockopt,
    .keepalive      = tcp_set_keepalive,
    .recvmsg        = tcp_recvmsg,     //recvmsg
    .sendmsg        = tcp_sendmsg,     //sendmsg
    .sendpage       = tcp_sendpage,
    .backlog_rcv        = tcp_v4_do_rcv,
    .release_cb     = tcp_release_cb,
    .hash           = inet_hash,
    .unhash         = inet_unhash,
    .get_port       = inet_csk_get_port,
    .enter_memory_pressure  = tcp_enter_memory_pressure,
    .leave_memory_pressure  = tcp_leave_memory_pressure,
    .stream_memory_free = tcp_stream_memory_free,
    .sockets_allocated  = &tcp_sockets_allocated,
    .orphan_count       = &tcp_orphan_count,
    .memory_allocated   = &tcp_memory_allocated,
    .memory_pressure    = &tcp_memory_pressure,
    .sysctl_mem     = sysctl_tcp_mem,
    .sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_tcp_wmem),
    .sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_tcp_rmem),
    .max_header     = MAX_TCP_HEADER,
    .obj_size       = sizeof(struct tcp_sock),
    .slab_flags     = SLAB_TYPESAFE_BY_RCU,
    .twsk_prot      = &tcp_timewait_sock_ops,
    .rsk_prot       = &tcp_request_sock_ops,
    .h.hashinfo     = &tcp_hashinfo,
    .no_autobind        = true,
#ifdef CONFIG_COMPAT
    .compat_setsockopt  = compat_tcp_setsockopt,
    .compat_getsockopt  = compat_tcp_getsockopt,
#endif
    .diag_destroy       = tcp_abort,
};

好了,到此传输层协议的初始化过程就结束了。

sokcet接口是如何调用具体协议的接口的?

相信看了上面的传输层协议初始化,这儿你就已经清楚了,是通过应用层接口的参数指定了传输层的协议,再通过结构体的绑定,从而调用相应协议的函数接口。

下面只是调试验证一下,由于我们写的协议是AF_INET,所以我的断点就直接打在tcp的tcp_v4_connect函数,看一看是否和我们想象的一样,接着前面我们分析到 __sys_connect,看看它是否是从 __sys_connect ->tcp_v4_connect。

(gdb) bt
#0   (sk=0xc71b06a0, uaddr=0xc7895ec4, addr_len=16)
    at net/ipv4/tcp_ipv4.c:203
#1  0xc18151a1 in __inet_stream_connect (sock=0xc77a04e0,
    uaddr=, addr_len=, flags=2, is_sendmsg=0)
    at net/ipv4/af_inet.c:655
#2  0xc18152c6 in inet_stream_connect (sock=0xc77a04e0, uaddr=0xc7895ec4,
    addr_len=16, flags=2) at net/ipv4/af_inet.c:719
#3  0xc1756f44 in <__sys_connect> (fd=,
    uservaddr=, addrlen=16) at net/socket.c:1663
#4  0xc1757b78 in __do_sys_socketcall (args=,
    call=) at net/socket.c:2561
#5  __se_sys_socketcall (call=3, args=-1076065920) at net/socket.c:2527
#6  0xc1002095 in do_syscall_32_irqs_on (regs=)
    at arch/x86/entry/common.c:334
#7  do_fast_syscall_32 (regs=0xc7895fb4) at arch/x86/entry/common.c:397
#8  0xc199141b in entry_SYSENTER_32 () at arch/x86/entry/entry_32.S:887
#9  0x00000003 in ?? ()
#10 0x00000000 in ?? ()

从堆栈情况来看,完全一致,关于tcp_v4_connect分析参考 https://blog.csdn.net/wangpengqi/article/details/9472699

到此终于分析结束了,虽然中间分析有很多小波澜,尤其系统调用方式不是INT80,这个过程让我很矛盾,但总归结束了。

致谢以下博客及文中提到的博客
https://blog.csdn.net/sunnybeike/article/details/6958473
https://blog.csdn.net/yin262/article/details/53928178
https://blog.csdn.net/qyanqing/article/details/8039343
https://cloud.tencent.com/developer/article/1492374

你可能感兴趣的:(Socket与系统调用深度分析(基于5.0.1/32,其实系统调用并不是int80,而是VDSO,另一种快速的系统调用方式)