51学通信论坛2017新版

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 1252|回复: 0
打印 上一主题 下一主题

OpenvSwitch系列之浅析main函数

[复制链接]

 成长值: 15613

  • TA的每日心情
    开心
    2022-7-17 17:50
  • 2444

    主题

    2544

    帖子

    7万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    74104
    跳转到指定楼层
    楼主
    发表于 2017-9-17 12:37:25 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    通过前面几篇解析OpenvSwitch内部主要数据结构和流程,对OpenvSwitch有了相对简单的了解,由于本人不是专业搞OpenvSwitch的,纯属业余爱好,今天可能是OpenvSwitch最后一篇了,我们要做到有始有终嘛,所以我们来分析一下main函数。然而main函数里面涉及内容比较多,而且比较深入,所以这篇文章只是浅析,不能算深入剖析,希望以后能有哪位大神能够做一个深入剖析。
    自己在学习开源软件总是喜欢看一下main函数,认为不把main函数搞明白了,就不算一个好程序员!!其实把main函数搞明白了,所有东西都会被串起来了,软件架构也就清晰啦。
    我们先看一下openvswitch默认启动时候的参数:
    整理一下参数如下:
    ovs-vswitchd: monitoring pid 2155 (healthy)
    ovs-vswitchd unix:/var/run/openvswitch/db.sock
    -vconsole:emer
    -vsyslog:err
    -vfile:info
    --mlockall
    --no-chdir
    --log-file=/var/log/openvswitch/ovs-vswitchd.log
    --pidfile=/var/run/openvswitch/ovs-vswitchd.pid
    --detach –monitor
    由此可知,上面是openvswitch会启动两个进程,一个进程是管理进程(2154),一个是业务处理进程(2155)。
    我们开始进入正题吧,main函数虽然不是很长,但是最复杂的函数,里面涉及很多与操作系统相关的功能和函数,比如,守护进程,信号,dpdk,pipe等。如果熟悉linux环境编程,看main函数可能比较轻松一点。我们采用分段介绍:
    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    int
    main(int argc, char *argv[])
    {
    char *unixctl_path = NULL;
    struct unixctl_server *unixctl;
    char *remote;
    bool exiting;
    int retval;
    set_program_name(argv[0]);/* ovs-vswitchd */
    retval = dpdk_init(argc,argv); /* Netdev-dpdk.c 只有设置--dpdk的参数才开启dpdk功能 */
    if (retval < 0) {
    return retval;
    }
    argc -= retval;
    argv += retval;
    ovs_cmdl_proctitle_init(argc, argv);/* windows函数是空 */
    service_start(&argc, &argv);/* linux函数这个是空 */
    remote = parse_options(argc, argv, &unixctl_path);
    fatal_ignore_sigpipe; /* linux设置 信号 */
    ovsrec_init;
    daemonize_start; /* 守护进程 */

    以上代码主要完成:
    1、dpdk初始化(如果系统支持),命令行参数解析,信号设置等,以便于openvswitch能够正常启动。参数解析比较枯燥,我们完全可以通过gdb调试跟踪,这样也比较方便理解。对于参数解析函数,我也没有深入研究,有兴趣网友可以分析一下。
    2、daemonize_start启动守护进程。这个函数我深入研究了一下,这里我会深入展开的。
    我们在这里就展开daemonize_start函数 : 我们假定进入函数daemonize_start的进程是进程A(PID=1005)
    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54

    void
    daemonize_start(void)
    {
    assert_single_threaded;
    daemonize_fd = -1;
    if (detach) {/* 根据命令行参数(命令行参数设置detach),则全局变量detach是1 因此会进入if分支。*/
    pid_t pid;/* 保存子进程ID */
    /* fork_and_wait_for_ startup这个函数用于创建一个子进程,下面我着重分析的。通过函数名可知,父进程被挂起了。 */
    if (fork_and_wait_for_startup(&daemonize_fd, &pid)) {
    VLOG_FATAL("could not detach from foreground session");
    }
    /* 进程A(PID=1005)一直阻塞在管道那里,等待数据可读。当管道有可读数据时候,进程A就会跳出阻塞状态,然后进入if分支,最后进程A(PID=1005)就结束了。 */
    if (pid > 0) {
    /* Running in parent process. */
    exit(0);
    }
    /* 执行到下面是进程A(PID=1004)的子进程,我们称作进程B(PID=1007)
    * 执行完下面函数,使得进程B与进程A脱离进程组关系。换句话说,
    * 如果进程A退出,进程B不会变成孤儿进程。
    */
    setsid; /* 子进程--进程B 与父进程脱离 进程组关系 */
    }
    /* 下面的代码 进程B会执行,父进程A一直阻塞,等待管道的输入 */
    if (monitor) {/* 根据命令行参数可知,全局变量monitor是1 */
    int saved_daemonize_fd = daemonize_fd;/* 管道的输入端文件描述符 */
    pid_t daemon_pid;
    /* 进程B创建一个子进程--进程C (PID=1010)*/
    if (fork_and_wait_for_startup(&daemonize_fd, &daemon_pid)) {
    VLOG_FATAL("could not initiate process monitoring");
    }
    /* 进入if分支,是进程B(PID=1007),但是进程B一直等待中。 */
    if (daemon_pid > 0) {
    /* Running in monitor process. */
    fork_notify_startup(saved_daemonize_fd);/* 在管道写入数据以便通知原始的父进程—进程A,这样进程A就会退出 */
    close_standard_fds;
    /* 进程B 阻塞这函数中,主要为了监控子进程—进程C(PID=1010) */
    monitor_daemon(daemon_pid);
    }
    /* Running in daemon process. */
    }
    forbid_forking("running in daemon process");
    if (pidfile) {
    make_pidfile; /* 创建pid文件 进程号是进程C的进程号 */
    }
    /* Make sure that the unixctl commands for vlog get registered in a
    * daemon, even before the first log message. */
    vlog_init;
    }

    我们来看一下,这个fork函数,比较欣慰的是这个函数有英文注释,而且英文注释说的也非常清楚,下面也有我自己添加的注释。
    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72

    /* Forks, then:
    *
    * - In the parent, waits for the child to signal that it has completed its
    * startup sequence. Then stores -1 in '*fdp' and returns the child's
    * pid in '*child_pid' argument.
    * 父进程一直等待子进程的完成信号,当收到子进程的信号后,文件描述符(*fdp)
    * 保存-1,child_pid保存子进程的进程号。
    *
    * - In the child, stores a fd in '*fdp' and returns 0 through '*child_pid'
    * argument. The caller should pass the fd to fork_notify_startup after
    * it finishes its startup sequence.
    * 子进程返回:*fdp保存有效的文件描述符,child_pid保存0。
    *
    *
    * Returns 0 on success. If something goes wrong and child process was not
    * able to signal its readiness by calling fork_notify_startup, then this
    * function returns -1. However, even in case of failure it still sets child
    * process id in '*child_pid'.
    *
    * fdp = 文件描述符
    */
    static int
    fork_and_wait_for_startup(int *fdp, pid_t *child_pid)
    {
    int fds[2];
    pid_t pid;
    int ret = 0;
    xpipe(fds); /* 初始化管道 */
    pid = fork_and_clean_up; /* 创建子进程 */
    if (pid > 0) {
    /* Running in parent process. 父进程 */
    size_t bytes_read;
    char c;
    close(fds[1]);/* 关闭写入端 也就是说父进程负责读 */
    if (read_fully(fds[0], &c, 1, &bytes_read) != 0) {/* 父进程一直,等待读取数据 */
    int retval;
    int status;
    do {
    retval = waitpid(pid, &status, 0);/* 等待子进程信号 */
    } while (retval == -1 && errno == EINTR);
    if (retval == pid) {
    if (WIFEXITED(status) && WEXITSTATUS(status)) {
    /* Child exited with an error. Convey the same error
    * to our parent process as a courtesy. */
    exit(WEXITSTATUS(status));
    } else {
    char *status_msg = process_status_msg(status);
    VLOG_ERR("fork child died before signaling startup (%s)",
    status_msg);
    ret = -1;
    }
    } else if (retval < 0) {
    VLOG_FATAL("waitpid failed (%s)", ovs_strerror(errno));
    } else {
    OVS_NOT_REACHED;
    }
    }
    close(fds[0]); /* 关闭读取端 并且返回 */
    *fdp = -1;
    } else if (!pid) {
    /* Running in child process. 子进程 */
    close(fds[0]); /* 关闭读取端 也就是说子进程负责写 */
    *fdp = fds[1];
    }
    *child_pid = pid; /* 保存进程 */
    return ret;
    }

    通过上面的分析可知,某个时刻,会有三个进程同事存在,为了验证准确性,我进行了gdb调试,调试截图如下:


    再启动另外一个终端查看,ovs-vswitchd进程是否为三个,如下图所示:


    通过查看,果然是存在三个进程。那么问题来了,当启动完成后,只有两个进程,那么什么时候进程A才会退出呢?
    我们现在回到main函数中,通过阅读代码,我现在是这下面这个函数中,进程C会通知进程B完成初始化,然后进程B在通知进程A,最终进程A退出。下面代码主要是main函数中死循环,具体函数说明如下:
    C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    exiting = false;
    while (!exiting) {
    memory_run;
    if (memory_should_report) {
    struct simap usage;
    simap_init(&usage);
    bridge_get_memory_usage(&usage);
    memory_report(&usage);
    simap_destroy(&usage);
    }
    /* 这个函数是vswitchd入口函数,所有虚拟交换机都是通过这个函数一点一点
    * 构建出来。之前几篇文件,如果查看函数调用,最终都会被这个函数调用。
    * 这个函数里面会调用函数daemonize_complete,这个函数是通知进程B初始化
    * 完成。
    */
    bridge_run;
    unixctl_server_run(unixctl);/* 针对上面命令行参数,unixctl是null. */
    netdev_run;
    memory_wait;
    bridge_wait;
    unixctl_server_wait(unixctl);
    netdev_wait;
    if (exiting) {
    poll_immediate_wake;
    }
    poll_block; /* 当初始化完成后,进程C 一直在这里进程轮询 */
    if (should_service_stop) {
    exiting = true;
    }
    }
    bridge_exit;
    unixctl_server_destroy(unixctl);
    service_stop;
    return 0;
    }

    我们总结一下
    1、任何程序都是可以调试,当我们通过看代码无法分析出层次关系,往往调试工具、日志是我们最好的伙伴。比如说,我当初不知道进程A是如何在什么情况下退出的,这个时候我就通过gdb调试,一步一步断点跟踪。
    2、对于main函数还是有很多需要仔细推敲的,这里个人水平有限,加上工作中又有很多事情无法进一步分析。这里有几个疑惑地方,在这里向大家说明一下,如果有哪位大神知道,请一定要指点迷津。
    疑惑1:函数unixctl_server_create会创建一个socket文件,这个socket文件默认是"punix:/usr/local/var/run/openvswitch/ovs-vswitchd.12861.ctl",用于通过和其他进程(主要是openvswitch相关工具),具体是哪些工具呢??
    疑惑2:在main函数中,注册了一个命令行exit,使得openvswitchd能够安全退出,不知道这个命令行怎么用??
    以上就是我分析openvswitchd源代码全部内容,有些地方分析不是很透彻,有些地方分析的也比较混乱。通过我这最近两月学习,有一些心得体会拿出来和大家分享一下:
    1、个人觉得openvswitchd中代码含金量还是很高的,无论是从个人还是公司角度,都是值得我们学习与阅读。强烈建议有相关需求的人去阅读一下。
    2、如果想深入学习openvswitchd,需要特别关注一下这几个方面,用户态和内核态程序间通信(Netlink),dpdk,netdev等。如果熟悉内核的人,对这些内容可能不是很陌生,然而很多人都不是搞内核的,所以这些内容需要我们个个突破。
    3、我前面几篇都是分析用户态程序(ovs-vswitchd),内核态程序没有分析,其实内核态程序占得比重也非常大。
    4、我建议在看我源码分析文章时,最好能够自己先看一下源代码,这样能够跟上我文章的思路,不然有的文章看着会很枯燥。
    这里感谢大家支持,感谢sdnlab支持,希望大家在SDN道路上越走越光明!!
    作者简介:
    徐小冰:毕业于河北大学,主要从事嵌入式软件开发,虚拟化,SDN。目前基于ODL和Open vSwitch进行二次开发,希望与广大网友一起探讨学习。作者系OpenDaylihgt群(194240432)资深活跃用户,@IT难人。
    声明:本文转载自网络。版权归原作者所有,如有侵权请联系删除。
    扫描并关注51学通信微信公众号,获取更多精彩通信课程分享。

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    Archiver|手机版|小黑屋|51学通信技术论坛

    GMT+8, 2025-1-31 18:09 , Processed in 0.074798 second(s), 32 queries .

    Powered by Discuz! X3

    © 2001-2013 Comsenz Inc.

    快速回复 返回顶部 返回列表