android 源码分析

1,init 进程
system\core\init\init.c
它是用户空间的第一个进程
它负责创建系统中的几个关键进程

重点 init 进程如何创建 zygote

int main(int argc, char **argv)
{
    int device_fd = -1;
    int property_set_fd = -1;
    int signal_recv_fd = -1;
    int keychord_fd = -1;
    int fd_count;
    int s[2];
    int fd;
    struct sigaction act;
    char tmp[PROP_VALUE_MAX];
    struct pollfd ufds[4];
    char *tmpdev;
    char* debuggable;

    act.sa_handler = sigchld_handler;
    act.sa_flags = SA_NOCLDSTOP;
    act.sa_mask = 0;
    act.sa_restorer = NULL;
    sigaction(SIGCHLD, &act, 0);

    /* clear the umask */
    umask(0);

        /* Get the basic filesystem setup we need put
         * together in the initramdisk on / and then we'll
         * let the rc file figure out the rest.
         * //创建一些文件夹,并挂载设备
         */
    mkdir("/dev", 0755);
    mkdir("/proc", 0755);
    mkdir("/sys", 0755);

    mount("tmpfs", "/dev", "tmpfs", 0, "mode=0755");
    mkdir("/dev/pts", 0755);
    mkdir("/dev/socket", 0755);
    mount("devpts", "/dev/pts", "devpts", 0, NULL);
    mount("proc", "/proc", "proc", 0, NULL);
    mount("sysfs", "/sys", "sysfs", 0, NULL);

        /* We must have some place other than / to create the
         * device nodes for kmsg and null, otherwise we won't
         * be able to remount / read-only later on.
         * Now that tmpfs is mounted on /dev, we can actually
         * talk to the outside world.
         * //重定向标准输入/输出/错误输出到/dev/_null_。
         */
    open_devnull_stdio();
    /*
    设置init的日志输出设备为/dev/__kmsg__,不过该文件打开后,会立即被unlink了,

这样,其他进程就无法打开这个文件读取日志信息了。
    */
    log_init();
    
    INFO("reading config file\n");
    //解析init.rc配置文件
    parse_config_file("/init.rc");

    /* pull the kernel commandline and ramdisk properties file in */
    qemu_init();
    import_kernel_cmdline(0);

    get_hardware_name();
    snprintf(tmp, sizeof(tmp), "/init.%s.rc", hardware);
    parse_config_file(tmp);

    action_for_each_trigger("early-init", action_add_queue_tail);
    drain_action_queue();

    INFO("device init\n");
    device_fd = device_init();

//初始化和属性相关的资源
    property_init();
    
    // only listen for keychords if ro.debuggable is true
    debuggable = property_get("ro.debuggable");
    if (debuggable && !strcmp(debuggable, "1")) {
        keychord_fd = open_keychord();
    }

    if (console[0]) {
        snprintf(tmp, sizeof(tmp), "/dev/%s", console);
        console_name = strdup(tmp);
    }

    fd = open(console_name, O_RDWR);
    if (fd >= 0)
        have_console = 1;
    close(fd);
	//下面这个函数将加载这个文件作为系统的开机画面
    if( load_565rle_image(INIT_IMAGE_FILE) ) {
    fd = open("/dev/tty0", O_WRONLY);
    if (fd >= 0) {
        const char *msg;
            msg = "\n"
        "\n"
        "\n"
        "\n"
        "\n"
        "\n"
        "\n"  // console is 40 cols x 30 lines
        "\n"
        "\n"
        "\n"
        "\n"
        "\n"
        "\n"
        "\n"
        "             A N D R O I D ";
        write(fd, msg, strlen(msg));
        close(fd);
    }
    }

    if (qemu[0])
        import_kernel_cmdline(1); 

    if (!strcmp(bootmode,"factory"))
        property_set("ro.factorytest", "1");
    else if (!strcmp(bootmode,"factory2"))
        property_set("ro.factorytest", "2");
    else
        property_set("ro.factorytest", "0");

    property_set("ro.serialno", serialno[0] ? serialno : "");
    property_set("ro.bootmode", bootmode[0] ? bootmode : "unknown");
    property_set("ro.baseband", baseband[0] ? baseband : "unknown");
    property_set("ro.carrier", carrier[0] ? carrier : "unknown");
    property_set("ro.bootloader", bootloader[0] ? bootloader : "unknown");

    property_set("ro.hardware", hardware);
    snprintf(tmp, PROP_VALUE_MAX, "%d", revision);
    property_set("ro.revision", tmp);

        /* execute all the boot actions to get us started */
        //执行位于init阶段的动作
    action_for_each_trigger("init", action_add_queue_tail);
    drain_action_queue();

        /* read any property files on system or data and
         * fire up the property service.  This must happen
         * after the ro.foo properties are set above so
         * that /data/local.prop cannot interfere with them.
         */
          //启动属性服务
    property_set_fd = start_property_service();

    /* create a signalling mechanism for the sigchld handler */
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) {
        signal_fd = s[0];
        signal_recv_fd = s[1];
        fcntl(s[0], F_SETFD, FD_CLOEXEC);
        fcntl(s[0], F_SETFL, O_NONBLOCK);
        fcntl(s[1], F_SETFD, FD_CLOEXEC);
        fcntl(s[1], F_SETFL, O_NONBLOCK);
    }

    /* make sure we actually have all the pieces we need */
    if ((device_fd < 0) ||
        (property_set_fd < 0) ||
        (signal_recv_fd < 0)) {
        ERROR("init startup failure\n");
        return 1;
    }

    /* execute all the boot actions to get us started */
    action_for_each_trigger("early-boot", action_add_queue_tail);
    action_for_each_trigger("boot", action_add_queue_tail);
    drain_action_queue();

        /* run all property triggers based on current state of the properties */
    queue_all_property_triggers();
    drain_action_queue();

        /* enable property triggers */   
    property_triggers_enabled = 1;     

    ufds[0].fd = device_fd;//device_fd用于监听来自内核的Uevent事件
    ufds[0].events = POLLIN;
    ufds[1].fd = property_set_fd;//property_set_fd用于监听来自属性服务器的事件
    ufds[1].events = POLLIN;
    ufds[2].fd = signal_recv_fd;//signal_recv_fd由socketpair创建,它的事件来自另外一个socket
    ufds[2].events = POLLIN;
    fd_count = 3;

    if (keychord_fd > 0) {
        ufds[3].fd = keychord_fd;//如果keychord设备初始化成功,则init也会关注来自这个设备的事件。
        ufds[3].events = POLLIN;
        fd_count++;
    } else {
        ufds[3].events = 0;
        ufds[3].revents = 0;
    }

#if BOOTCHART
    bootchart_count = bootchart_init();
    if (bootchart_count < 0) {
        ERROR("bootcharting init failure\n");
    } else if (bootchart_count > 0) {
        NOTICE("bootcharting started (period=%d ms)\n", bootchart_count*BOOTCHART_POLLING_MS);
    } else {
        NOTICE("bootcharting ignored\n");
    }
#endif

    for(;;) {
    	//从此init将进入一个无限循环。
        int nr, i, timeout = -1;

        for (i = 0; i < fd_count; i++)
            ufds[i].revents = 0;
		//在循环中执行动作
        drain_action_queue();
        //重启那些已经死去的进程
        restart_processes();

        if (process_needs_restart) {
            timeout = (process_needs_restart - gettime()) * 1000;
            if (timeout < 0)
                timeout = 0;
        }

#if BOOTCHART
        if (bootchart_count > 0) {
            if (timeout < 0 || timeout > BOOTCHART_POLLING_MS)
                timeout = BOOTCHART_POLLING_MS;
            if (bootchart_step() < 0 || --bootchart_count == 0) {
                bootchart_finish();
                bootchart_count = 0;
            }
        }
#endif
		//调用poll等待一些事情的发生
        nr = poll(ufds, fd_count, timeout);
        if (nr <= 0)
            continue;
   //ufds[2]保存的是signal_recv_fd,用于接收来自socket的消息。
        if (ufds[2].revents == POLLIN) {
            /* we got a SIGCHLD - reap and restart as needed */
            read(signal_recv_fd, tmp, sizeof(tmp));
            while (!wait_for_one_process(0))
                ;
            continue;
        }

        if (ufds[0].revents == POLLIN)
            handle_device_fd(device_fd);//处理Uevent事件

        if (ufds[1].revents == POLLIN)
            handle_property_set_fd(property_set_fd);//处理属性服务的事件
        if (ufds[3].revents == POLLIN)
            handle_keychord(keychord_fd);//处理keychord事件。
    }

    return 0;
}

解析init.rc 文件
执行各个阶段动作创建Zygote 的工作就是
调用property_init初始化属性相关的资源,并且通过property_start_service启动属性服务
init进入一个无限循环,并且等待一些事情的发生。重点关注init如何处理来自socket和来自属性服务器相关的事情。

解析配置文件
system\core\init\parser.c

int parse_config_file(const char *fn)
{
    char *data;
    //读取配置文件的内容,这个文件是init.rc。
    data = read_file(fn, 0);
    if (!data) return -1;
	//调用parse_config做真正的解析
    parse_config(fn, data);
    DUMP();
    return 0;
}
static void parse_config(const char *fn, char *s)
{
    struct parse_state state;
    char *args[SVC_MAXARGS];
    int nargs;

    nargs = 0;
    state.filename = fn;
    state.line = 1;
    state.ptr = s;
    state.nexttoken = 0;
    //设置解析函数,不同的内容用不同的解析函数
    state.parse_line = parse_line_no_op;
    for (;;) {
        switch (next_token(&state)) {
        case T_EOF:
            state.parse_line(&state, 0, 0);
            return;
        case T_NEWLINE:
            if (nargs) {
           		 //得到关键字的类型
                int kw = lookup_keyword(args[0]);
                 //判断关键字类型是不是SECTION。
                if (kw_is(kw, SECTION)) {
                    state.parse_line(&state, 0, 0);
                    //解析这个SECTION。
                    parse_new_section(&state, kw, nargs, args);
                } else {
                    state.parse_line(&state, nargs, args);
                }
                nargs = 0;
            }
            break;
        case T_TEXT:
            if (nargs < SVC_MAXARGS) {
                args[nargs++] = state.text;
            }
            break;
        }
    }
}

init.rc的解析

on init  #根据上面的分析,on关键字标示一个section,对应的名字是”init”
 #下面所有的内容都属于这个section,直到下一个section开始时。
 exportPATH /sbin:/system/sbin:/system/bin:/system/xbin
exportANDROID_BOOTLOGO 1 #根据keywords.h的定义,export表示一个COMMAND

#service也是section的标示,对应section的名为“zygote“
service zygote /system/bin/app_process -Xzygote/system/bin –zygote        \
--start-system-server
socket zygote stream 666  #socket关键字表示OPTION
on restart write /sys/android_power/request_state wake #onrestart也是OPTION
on restart write /sys/power/state on
on restart restart media

解析service
解析section的入口函数是parse_new_section

void parse_new_section(struct parse_state *state, int kw,
                       int nargs, char **args)
{
    printf("[ %s %s ]\n", args[0],
           nargs > 1 ? args[1] : "");
    switch(kw) {
    case K_service:  //解析service,用parse_service和parse_line_service
        state->context = parse_service(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_service;
            return;
        }
        break;
    case K_on: //解析on section
        state->context = parse_action(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_action;
            return;
        }
        break;
    }
    state->parse_line = parse_line_no_op;
}

service解析时,用到了parse_service和parse_line_service两个函数
service结构体

struct service {
        /* list of all services */
        //listnode是一个特殊的结构体,在内核代码中用得非常多,主要用来将结构体链接成一个
      //双向链表。init中有一个全局的service_list,专门用来保存解析配置文件后得到的service。  
    struct listnode slist;

    const char *name; //service的名字,对应我们这个例子就是”zygote”
    const char *classname;//service所属class的名字,默认是”defult”

    unsigned flags;//service的属性
    pid_t pid;  //进程号
    time_t time_started; //上一次启动的时间   /* time of last start */
    time_t time_crashed;  //上一次死亡的时间    /* first crash within inspection window */
    int nr_crashed;   //死亡次数      /* number of times crashed within window */
    
    uid_t uid;//uid,gid相关
    gid_t gid;
    gid_t supp_gids[NR_SVC_SUPP_GIDS];
    size_t nr_supp_gids;
/*
有些service需要使用socket,下面这个socketinfo用来描述socket的相关信息。

我们的zygote也使用了socket,配置文件中的内容是socket zygote stream 666。

它表示将创建一个AF_STREAM类型的socket(其实就是TCP socket),该socket的名为“zygote”,

读写权限是666。
*/
    struct socketinfo *sockets;
    //service一般运行在单独的一个进程中,envvars用来描述创建这个进程时所需的环境变量信息。
    struct svcenvinfo *envvars;

    struct action onrestart;  /* Actions to execute on restart. */
    
    /* keycodes for triggering this service via /dev/keychord */
       //和keychord相关的内容
    int *keycodes;
    int nkeycodes;
    int keychord_id;

	//参数个数
    int nargs;
    /* "MUST BE AT THE END OF THE STRUCT" */
     //用于存储参数
    char *args[1];
};

parse_service

static void *parse_service(struct parse_state *state, int nargs, char **args)
{
    struct service *svc; //声明一个service结构体
    if (nargs < 3) {
        parse_error(state, "services must have a name and a program\n");
        return 0;
    }
    if (!valid_name(args[1])) {
        parse_error(state, "invalid service name '%s'\n", args[1]);
        return 0;
    }
 //init维护了一个全局的service链表,先判断是否已经有同名的service了。
    svc = service_find_by_name(args[1]);
    if (svc) {
        parse_error(state, "ignored duplicate definition of service '%s'\n", args[1]);
        //如果有同名的service,则不能继续后面的操作
        return 0;
    }
    
    nargs -= 2;
    svc = calloc(1, sizeof(*svc) + sizeof(char*) * nargs);
    if (!svc) {
        parse_error(state, "out of memory\n");
        return 0;
    }
    svc->name = args[1];
    svc->classname = "default";//设置classname为”default”,这个很关键!
    memcpy(svc->args, args + 2, sizeof(char*) * nargs);
    svc->args[nargs] = 0;
    svc->nargs = nargs;
    svc->onrestart.name = "onrestart";
    list_init(&svc->onrestart.commands);
    //把zygote这个service加到全局链表service_list中。
    list_add_tail(&service_list, &svc->slist);
    return svc;
}

parse_service函数只是搭建了一个service的架子

parse_line_service

static void parse_line_service(struct parse_state *state, int nargs, char **args)
{
    struct service *svc = state->context;
    struct command *cmd;
    int i, kw, kw_nargs;

    if (nargs == 0) {
        return;
    }
    //其实还是根据关键字来做各种处理。
    kw = lookup_keyword(args[0]);
    switch (kw) {
    case K_capability:
        break;
    case K_class:
        if (nargs != 2) {
            parse_error(state, "class option requires a classname\n");
        } else {
            svc->classname = args[1];
        }
        break;
    case K_console:
        svc->flags |= SVC_CONSOLE;
        break;
    case K_disabled:
        svc->flags |= SVC_DISABLED;
        break;
    case K_group:
        if (nargs < 2) {
            parse_error(state, "group option requires a group id\n");
        } else if (nargs > NR_SVC_SUPP_GIDS + 2) {
            parse_error(state, "group option accepts at most %d supp. groups\n",
                        NR_SVC_SUPP_GIDS);
        } else {
            int n;
            svc->gid = decode_uid(args[1]);
            for (n = 2; n < nargs; n++) {
                svc->supp_gids[n-2] = decode_uid(args[n]);
            }
            svc->nr_supp_gids = n - 2;
        }
        break;
    case K_keycodes:
        if (nargs < 2) {
            parse_error(state, "keycodes option requires atleast one keycode\n");
        } else {
            svc->keycodes = malloc((nargs - 1) * sizeof(svc->keycodes[0]));
            if (!svc->keycodes) {
                parse_error(state, "could not allocate keycodes\n");
            } else {
                svc->nkeycodes = nargs - 1;
                for (i = 1; i < nargs; i++) {
                    svc->keycodes[i - 1] = atoi(args[i]);
                }
            }
        }
        break;
    case K_oneshot:
     /*
这是service的属性,它一共有五个属性,分别为:
SVC_DISABLED:不随class自动启动。下面将会看到class的作用。
SVC_ONESHOT:退出后不需要重启,也就是这个service只启动一次就可以了。
SVC_RUNNING:正在运行,这是service的状态。
SVC_RESTARTING:等待重启,这也是service的状态。
SVC_CONSOLE:该service需要使用控制台 。
SVC_CRITICAL:如果在规定时间内该service不断重启,则系统会重启并进入恢复模式。
zygote没有使用任何属性,这表明它:会随着class的处理自动启动;
退出后会由init重启;不使用控制台;即使不断重启也不会导致系统进入恢复模式。
       */
        svc->flags |= SVC_ONESHOT;
        break;
    case K_onrestart: //根据onrestart的内容,填充action结构体的内容
        nargs--;
        args++;
        kw = lookup_keyword(args[0]);
        if (!kw_is(kw, COMMAND)) {
            parse_error(state, "invalid command '%s'\n", args[0]);
            break;
        }
        kw_nargs = kw_nargs(kw);
        if (nargs < kw_nargs) {
            parse_error(state, "%s requires %d %s\n", args[0], kw_nargs - 1,
                kw_nargs > 2 ? "arguments" : "argument");
            break;
        }
//创建command结构体
        cmd = malloc(sizeof(*cmd) + sizeof(char*) * nargs);
        cmd->func = kw_func(kw);
        cmd->nargs = nargs;
        memcpy(cmd->args, args, sizeof(char*) * nargs);
        //把新建的command加入到双向链表中。
        list_add_tail(&svc->onrestart.commands, &cmd->clist);
        break;
    case K_critical:
        svc->flags |= SVC_CRITICAL;
        break;
    case K_setenv: { /* name value */
        struct svcenvinfo *ei;
        if (nargs < 2) {
            parse_error(state, "setenv option requires name and value arguments\n");
            break;
        }
        ei = calloc(1, sizeof(*ei));
        if (!ei) {
            parse_error(state, "out of memory\n");
            break;
        }
        ei->name = args[1];
        ei->value = args[2];
        ei->next = svc->envvars;
        svc->envvars = ei;
        break;
    }
    case K_socket: {//创建socket相关信息
    /* name type perm [ uid gid ] */
    	
        struct socketinfo *si;
        if (nargs < 4) {
            parse_error(state, "socket option requires name, type, perm arguments\n");
            break;
        }
        if (strcmp(args[2],"dgram") && strcmp(args[2],"stream")) {
            parse_error(state, "socket type must be 'dgram' or 'stream'\n");
            break;
        }
        si = calloc(1, sizeof(*si));
        if (!si) {
            parse_error(state, "out of memory\n");
            break;
        }
        si->name = args[1];//socket的名字
        si->type = args[2];//socket的类型
        si->perm = strtoul(args[3], 0, 8); //socket的读写权限
        if (nargs > 4)
            si->uid = decode_uid(args[4]);
        if (nargs > 5)
            si->gid = decode_uid(args[5]);
        si->next = svc->sockets;
        svc->sockets = si;
        break;
    }
    case K_user:
        if (nargs != 2) {
            parse_error(state, "user option requires a user id\n");
        } else {
            svc->uid = decode_uid(args[1]);
        }
        break;
    default:
        parse_error(state, "invalid option '%s'\n", args[0]);
    }
}

init控制service
1,启动zygote
init.rc 中 class_start default

void service_start(struct service *svc, const char *dynamic_args)
{
    struct stat s;
    pid_t pid;
    int needs_console;
    int n;

        /* starting a service removes it from the disabled
         * state and immediately takes it out of the restarting
         * state if it was in there
         */
    svc->flags &= (~(SVC_DISABLED|SVC_RESTARTING));
    svc->time_started = 0;
    
        /* running processes require no additional work -- if
         * they're in the process of exiting, we've ensured
         * that they will immediately restart on exit, unless
         * they are ONESHOT
         */
   
    if (svc->flags & SVC_RUNNING) {
        return;//如果这个service已在运行,则不用处理
    }

    needs_console = (svc->flags & SVC_CONSOLE) ? 1 : 0;
    if (needs_console && (!have_console)) {
        ERROR("service '%s' requires console\n", svc->name);
        svc->flags |= SVC_DISABLED;
        return;
    }
       /*
service一般运行于另外一个进程中,这个进程也是init的子进程,所以启动service前需要判断
对应的可执行文件是否存在,zygote对应的可执行文件是/system/bin/app_process
*/
    if (stat(svc->args[0], &s) != 0) {
        ERROR("cannot find '%s', disabling '%s'\n", svc->args[0], svc->name);
        svc->flags |= SVC_DISABLED;
        return;
    }

    if ((!(svc->flags & SVC_ONESHOT)) && dynamic_args) {
        ERROR("service '%s' must be one-shot to use dynamic args, disabling\n",
               svc->args[0]);
        svc->flags |= SVC_DISABLED;
        return;
    }

    NOTICE("starting '%s'\n", svc->name);

    pid = fork(); //调用fork创建子进程

    if (pid == 0) {
    //pid为零,我们在子进程中
        struct socketinfo *si;
        struct svcenvinfo *ei;
        char tmp[32];
        int fd, sz;
	//得到属性存储空间的信息并加到环境变量中,后面在属性服务一节中会碰到使用它的地方。
        get_property_workspace(&fd, &sz);
        sprintf(tmp, "%d,%d", dup(fd), sz);
        add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);
//添加环境变量信息
        for (ei = svc->envvars; ei; ei = ei->next)
            add_environment(ei->name, ei->value);
//根据socketinfo创建socket
        for (si = svc->sockets; si; si = si->next) {
            int s = create_socket(si->name,
                                  !strcmp(si->type, "dgram") ? 
                                  SOCK_DGRAM : SOCK_STREAM,
                                  si->perm, si->uid, si->gid);
            if (s >= 0) {
            //在环境变量中添加socket信息。
                publish_socket(si->name, s);
            }
        }

        if (needs_console) {
            setsid();
            open_console();
        } else {
            zap_stdio();
        }

#if 0
        for (n = 0; svc->args[n]; n++) {
            INFO("args[%d] = '%s'\n", n, svc->args[n]);
        }
        for (n = 0; ENV[n]; n++) {
            INFO("env[%d] = '%s'\n", n, ENV[n]);
        }
#endif
//设置uid,gid等
        setpgid(0, getpid());

    /* as requested, set our gid, supplemental gids, and uid */
        if (svc->gid) {
            setgid(svc->gid);
        }
        if (svc->nr_supp_gids) {
            setgroups(svc->nr_supp_gids, svc->supp_gids);
        }
        if (svc->uid) {
            setuid(svc->uid);
        }
 /*
执行/system/bin/app_process,这样就进入到app_process的main函数中了。
fork、execve这两个函数都是Linux系统上常用的系统调用。
        */
        if (!dynamic_args)
            execve(svc->args[0], (char**) svc->args, (char**) ENV);
        else {
            char *arg_ptrs[SVC_MAXARGS+1];
            int arg_idx = svc->nargs;
            char *tmp = strdup(dynamic_args);
            char *next = tmp;
            char *bword;

            /* Copy the static arguments */
            memcpy(arg_ptrs, svc->args, (svc->nargs * sizeof(char *)));

            while((bword = strsep(&next, " "))) {
                arg_ptrs[arg_idx++] = bword;
                if (arg_idx == SVC_MAXARGS)
                    break;
            }
            arg_ptrs[arg_idx] = '\0';
            execve(svc->args[0], (char**) arg_ptrs, (char**) ENV);
        }
        _exit(127);
    }

    if (pid < 0) {
        ERROR("failed to start '%s'\n", svc->name);
        svc->pid = 0;
        return;
    }
//父进程init的处理,设置service的信息,如启动时间、进程号,以及状态等。
    svc->time_started = gettime();
    svc->pid = pid;
    svc->flags |= SVC_RUNNING;
//每一个service都有一个属性,zygote的属性为init.svc.zygote,现在设置它的值为running
    notify_service_state(svc->name, "running");
}

原来,zygote是通过fork和execv共同创建的

重启zygote

static void sigchld_handler(int s)
{
//当子进程退出时,init的这个信号处理函数会被调用
    write(signal_fd, &s, 1);
}

signal_fd,就是在init中通过socketpair创建的两个socket中的一个,既然会往这个signal_fd中发送数据,那么另外一个socket就一定能接收到,这样就会导致init从poll函数中返回

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值