Linux c编程之多进程

一、说明

  在实际应用中,一个程序需要完成很多逻辑功能,有的功能(如数据处理)特别耗时,为了不影响主进程的处理速度,一般在启动一个主进程后,可以同时启动一个或多个进程,或者在需要的时候启动额外的进程去完成一些耗时的或独立的功能,这种应用编程模式叫做多进程。
多进程有如下特点:

  • 每个进程都拥有独立的地址空间,子进程崩溃不影响主进程

二、常用API

2.1 fork()

     #include <unistd.h>
     pid_t fork(void);

作用:创建一个子进程。通过复制调用进程的方式创建一个新进程,新进程称为子进程,调用进程称为父进程。子进程几乎复制了父进程全部的内容,除了以下几点:

  1. 子进程有自己唯一的进程ID
  2. 子进程中的父进程ID就是父进程的进程ID
  3. 子进程不继承父进程的内存锁
  4. 进程资源占用及CPU时间计数重置为0

返回值:
  成功时,在父进程中返回子进程的ID,在子进程中返回0.失败时,返回-1

2.2 getpid()

pid_t getpid(void);

作用:获取当前进程的ID

pid_t getppid(void);

作用:获取当前进程的父进程ID

2.3 waitpid()

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

作用:等待子进程的状态,回收子进程结束后的资源
参数说明:
  pid:
    小于 -1:等待进程组号为pid绝对值的任何子进程
     -1:等待任何子进程
    0: 等待任何进程组ID等于调用进程的子进程
    大于0: 等待进程ID等于该值的子进程
  option:
     WNOHANG 没有子进程退出时立即返回
    WUNTRACED 子进程停止时返回
    WCONTINUED 处于停止状态的子进程恢复时返回

  status: 如果不是NULL的话,返回状态信息,可以通过WIFEXITED(status)、WEXITSTATUS(status)等宏定义获取具体的信息

三、示例分析

3.1 最简单的例子

multi_process.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    pid_t ret_pid = -1;

    ret_pid = fork();
    if (ret_pid < 0) {
        printf("fork() failed, ret_pid:%d\n", ret_pid);
    } else if (0 == ret_pid) {
        printf("I am child process, ret_pid:%d\n", ret_pid);
        sleep(2);
        printf("child exit\n");
    } else {
        printf("I am parent process, ret_pid:%d\n", ret_pid);
        printf("wait child exit\n");
        waitpid(ret_pid, NULL, 0);
        printf("wait child exit end\n");
    }

    return 0;
}

Makefile:

all:
	gcc -o a.out multi_process.c
clean:
	-@rm a.out

测试:

$ ./a.out 
I am parent process, ret_pid:76977
wait child exit
I am child process, ret_pid:0
child exit
wait child exit end

3.2 获取(父)进程的ID

multi_process.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

void child_process()
{
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    printf("%s: pid[%d], parent pid[%d]\n", __FUNCTION__, pid, ppid);
    sleep(2);
}

void parent_process()
{
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    printf("%s: pid[%d], parent pid[%d]\n", __FUNCTION__, pid, ppid);
}

int main(int argc, char *argv[])
{
    pid_t ret_pid = -1;

    ret_pid = fork();
    if (ret_pid < 0) {
        printf("fork() failed, ret_pid:%d\n", ret_pid);
    } else if (0 == ret_pid) {
        child_process();
    } else {
        parent_process();
    }

    waitpid(ret_pid, NULL, 0);

    return 0;
}

Makefile:

all:
	gcc -o a.out multi_process.c
clean:
	-@rm a.out

测试:

$ ./a.out 
parent_process: pid[77080], parent pid[76350]
child_process: pid[77081], parent pid[77080]

3.3 僵尸进程

multi_process.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

void child_process()
{
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    printf("%s: pid[%d], parent pid[%d]\n", __FUNCTION__, pid, ppid);
    sleep(2);
    exit(1);
}

void parent_process()
{
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    printf("%s: pid[%d], parent pid[%d]\n", __FUNCTION__, pid, ppid);
}

int main(int argc, char *argv[])
{
    pid_t ret_pid = -1;

    ret_pid = fork();
    if (ret_pid < 0) {
        printf("fork() failed, ret_pid:%d\n", ret_pid);
    } else if (0 == ret_pid) {
        child_process();
    } else {
        parent_process();
    }

    //waitpid(ret_pid, NULL, 0);

    while (1) {
        sleep(1);
    }

    return 0;
}

Makefile:

all:
	gcc -o a.out multi_process.c
clean:
	-@rm a.out

测试结果:

$ ps aux | grep a.out
zorro    101317  0.0  0.0   4200   684 pts/12   S+   17:14   0:00 ./a.out
zorro    101318  0.0  0.0      0     0 pts/12   Z+   17:14   0:00 [a.out] <defunct>

如果杀掉父进程101317后,则僵尸进程101318就会消失。原因是父进程消失后,子进程变成了孤儿进程,孤儿进程由系统回收。

$ kill -9 101317
$ ps aux | grep a.out

3.4 两次fork避免不调用waitpid产生的僵尸进程

  在实际应用中,主程序关心子进程的状态,也不想调用waitpid函数。但是如果不调用waitpid,那么在子进程退出后会产生僵尸进程。为了避免这个问题,结合前文提到的“系统会回收孤儿进程”的特性,可以通过两次fork的方法解决。具体原理如下:

  1. 主进程第一次fork, 产生新的子进程,在主进程中阻塞调用waitpid函数
  2. 新的子进程再次调用fork,产生新的孙子进程,由孙子进程执行真正的逻辑,子进程则退出
  3. 由于子进程退出,导致孙子进程成为了孤儿进程。而主进程调用waitpid正常回收子进程的资源,继续执行主进程逻辑
  4. 当孙子进程执行完功能逻辑后退出时,因为是孤儿进程,则由系统自动回收资源
    代码示例:
    multi_process.c:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

void grandson_process()
{
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    printf("grandson start\n");
    printf("%s: pid[%d], parent pid[%d]\n", __FUNCTION__, pid, ppid);
    sleep(10);
    printf("grandson exit\n");
    exit(1);
}

void parent_process()
{
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    printf("%s: pid[%d], parent pid[%d]\n", __FUNCTION__, pid, ppid);
}

int main(int argc, char *argv[])
{
    pid_t ret_pid = -1;
    pid_t ret_pid2 = -1;

    ret_pid = fork();
    if (ret_pid < 0) {
        printf("fork() failed, ret_pid:%d\n", ret_pid);
    } else if (0 == ret_pid) {
        ret_pid2 = fork();
        if (ret_pid2 < 0) {
            printf("fork() failed, ret_pid2:%d\n", ret_pid2);
        } else if (0 == ret_pid2) {
            grandson_process();
        } else {
            printf("child start\n");
            sleep(5);
            printf("child exit\n");
            exit(1);
        }
    } else {
        parent_process();
        printf("before waitpid\n");
        waitpid(ret_pid, NULL, 0);
        printf("after waitpid\n");
    }

    while (1) {
        sleep(1);
    }

    return 0;
}

Makefile:

all:
	gcc -o a.out multi_process.c
clean:
	-@rm a.out

测试:

$ ./a.out 
parent_process: pid[101584], parent pid[100349]
before waitpid
child start
grandson start
grandson_process: pid[101586], parent pid[101585]
child exit
after waitpid
grandson exit
$ ps aux | grep a.out
zorro    101584  0.0  0.0   4200   660 pts/12   S+   18:12   0:00 ./a.out
zorro    101585  0.0  0.0   4200    88 pts/12   S+   18:12   0:00 ./a.out
zorro    101586  0.0  0.0   4200    88 pts/12   S+   18:12   0:00 ./a.out
zorro    101588  0.0  0.1  15944  2276 pts/0    S+   18:12   0:00 grep --color=auto a.out
$ ps aux | grep a.out
zorro    101584  0.0  0.0   4200   660 pts/12   S+   18:12   0:00 ./a.out
zorro    101586  0.0  0.0   4200    88 pts/12   S+   18:12   0:00 ./a.out
zorro    101590  0.0  0.1  15944  2240 pts/0    S+   18:12   0:00 grep --color=auto a.out
$ ps aux | grep a.out
zorro    101584  0.0  0.0   4200   660 pts/12   S+   18:12   0:00 ./a.out
zorro    101593  0.0  0.1  15944  2204 pts/0    S+   18:12   0:00 grep --color=auto a.out

3.5 无名管道pipe

multi_process.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int g_pipefd[2];

void child_process()
{
    char buf[64] = {0};
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    close(g_pipefd[1]);

    while (1) {
        read(g_pipefd[0], buf, sizeof(buf));
        printf("%s: read data:%s\n", __FUNCTION__, buf);
        sleep(1);
    }
}

void parent_process()
{
    pid_t pid = -1;
    pid_t ppid = -1;
    char buf[64] = {0};
    int number = 0;

    pid = getpid();
    ppid = getppid();

    close(g_pipefd[0]);
    while (1) {
        number++;
        snprintf(buf, sizeof(buf), "NO. %02d: hello world", number);
        write(g_pipefd[1], buf, strlen(buf));
        printf("%s: write data:%s\n", __FUNCTION__, buf);
        sleep(1);
    }
}

int main(int argc, char *argv[])
{
    int ret = 0;
    pid_t ret_pid = -1;

    ret = pipe(g_pipefd);
    if (ret < 0) {
        printf("pipe failed\n");
        return -1;
    }
    ret_pid = fork();
    if (ret_pid < 0) {
        printf("fork() failed, ret_pid:%d\n", ret_pid);
    } else if (0 == ret_pid) {
        child_process();
    } else {
        parent_process();
    }

    waitpid(ret_pid, NULL, 0);

    while (1) {
        sleep(1);
    }

    return 0;
}

Makefile:

all:
	gcc -o a.out multi_process.c
clean:
	-@rm a.out

测试结果:

parent_process: write data:NO. 01: hello world
child_process: read data:NO. 01: hello world
parent_process: write data:NO. 02: hello world
child_process: read data:NO. 02: hello world
parent_process: write data:NO. 03: hello world
child_process: read data:NO. 03: hello world
parent_process: write data:NO. 04: hello world
child_process: read data:NO. 04: hello world
parent_process: write data:NO. 05: hello world
child_process: read data:NO. 05: hello world

3.6 有名管道

read_process.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

#define FIFO_NAME "/tmp/myfifo"

int main(int argc, char *argv[])
{
    int ret = 0;
    int fd = -1;
    char buf[64] = {0};
    pid_t ret_pid = -1;

    unlink(FIFO_NAME);

    ret = mkfifo(FIFO_NAME, 0666);
    if (ret < 0) {
        printf("mkfifo failed\n");
        return -1;
    }

    fd = open(FIFO_NAME,O_RDONLY);
    if (fd < 0) {
        printf("open failed\n");
        return -1;
    }

    while (1) {
        read(fd, buf, sizeof(buf));
        printf("%s: read data:%s\n", __FUNCTION__, buf);
        sleep(1);
    }

    return 0;
}

write_process.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

#define FIFO_NAME "/tmp/myfifo"

int main(int argc, char *argv[])
{
    int ret = 0;
    int fd = -1;
    int number = 0;
    char buf[64] = {0};
    pid_t ret_pid = -1;

    fd = open(FIFO_NAME, O_WRONLY);
    if (fd < 0) {
        printf("open failed\n");
        return -1;
    }

    while (1) {
        number++;
        snprintf(buf, sizeof(buf), "NO. %02d: hello world", number);
        write(fd, buf, strlen(buf));
        printf("%s: write data:%s\n", __FUNCTION__, buf);
        sleep(1);
    }   

    return 0;
}

Makefile:

all:
	gcc -o read.out read_process.c
	gcc -o write.out write_process.c
clean:
	-@rm *.out

测试:

$ ./read.out 
main: read data:NO. 01: hello world
main: read data:NO. 02: hello world
main: read data:NO. 03: hello world
main: read data:NO. 04: hello world
main: read data:NO. 05: hello world
main: read data:NO. 05: hello world
$ ./write.out
main: write data:NO. 01: hello world
main: write data:NO. 02: hello world
main: write data:NO. 03: hello world
main: write data:NO. 04: hello world
main: write data:NO. 05: hello world

3.7 消息队列

msg_queue_send.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#include "msg_queue.h"

int msg_id = -1;

int main(int argc, char *argv[])
{
    int i = 0;
    key_t key = -1;
    char buf[32] = {0};
    msg_event_t msg;
    int ret = -1;
    struct msqid_ds msq_ds;

    key = ftok(KEY_DIR, 0);
    printf("%s: key[%d]\n", __FUNCTION__, key);

    msg_id = msgget(key, IPC_CREAT | 0666);
    if (msg_id < 0) {
        printf("%s: msg get failed: %s\n", __FUNCTION__, strerror(errno));
        exit(1);
    }

    printf("%s: msg id:%d\n", __FUNCTION__, msg_id);

    memset(&msq_ds, 0, sizeof(msq_ds));
    ret = msgctl(msg_id, IPC_STAT, &msq_ds);
    if (ret < 0) {
        printf("%s: msg get failed: %s\n", __FUNCTION__, strerror(errno));
        exit(1);
    }

    printf("msg_stime: %ld, msg_rtime: %ld, msg_ctime: %ld, msg_qnum: %ld,"
"msg_qbytes:%ld\n",
            msq_ds.msg_stime, msq_ds.msg_rtime, msq_ds.msg_ctime, msq_ds.msg_qnum,
            msq_ds.msg_qbytes);

    while (1) {
        memset(&msg, 0, sizeof(msg));
        ret = read(STDIN_FILENO, buf, sizeof(buf));
        if (ret <= 0) {
            printf("%s: read from stdin %s\n", __FUNCTION__, strerror(errno));
            break;
        }

        if (ret < 2) {
            continue;
        }
        buf[ret - 1] = '\0';

        msg.msg_type = 1;
        strcpy(msg.buf, buf);
        ret = msgsnd(msg_id, &msg, sizeof(msg) - sizeof(long int), IPC_NOWAIT);
        if (ret < 0) {
            printf("%s: msg send failed: %s\n", __FUNCTION__, strerror(errno));
            exit(1);
        }

        if (0 == strcmp(buf, "exit")) {
            printf("msg snd exit\n");
            break;
        }
    } 

    return 0;
}

msg_queue_recv.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#include "msg_queue.h"

int msg_id = -1;

int main(int argc, char *argv[])
{
    int i = 0;
    key_t key = -1;
    msg_event_t msg;
    int ret = -1;

    key = ftok(KEY_DIR, 0);
    printf("%s: key:[%d]\n", __FUNCTION__, key);

    msg_id = msgget(key, 0666);
    if (msg_id < 0) {
        printf("%s: msg get failed: %s\n", __FUNCTION__, strerror(errno));
        exit(1);
    }

    printf("%s: msg id:%d\n", __FUNCTION__, msg_id);

    while (1) {
        memset(&msg, 0, sizeof(msg));

        msg.msg_type = 0;
        ret = msgrcv(msg_id, &msg, sizeof(msg), 0, 0);
        if (ret < 0) {
            printf("%s: msg get failed: %s\n", __FUNCTION__, strerror(errno));
            exit(1);
        }
        printf("get msg: %s\n", msg.buf);
        if (0 == strcmp(msg.buf, "exit")) {
            printf("msg get exit\n");
            break;
        }
    }

    msgctl(msg_id, IPC_RMID, NULL);

    return 0;
}

msg_queue.h:

#ifndef __MSG_QUEUE_H__
#define __MSG_QUEUE_H__

#define KEY_DIR "/tmp"

typedef struct msg_event_s
{
    long int msg_type;
    char buf[512];
} msg_event_t;

#endif

Makefile:

all:
	gcc -o msg_send.out msg_queue_send.c -g
	gcc -o msg_recv.out msg_queue_recv.c -g

clean:
	-@rm *.out

测试:

$ sudo ./msg_send.out 
main: key[65539]
main: msg id:0
msg_stime: 1667902385, msg_rtime: 1667902385,
msg_ctime: 1667901682, msg_qnum: 0,msg_qbytes:32768

hello world
001
$ sudo ./msg_recv.out 
main: key:[65539]
main: msg id:0

get msg: hello world
get msg: 001

3.8 共享内存

share_memory_read.c:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

struct my_shared
{
    int flag;
    char str[1024];
};

int main(void)
{
    int running = 1;
    int shmid;
    void *shared_memory = NULL;
    struct my_shared *shared_buff = NULL;

    shmid = shmget((key_t)1234, sizeof(struct my_shared), 0666 | IPC_CREAT);
    if (shmid < 0) {
        perror("fail to shmget");
        exit(1);
    }

    shared_memory = shmat(shmid, NULL, 0);
    if(shared_memory == NULL) {
        perror("fail to shmat");
        exit(1);
    }

    shared_buff = (struct my_shared *)shared_memory;
    shared_buff->flag = 0;

    while(running) {
        if (shared_buff->flag) {
            printf("read message is: %s\n", shared_buff->str);
            shared_buff->flag = 0;

            if (strncmp(shared_buff->str, "end", 3) == 0) {
                running = 0;
            }
        }
    }

    if (shmdt(shared_memory) == -1) {
        perror("fail to shmdt");
        exit(1);
    }

    if (shmctl(shmid, IPC_RMID, 0) == -1) {
        perror("fail to shmctl");
        exit(1);
    }

    return 0;
}

share_memory_write.c:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>


struct my_shared
{
    int flag;
    char str[1024];
};

int main(void)
{
    int running = 1;
    int shmid = -1;
    char buffer[512] = {0};
    void *shared_memory = NULL;
    struct my_shared *shared_buff = NULL;

    shmid = shmget((key_t)1234, sizeof(struct my_shared), 0666 | IPC_CREAT);
    if (shmid < 0) {
        perror("fail to shmget");
        exit(1);
    }

    shared_memory = shmat(shmid, NULL, 0);
    if (!shared_memory) {
        perror("fail to shmat");
        exit(1);
    }

    shared_buff = (struct my_shared *)shared_memory;
    shared_buff->flag = 0;

    while (running) {
        while (shared_buff->flag) {
            printf("wait for other process's reading\n");
            sleep(2);
        }

        printf("Please input some data\n");
        fgets(buffer, 512, stdin);

        shared_buff->flag = 1;
        strncpy(shared_buff->str, buffer, 1024);

        if (strncmp(shared_buff->str, "end", 3) == 0) {
            running = 0;
        }
    }

    if (shmdt(shared_memory) == -1) {
        perror("fail to shmdt");
        exit(1);
    }

    return 0;
}

Makefile:

all:
	gcc -o ./write.out share_memory_write.c -g
	gcc -o ./read.out share_memory_read.c -g

clean:
	-@rm *.out

测试:

$ ./read.out 
read message is: hello world
$ ./write.out 
Please input some data
hello world
wait for other process's reading

3.9 exec()应用

exec()系列函数可以将当前进程的程序替换成新的可执行程序,而进程号不发生变化。
以下示例代码说明:
通过2次fork()得到孙子进程,在孙子进程内调用exec()函数将运行逻辑替换为new_app.out的程序(该程序创建my.txt并写入进程信息)
multi_process.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

void grandson_process()
{
    char buf[64] = {0};
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    snprintf(buf, sizeof(buf), "pid:%d, ppid:%d", pid, ppid);

    execl("/home/zorro/voip/Shengy-st/linux-c-demo/039-8-exec/new_app.out",
"new_app.out", buf, NULL);
}

void parent_process()
{
    pid_t pid = -1;
    pid_t ppid = -1;

    pid = getpid();
    ppid = getppid();

    printf("%s: pid[%d], parent pid[%d]\n", __FUNCTION__, pid, ppid);
}

int main(int argc, char *argv[])
{
    pid_t ret_pid = -1;
    pid_t ret_pid2 = -1;

    ret_pid = fork();
    if (ret_pid < 0) {
        printf("fork() failed, ret_pid:%d\n", ret_pid);
    } else if (0 == ret_pid) {
        ret_pid2 = fork();
        if (ret_pid2 < 0) {
            printf("fork() failed, ret_pid2:%d\n", ret_pid2);
        } else if (0 == ret_pid2) {
            grandson_process();
        } else {
            sleep(5);
            exit(1);
        }
    } else {
        parent_process();
        waitpid(ret_pid, NULL, 0);
    }

    while (1) {
        sleep(1);
    }

    return 0;
}

new_app.c:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    FILE *fp = NULL;
    char buf[128] = {0};

    fp = fopen("./my.txt", "w");
    if (!fp) {
        return -1;
    }

    if (argc > 1) {
        snprintf(buf, sizeof(buf), "%s:%s\n", argv[0], argv[1]);
        fwrite(buf, strlen(buf), 1, fp);
    }

    fclose(fp);

    sleep(10);

    return 0;
}

Makefile:

all:
	gcc -o a.out multi_process.c
	gcc -o new_app.out new_app.c
clean:
	-@rm *.out

测试:

$ ps aux | grep .out
zorro     50278  0.0  0.0   4200   660 pts/24   S+   15:29   0:00 ./a.out
zorro     50279  0.0  0.0   4196    88 pts/24   S+   15:29   0:00 ./a.out
zorro     50280  0.0  0.0   4328  1272 pts/24   S+   15:29   0:00 new_app.out pid:50280, ppid:50279
$ ps aux | grep .out
zorro     50278  0.0  0.0   4200   660 pts/24   S+   15:29   0:00 ./a.out
zorro     50280  0.0  0.0   4328  1272 pts/24   S+   15:29   0:00 new_app.out pid:50280, ppid:50279
$ cat my.txt 
new_app.out:pid:50280, ppid:50279
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪游东戴河

你就是这个世界的唯一

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值