《unix环境高级编程》--- 高级I/O

长的非阻塞write
从标准输入读500000字节,并试图写到标准输出。先将标准输出设置为非阻塞,然后用for循环输出,每次write调用的结果都在标准出错上打印。

#include "apue.h"
#include <errno.h>
#include <fcntl.h>

char buf[500000];

int main(void)
{
    int ntowrite, nwrite;
    char *ptr;

    ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
    fprintf(stderr, "read %d bytes\n", ntowrite);

    set_fl(STDOUT_FILENO, O_NONBLOCK);  /* set nonblocking */

    ptr = buf;
    while(ntowrite > 0)
    {
        errno = 0;
        nwrite = write(STDOUT_FILENO, ptr, ntowrite);
        fprintf(stderr, "nwrite = %d, errno = %d\n", nwrite, errno);

        if(nwrite > 0)
        {
            ptr += nwrite;
            ntowrite -= nwrite;
        }
    }

    clr_fl(STDOUT_FILENO, O_NONBLOCK);  /* clear nonblocking */
    exit(0);
}

结果

yjp@yjp-VirtualBox:~/apue/14advio$ fallocate -l 800m 800m.txt    创建一个大文件
yjp@yjp-VirtualBox:~/apue/14advio$ ls -l 800m.txt
-rw-rw-r-- 1 yjp yjp 838860800 529 16:05 800m.txt
yjp@yjp-VirtualBox:~/apue/14advio$ ./nonblockw < 800m.txt > temp.file 标准输出是普通文件
read 500000 bytes
nwrite = 500000, errno = 0    一次写
yjp@yjp-VirtualBox:~/apue/14advio$ ls -l temp.file    查看
-rw-rw-r-- 1 yjp yjp 500000 529 16:06 temp.file

yjp@yjp-VirtualBox:~/apue/14advio$ ./nonblockw < 800m.txt 2> stderr.out  输出至终端
yjp@yjp-VirtualBox:~/apue/14advio$ cat stderr.out
read 500000 bytes
nwrite = 9729, errno = 0
nwrite = 3589, errno = 0
nwrite = -1, errno = 11     出错
nwrite = -1, errno = 11
...

数千个write调用中,只有几十个是真正输出数据的,其余的则出错返回。这种形式的循环成为轮询,浪费CPU时间,可采用非阻塞描述符的I/O多路转接。

加锁和解锁一个文件区域的函数
为避免每次分配flock结构,然后又填入各项信息,可用如下函数。

#include "apue.h"
#include <fcntl.h>

int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
    /*
       struct flock
       {
        short l_type;
        off_t l_start;
        short l_whence;
        off_t l_len;
        pid_t l_pid;     returned with F_GETLK
       };
    */
    struct flock lock;

    lock.l_type = type;       /* F_RDLCK, F_WRLCK, F_UNLCK */
    lock.l_start = offset;    /* byte offset, relative to l_whence */
    lock.l_whence = whence;   /* SEEK_SET, SEEK_CUR, SEEK_END */
    lock.l_len = len;     /* bytes (0 means to EOF) */

    /*
        int fcntl(int filedes, int cmd, ...);
        cmd: F_GETLK, F_SETLK, F_SETLKW
    */
    return (fcntl(fd, cmd, &lock));
}

通用宏

#define read_lock(fd, offset, whence, len)\
    lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))
#define readw_lock(fd, offset, whence, len)\
    lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len))
#define write_lock(fd, offset, whence, len)\    
    lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))
#define writew_lock(fd, offset, whence, len)\
    lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))
#define un_lock(fd, offset, whence, len)\
    lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))

测试一个锁状态的函数

#include "apue.h"
#include <fcntl.h>

pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;
    lock.l_type = type;   /* F_RDLCK or F_WRLCK */
    lock.l_start = offset;  /* byte offset, relative to l_whence */
    lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
    lock.l_len = len;  /* bytes (0 means to EOF */

    if(fcntl(fd, F_GETLK, &lock) < 0)
        err_sys("fcntl error");

    if(lock.l_type == F_UNLCK)
        return (0);  /* false, region isn't locked by another proc */
    return (lock.l_pid); /* true, return pid of lock owner */
}

通用宏

#define is_read_lockable(fd, offset, whence, len)\
    (lock_test((fd), F_RDLCK, (offset), (whence), (len)) == 0)
#define is_write_lockable(fd, offset, whence, len)\
    (lock_test((fd), F_WRLCK, (offset), (whence), (len)) == 0)

死锁检测实例
如果两个进程相互等待对方持有并且锁定的资源时,则这两个进程就处于死锁状态。
子进程锁字节0,父进程锁字节1。然后,它们又都试图锁对方已经加锁的字节。使用了父、子进程同步例程,使得每个进程能够等待另一个进程获得它设置的第一把锁。

#include "apue.h"
#include <fcntl.h>

static void lockabyte(const char *name, int fd, off_t offset)
{
    if(writew_lock(fd, offset, SEEK_SET, 1) < 0)
        err_sys("%s: writew_lock error", name);
    printf("%s: got the lock, byte %ld\n", name, offset);
}

int main(void)
{
    int fd;
    pid_t pid;

    /* Create a file and write two bytes to it */
    if((fd = creat("templock", FILE_MODE)) < 0)
        err_sys("creat error");
    if(write(fd, "ab", 2) != 2)
        err_sys("write error");

    TELL_WAIT();

    if((pid = fork()) < 0)
        err_sys("fork error");
    else if(pid == 0)
    {
        lockabyte("child", fd, 0);
        TELL_PARENT(getppid());
        WAIT_PARENT();
        lockabyte("child", fd, 1);
    }
    else
    {
        lockabyte("parent", fd, 1);
        TELL_CHILD(pid);
        WAIT_CHILD();
        lockabyte("parent", fd, 0);
    }
    exit(0);
}

这里写图片描述
本实例中,检测到死锁时,子进程接收出错并返回。

锁的隐含继承和释放
1、锁与进程和文件两方面有关。
2、由fork产生的子进程不继承父进程锁设置的锁。
3、在执行exec后,新程序可以继承原执行程序的锁。

在文件整体上加锁
守护进程可用一把文件锁保证只有该守护进程的唯一副本正在运行。

#include <unistd.h>
#include <fcntl.h>

int lockfile(int fd)
{
    struct flock f1;

    f1.l_type = F_WRLCK;
    f1.l_start = 0;
    f1.l_whence = SEEK_SET;
    f1.l_len = 0;
    return (fcntl(fd, F_SETLK, &f1));
}
#define lockfile(fd) write_lock((fd), 0, SEEK_SET, 0)

确定是否支持强制性锁
建议性锁不能阻止对数据库文件有写权限的任何其他进程对数据库文件进行任意的写操作。如果仅仅是访问数据库,那么可使用建议性锁。
强制性锁使内核对每一个open、read和write系统调用都进行检查,检查调用进程对正在访问的文件是否违背了一把锁的作用。
对一个特定文件打开其设置组ID位并关闭其组执行位,则对该文件开启了强制性锁机制。

首先创建一个文件,并使强制性锁机制对其起作用。然后程序分裂为父进程和子进程。父进程对整个文件设置一把写锁,子进程则将该文件的描述符设置为非阻塞的,然后企图对该文件设置一把读锁,会导致出错返回,返回EACCESS或EAGAIN。接着,子进程将文件读、写位置调整到文件起点,并试图读该文件。如果系统提供强制性锁机制,则read返回EACESS或EAGAIN(因为该描述符是非阻塞的),否则read返回所读的数据。

#include "apue.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    int fd;
    pid_t pid;
    char buf[5];
    struct stat statbuf;
    if(argc != 2)
    {
        fprintf(stderr, "usage: %s filename\n", argv[0]);
        exit(1);
    }
    if((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
        err_sys("open error");
    if(write(fd, "abcdef", 6) != 6)
        err_sys("write error");

    /* turn on set-group-ID and turn off group-execute */
    if(fstat(fd, &statbuf) < 0)
        err_sys("fstat error");
    if(fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
        err_sys("fchmod error");

    TELL_WAIT();

    if((pid = fork()) < 0)
        err_sys("fork error");
    else if(pid > 0)
    {
        /* write lock entire file */
        if(write_lock(fd, 0, SEEK_SET, 0) < 0)
            err_sys("write_lock error");

        TELL_CHILD(pid);

        if(waitpid(pid, NULL, 0) < 0)
            err_sys("waitpid error");
    }
    else
    {
        WAIT_PARENT();  /* wait parent to set lock */

        set_fl(fd, O_NONBLOCK);

        /* first let's see what error we get if region is locked */
        if(read_lock(fd, 0, SEEK_SET, 0) != -1)  /* no wait */
            err_sys("child: read_lock succeeded");
        printf("read_lock of already-locked region returns %d\n", errno);

        /* now try to read the mandatory locked file */
        if(lseek(fd, 0, SEEK_SET) == -1)
            err_sys("lseek error");
        if(read(fd, buf, 2) < 0)
            err_ret("read failed (mandatory locking works)");
        else
            printf("read OK (no mandatory locking), buf = %2.2s\n", buf);
    }
    exit(0);
}   

这里写图片描述
可见系统不支持强制性锁。

检查描述符是否引用STREAMS设备
使用I_CANPUT来测试由第三个参数说明的优先级波段(本例中为0)是否可写。如果该ioctl执行成功,则它对所涉及的流并未作任何改变。

#include <stropts.h>
#include <unistd.h>

int isastream(int fd)
{
    return (ioctl(fd, I_CANPUT, 0) != -1);
}

测试此函数

#include <fcntl.h>
#include "apue.h"
#include <stropts.h>
#include <unistd.h>

int isastream(int fd)
{
    return (ioctl(fd, I_CANPUT, 0) != -1);
}


int main(int argc, char *argv[])
{
    int i, fd;

    for(i=1; i<argc; i++)
    {
        if((fd = open(argv[i], O_RDONLY)) < 0)
        {
            err_ret("%s: can't open", argv[i]);
            continue;
        }

        if(isastream(fd) == 0)
            err_ret("%s: not a stream", argv[i]);
        else
            err_msg("%s: stream device", argv[i]);
    }

    exit(0);
}

这里写图片描述

列表流中的模块名

#include "apue.h"
#include <fcntl.h>
#include <stropts.h>
#include <unistd.h>
#define FMNAMESZ 8 

int isastream(int fd)
{
    return (ioctl(fd, I_CANPUT, 0) != -1);
}

int main(int argc, char *argv[])
{
    int fd, i, nmods;
    /*
       struct str_list
       {
        int sl_nmods;   number of entries in array
        struct str_mlist *sl_modlist;  ptr to first element of array
       };
       struct str_mlist
       {
        char l_name[FMNAMESZ+1];  null terminated module name
       };
    */
    struct str_list list;

    if(argc != 2)
        err_quit("usage: %s <pathname>", argv[0]);

    if((fd = open(argv[1], O_RDONLY)) < 0)
        err_sys("can't open %s", argv[1]);
    if(isastream(fd) == 0)
        err_quit("%s is not a stream", argv[1]);

    /* Fetch number of modules 
       最后一个参数为0时,返回值是模块数*/
    if((nmods = ioctl(fd, I_LIST, (void *)0)) < 0)
        err_sys("I_LIST error for nmods");
    printf("#modules = %d\n", nmods);

    /* Allocate storage for all the module names */
    list.sl_modlist = calloc(nmods, sizeof(struct str_mlist));
    if(list.sl_modlist == NULL)
        err_sys("calloc error");
    list.sl_nmods = nmods;

    /* Fetch the module names */
    if(ioctl(fd, I_LIST, &list) < 0)
        err_sys("I_LIST error for list");

    /* Print the names 
       列表的最后一是项处于流底部的驱动程序*/
    for(i=1; i<=nmods; i++)
        printf(" %s: %s\n", (i == nmods) ? "driver" : "module", 
                list.sl_modlist++->l_name);

    exit(0);
}

这里写图片描述
这里写图片描述
没有按预期运行?

用getmsg将标准输入复制到标准输出

#include "apue.h"
#include <stropts.h>

#define BUFFSIZE 4096

int main(void)
{
    int n, flag;
    char ctlbuf[BUFFSIZE], datbuf[BUFFSIZE];
    struct strbuf ctl, dat;

    ctl.buf = ctlbuf;
    ctl.maxlen = BUFFSIZE;
    dat.buf = datbuf;
    dat.maxlen = BUFFSIZE;
    for(;;)
    {
        flag = 0;  /* return any message */
        /*
           int getmsg(int fileds, struct strbuf *restrict ctlptr, 
              struct strbuf *restrict dataptr, int *restrict flagptr);
           从流首读STREAMS消息。
           如果flagptr指向的整型单元的值是0,则返回流首读队列的下一个消息。
           如果下一个消息是高优先级消息,则返回时flagptr指向的整型单元设置为RS_HIPRI。
           如果只希望接收高优先级消息,则在调用getmsg前将flagptr指向的整型单元设置为RS_HIPRI。
           如果ctlptr是null,或cplptr->maxlen==-1,则消息的控制部分仍保留在流首读队列中,不处理。
           类似,若dataptr是null,或dataptr->maxlen==-1,那么消息的数据部分仍保留在流首读队列中,
           不处理。否则,按照缓冲区的容量取到消息中尽可能多的控制和数据部分,余下的仍留在队首,
           等待下次取用。
        */
        if((n = getmsg(STDIN_FILENO, &ctl, &dat, &flag)) < 0)
            err_sys("getmsg error");
        fprintf(stderr, "flag = %d, ctl.len = %d, dat.len = %d\n",
            flag, ctl.len, dat.len);
        if(dat.len == 0)
            exit(0);
        else if(dat.len > 0)
            if(write(STDOUT_FILENO, dat.buf, dat.len) != dat.len)
                err_sys("write error");
    }
}

编译后,提示getmsg未实现。

I/O多路转接

当从两个描述符读,仍用阻塞I/O时,可能长时间阻塞在一个描述符上,而另一个描述符虽由很多数据却不能得到及时处理。
如果使用两个进程,则可使每个进程都执行阻塞read,但操作什么时候终止?可采用信号,但使得程序复杂。
也可以使用一个进程中的两个线程,但却要处理线程之间的同步。
另一中方法时仍使用一个程序,但使用非阻塞I/O读数据,但需要轮询,浪费CPU时间。
还有一种技术称为异步I/O。当一个描述符已准备好I/O时,用一个信号通知,但无法确定哪个描述符已准备好I/O。
一种比较好的技术是使用I/O多路转接。先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好I/O时,该函数才返回。返回时,告诉进程哪些描述符已准备好I/O。

select

#include <sys/select.h>

int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds,
           fd_set *restrict exceptfds, struct timeval *restrict tvptr);

tvptr:愿意等待的时间
struct timeval
{
    long tv_sec;     seconds
    long tv_usec;    and microseconds
};
tvptr == NULL:永远等待。
tvptr->tv_sec == 0 && tvptr->tv_usec == 0:完全不等待。
tvptr->tv_sec != 0 || tvptr->tv_usec != 0:等待指定的秒数和微妙数。

readfs、writefds、exceptfds分别表示可读、可写、处于异常的描述符。
对fd_set类型的变量可用如下函数处理:
int FD_ISSET(int fd, fd_set *fdset);  fd在fdset中则返回非0,否则返回0
void FD_CLR(int fd, fd_set *fdset);   清除指定位
void FD_SET(int fd, fd_set *fdset);   设置指定位
void FD_ZERO(fd_set *fdset);          清除所有位

maxfdp1:最大描述符+1,因为描述符从0开始,相当于描述符数量。

返回:-1--出错;0--没有描述符准备好;正值--已经准备好的描述符数。

使用:
fd set readset, writeset;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_SET(0, &readset);
FD_SET(3, &readset);
FD_SET(1, &writeset);
FD_SET(2, &writeset);
select(4, &readset, &writeset, NULL, NULL);

poll

#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
返回:准备就绪的描述符数,超时返回0,出错返回-1
timeout:愿意等待的时间。-1--永远等待;0--不等待;>0:等待timeout毫秒

struct poolfd
{
    int fd;           file descriptor to check, or < 0 to ignore
    short events;     requested events
    short revents;    returned events
};

这里写图片描述
头4行测试读,中间3行测试写,后3行测试异常。

readn和writen函数
在已经读、写了一些数据出错,则这两个函数返回已传输的数据量,而非出错返回。

#include "apue.h"

/* read n bytes from a descriptor */
ssize_t readn(int fd, void *ptr, size_t n)
{
    size_t nleft;
    ssize_t nread;

    nleft = n;
    while(nleft > 0)
    {
        if((nread = read(fd, ptr, nleft)) < 0)
        {
            if(nleft == n)
                return -1;   /* error, return -1 */
            else
                break;  /* error, return amount read so far */
        }
        else if(nread == 0)
            break;   /* EOF */

        nleft -= nread;
        ptr += nread;
    }
    return (n - nleft);  /* return  >= 0*/
}

/* write n bytes to a descriptor */
ssize_t writen(int fd, const void *ptr, size_t n)
{
    size_t nleft;
    ssize_t nwritten;

    nleft = n;
    while(nleft > 0)
    {
        if((nwritten = write(fd, ptr, nleft)) < 0)
        {
            if(nleft == n)
                return -1;  /* error, return -1 */
            else 
                break;   /* error, return amount written so far */
        }
        else if(nwritten == 0)
            break;

        nleft -= nwritten;
        ptr += nwritten;
    }
    return (n - nleft);  /* return >= 0*/
}

用存储映射I/O复制文件
存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射,这就可以在不使用read和write的情况下执行I/O。
首先打开两个文件,然后调用fstat得到输入文件的长度。在位输入文件调用mmap和设置输出文件长度时都需要使用输入文件长度。调用lseek,然后写一个字节以设置输出文件长度。

#include "apue.h"
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char *argv[])
{
    int fdin, fdout;
    void *src, *dst;
    struct stat statbuf;

    if(argc != 3)
        err_quit("usage: %s <fromfile> <tofile>", argv[0]);

    if((fdin = open(argv[1], O_RDONLY)) < 0)
        err_sys("can't open %s for reading", argv[1]);

    if((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
        err_sys("can't creat %s for writing", argv[2]);

    if(fstat(fdin, &statbuf) < 0)  /* need size of input file */
        err_sys("fstat error");

    /* set size of ouput file */
    if(lseek(fdout, statbuf.st_size-1, SEEK_SET) == -1)
        err_sys("lseek error");
    if(write(fdout, "", 1) != 1)
        err_sys("write error");

    /*
       void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);
       addr:指定映射存储器区的起始地址,0表示系统选择该映射区的起始地址,同时也是函数的返回。
       filedes:指定要映射文件的描述符,映射前需打开。
       len:映射的字节数
       off:要映射字节在文件中的起始偏移量
       prot:对映射存储区的保护要求,PROT_READ--映射区可读;PROT_WRITE--映射区可写
       flag:影响存储区的多种属性,MAP_SHARED--存储操作相当于对该文件的write
    */
    if((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED)
        err_sys("mmap error for input");

    if((dst = mmap(0, statbuf.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, 
            fdout, 0)) == MAP_FAILED)
        err_sys("mmap error for output");

    memcpy(dst, src, statbuf.st_size); /* does the file copy */
    exit(0);
}

这里写图片描述

本书全面介绍了UNIX系统的程序设计界面—系统调用界面和标准C库提供的许多函数。 本书的前15章着重于理论知识的阐述,主要内容包括UNIX文件和目录、进程环境、进程控制、 进程间通信以及各种I/O。在此基础上,分别按章介绍了多个应用实例,包括如何创建数据库函数库, PostScript 打印机驱动程序,调制解调器拨号器及在伪终端上运行其他程序的程序等。 本书内容丰富权威, 概念清晰精辟,一直以来被誉为UNIX编程的“圣经”,对于所有UNIX程序员—无论是初学者还是专家级人士 —都是一本无价的参考书籍。 目 录 译者序 译者简介 前言 第1章 UNIX基础知识 1 1.1 引言 1 1.2 登录 1 1.2.1 登录名 1 1.2.2 shell 1 1.3 文件和目录 2 1.3.1 文件系统 2 1.3.2 文件名 2 1.3.3 路径名 2 1.3.4 工作目录 4 1.3.5 起始目录 4 1.4 输入和输出 5 1.4.1 文件描述符 5 1.4.2 标准输入、标准输出和标准 出错 5 1.4.3 不用缓存的I/O 5 1.4.4 标准I/O 6 1.5 程序和进程 7 1.5.1 程序 7 1.5.2 进程和进程ID 7 1.5.3 进程控制 7 1.6 ANSI C 9 1.6.1 函数原型 9 1.6.2 类属指针 9 1.6.3 原始系统数据类型 10 1.7 出错处理 10 1.8 用户标识 11 1.8.1 用户ID 11 1.8.2 组ID 12 1.8.3 添加组ID 12 1.9 信号 12 1.10 UNIX时间值 14 1.11 系统调用和库函数 14 1.12 小结 16 习题 16 第2章 UNIX标准化及实现 17 2.1 引言 17 2.2 UNIX标准化 17 2.2.1 ANSI C 17 2.2.2 IEEE POSIX 18 2.2.3 X/Open XPG3 19 2.2.4 FIPS 19 2.3 UNIX实现 19 2.3.1 SVR4 20 2.3.2 4.3+BSD 20 2.4 标准和实现的关系 21 2.5 限制 21 2.5.1 ANSI C限制 22 2.5.2 POSIX限制 22 2.5.3 XPG3限制 24 2.5.4 sysconf、pathconf 和fpathconf 函数 24 2.5.5 FIPS 151-1要求 28 2.5.6 限制总结 28 2.5.7 未确定的运行时间限制 29 2.6 功能测试宏 32 2.7 基本系统数据类型 32 2.8 标准之间的冲突 33 2.9 小结 34 习题 34 第3章 文件I/O 35 3.1 引言 35 3.2 文件描述符 35 3.3 open函数 35 3.4 creat函数 37 3.5 close函数 37 3.6 lseek函数 38 3.7 read函数 40 3.8 write函数 41 3.9 I/O的效率 41 3.10 文件共享 42 3.11 原子操作 45 3.11.1 添加至一个文件 45 3.11.2 创建一个文件 45 3.12 dup和dup2函数 46 3.13 fcntl函数 47 3.14 ioctl函数 50 3.15 /dev/fd 51 3.16 小结 52 习题 52 第4章 文件和目录 54 4.1 引言 54 4.2 stat, fstat和lstat函数 54 4.3 文件类型 55 4.4 设置-用户-ID和设置-组-ID 57 4.5 文件存取许可权 58 4.6 新文件和目录的所有权 60 4.7 access函数 60 4.8 umask函数 62 4.9 chmod和fchmod函数 63 4.10 粘住位 65 4.11 chown, fchown和 lchown函数 66 4.12 文件长度 67 4.13 文件截短 68 4.14 文件系统 69 4.15 link, unlink, remove和rename 函数 71 4.16 符号连接 73 4.17 symlink 和readlink函数 76 4.18 文件的时间 76 4.19 utime函数 78 4.20 mkdir和rmdir函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值