开发者社区> 毛毛虫的爹> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

VxWorks程序指南

简介: <p><span style="font-family:Arial">*******************************************<br> 一、任务<br> *******************************************<br> 任务状态:挂起、阻塞、就绪、睡眠。<br> 任务可以在任何一种状态被删除。<br> Wind内核里有2
+关注继续查看

*******************************************
一、任务
*******************************************
任务状态:挂起、阻塞、就绪、睡眠。
任务可以在任何一种状态被删除。
Wind内核里有256种优先级,0最高,255为最低。
任务调度控制函数
kernelTimeSlice() 控制轮转调度
taskPrioritySet() 改变任务优先级
taskLock()    禁止任务调度
taskUnlock()   允许任务调度

当任务访问一个可能会被中断服务程序访问的变量或者数据结构时,可以使用intLock()实现互斥。
通过taskLock()、taskUnlock()抢占上锁可禁止优先级的抢占,中断服务程序仍然可以执行。
所有应用任务的优先级应该在100-250之间;但是驱动程序支持的任务(与中断服务程序关联的任务)优先级能够位于51-99。
任务堆栈的空间分配:为了避免堆栈溢出和破坏任务堆栈可使用下列方法:
在最初分配堆栈空间时,分配比预先估计大一些的空间;然后周期性地调用checkStack()函数监控;若可以安全使用更小的空间,将修改分配空间的尺寸。
一般vxWorks使用任务ID号来定位任务,约定ID号为0的表示任务调用。
vxWorks操作系统不需要惟一的任务名,但为了避免混乱,建议使用惟一的任务名。
vxWorks操作系统的任务命名规则:所有从目标机启动的任务以字母t开头,而从主机启动的任务以字母u开头。

任务创建函数
taskSpawn()   创建并激活一个新任务
taskInit()    初始化一个新任务
taskActivate()激活一个初始化任务
任务名和ID函数
taskName()    得到与任务号相关的任务名
taskNameToId()寻找与任务名相关的任务ID
taskIdSelf()  获得调用任务的ID号
taskIdVerify()检查一个特定任务的存在性
在任务创建时,若需执行下列操作则要包括VX_FP_TASK选项:
1实行浮点操作2调用返回浮点值的函数3调用以浮点值为参数的函数
任务选项函数
taskOptionGet() 检查任务选项
taskOptionSet() 设置任务选项 目前只有VX_UNBREAKABLE选项可以被修改
任务信息:由于任务状态是动态的,除非知道任务处于挂起,否则不能获得当前信息
taskIdListGet() 用ID填充一组所有激活的任务
taskInfoGet()   得到任务的信息
taskPriorityGet() 查看任务的优先级
taskRegsGet()  检查任务寄存器(不能使用单前任务时)
taskRegsSet()  设置任务寄存器(不能使用单前任务时)
taskIsSuspended()检查任务是否处于挂起状态
taskIsReady()  检查任务是否处于就绪状态
taskTcb()      获得任务控制块的指针
任务删除函数
exit()    终止任务调用,释放内存
taskDelete()终止指定任务,释放内存
taskSafe() 保护调用任务免于删除
taskUnsafe() 解除任务删除保护

下面的代码表明了如何使用taskSafe()函数和taskUnsafe()函数去保护一个临界代码区域
taskSafe();
semTake (semId,WAIT_FOREVER);/*阻塞直至信号量可用*/
.../*临界区域代码*/
semGive(semId); /*释放信号量*/
taskUnsafe();

任务控制函数
taskSuspend() 挂起任务,用来冻结任务状态并进行检查
taskResume()  恢复任务执行
taskRestart() 重新启动任务
taskDelay()   延迟任务,延迟单位为“tick”
nanosleep()   延迟任务,延迟单位为纳秒

下面的代码无需考虑时钟速率,将使任务延时半秒:
taskDelay(sysClkRateGet()/2);/*函数sysClkRateGet()返回系统时钟的速率,单位tick/秒*/
taskDelay()函数把作为调用者的任务移动到相同优先级队列的尾部。
特别是,当调用taskDelay(0)时,将会把CPU交给系统中其他相同优先级任务。
taskDelay(NO_WAIT);/*允许其他相同优先级的任务运行*/
延时为零时,只能调用taskDelay()函数,函数nanosleep()中的延时参数禁止为零。

任务扩展函数
为了允许其他相关的任务函数加入到系统中去,vxWorks提供“hook”函数。该函数允许任务在创建
、删除和上下文交换时调用附加的函数。
taskCreateHookAdd()  增加一个在每个任务创建时都调用的函数
taskCreateHookDelete() 删除一个以前加入的任务创建函数
taskSwitchHookAdd()  增加一个在每个任务切换时都调用的函数
taskSwitchHookDelete() 删除一个以前加入的任务切换函数
taskDeleteHookAdd()  增加一个在每个任务被删除时都调用的函数
taskDeleteHookDelete() 删除一个以前加入的任务删除函数
*******************************************
二、任务通信
*******************************************
大部分vxWorks函数使用下列重入技术:1动态堆栈变量2被信号保护的全局和静态变量2任务变量
在编写被多个任务内务调用的应用代码时,建议使用这些技术。
vxWorks的任务间通信:
1共享内存,数据的简单共享
2信号量,基本的互斥和同步
3Mutexe和条件变量,使用POSIX接口时互斥与同步操作
4消息队列和管道,同一个CPU内任务间消息的传递
5sockets和远程任务调用,任务间透明的网络通信
6信号,用于异常处理。

信号量控制函数
semBCreate() 分配并初始化一个二进制信号量
semMCreate() 分配并初始化一个互斥信号量
semCCreate() 分配并初始化一个计数器信号量
semDelete()  终止并释放一个信号量
semTake()    获取一个信号量
semGive()    提供一个信号量
semFlush()   解锁所有正在等待信号量的任务

二进制信号量
使用二进制信号量能够满足两种任务的协调需要:互斥和同步。
互斥的实现:
创建一个二进制信号量,初始可用(SEM_FULL)。
当任务访问资源时,首先必须获得信号量。
只要任务持有信号量,其他所有需要访问该资源的任务将被阻塞。
#include "vxWorks.h"
#include "semLib.h"
SEM_ID semMutex;
semMutex = semBCreate(SEM_Q_PRIORITY,SEM_FULL);
...
semTake (semMutex,WAIT_FOREVER);
.../*临界区域,任何时候仅单个任务可以访问*/
semGive(semMutex);
同步的实现:
初始时信号量不可用(SEM_EMPTY)。
任务或中断服务程序通过释放信号量来表明事件的发生。
调用semTake()函数提取信号量的其他任务处于等待状态,直至事件发生,并释放信号量。
#include "vxWorks.h"
#include "semLib.h"
#include "arch/arch/ivarch.h"/*用结构体类型代替arch*/
SEM_ID syncSem;
init(int someIntNum)
{
 /*连接中断服务函数*/
 intConnect(INUM_TO_IVEC(someIntNum),evertInterruptSvcRout,0);
 /*建立信号量*/
 syncSem = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
 /*发起用于同步的任务*/
 taskSpawn("sample",100,0,20000,task1,0,0,0,0,0,0,0,0,0,0);
}

task1(void)
{
 ...
 semTake(syncSem, WAIT_FOREVER);/*等待事件发生*/
 printf("task 1 got the semaphore\n");
 .../*启动事件*/
}
eventInterruptSvcRout(void)
{
 ...
 semGive(syncSem);/*让任务1启动事件*/
 ...
}

互斥信号量:
互斥信号量是一种用于解决内在互斥问题的特殊的二进制信号量,包括优先级倒置,删除安区以及资源的递归访问。
互斥信号量的基本行为与二进制信号量一致,不同之处如下:
1它仅用于互斥2它仅能由提取它(即调用semTake())的任务释放3不能在中断服务程序中释放4semFlush()函数操作非法。
当一个高优先级任务需要等待一段不确定的时间,让低优先级任务完成时(如低优先级通过信号量占有高优先级任务需要的资源),
需要发生优先级倒置。
下例用优先级继承算法创建了一个互斥信号量:
semId = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE);/*使用优先级继承选项SEM_INVERSION_SAFE的信号量必须选择优先级顺序队列*/
下例是为防止应用临界资源的任务被意外删除的一个互斥信号量
semId = semMCreate(SEM_Q_FIFO | SEM_DELETE_SAFE);
计数信号量应用:计数信号量适用于保护多份复制的资源:如可以使用一个初始值为5的计数器信号量来协调5个磁带驱动器工作。
Wind消息队列控制
msgQCreate() 分配并初始化一个消息队列
msgQDelete() 终止并释放一个消息队列
msgQSend() 向一个消息队列发送消息
msgQReceive() 从一个消息队列接收消息
/* In this example, task t1 creates the message queue and sends a message
 * to task t2. Task t2 receives the message from the queue and simply
 * displays the message.
 */
 
/* includes */
#include "vxWorks.h"
#include "msgQLib.h"
 
/* defines */
#define MAX_MSGS (10)
#define MAX_MSG_LEN (100)
 
MSG_Q_ID myMsgQId;
 
task2 (void)
    {
    char msgBuf[MAX_MSG_LEN];
 
    /* get message from queue; if necessary wait until msg is available */
    if (msgQReceive(myMsgQId, msgBuf, MAX_MSG_LEN, WAIT_FOREVER) == ERROR)
        return (ERROR);
 
    /* display message */
    printf ("Message from task 1:\n%s\n", msgBuf);
    }
 
#define MESSAGE "Greetings from Task 1"
task1 (void)
    {
    /* create message queue */
    if ((myMsgQId = msgQCreate (MAX_MSGS, MAX_MSG_LEN, MSG_Q_PRIORITY)) 
        == NULL)
        return (ERROR);
 
    /* send a normal priority message, blocking if queue is full */
    if (msgQSend (myMsgQId, MESSAGE, sizeof (MESSAGE), WAIT_FOREVER,
                  MSG_PRI_NORMAL) == ERROR)
        return (ERROR);
    }
   
实时系统经常构造成任务的客户机/服务器使用模式。
vxWorks事件是一种在任务和中断处理程序间,或任务和vxWorks结构体间的通信方式。
看门狗定时器函数调用
wdCreate()  分配并初始化一个看门狗定时器
wdDelete()  终止并释放一个看门狗定时器
wdStart()   启动一个看门狗定时器
wdCancel()  取消当前的一个技术的看门狗定时器
/* Creates a watchdog timer and sets it to go off in 3 seconds.*/
 
/* includes */
#include "vxWorks.h"
#include "logLib.h"
#include "wdLib.h"
 
/* defines */
#define  SECONDS (3)
 
WDOG_ID myWatchDogId;
task (void)
{
    /* Create watchdog */
    if ((myWatchDogId = wdCreate( )) == NULL)
        return (ERROR);
 
    /* Set timer to go off in SECONDS - printing a message to stdout */
    if (wdStart (myWatchDogId, sysClkRateGet( ) * SECONDS, logMsg, 
                 "Watchdog timer just expired\n") == ERROR)
        return (ERROR);
    /* ... */
}
*******************************************
三、中断
*******************************************
中断处理函数
intConnect()  设置中断处理的C程序
intContext()  如果是从中断级调用,返回真
intCount()    获得当前中断嵌套深度
intLevelSet(() 设置处理器的中断屏蔽级
intLock()     禁止中断
intUnlock()   重新允许中断
intVecBaseSet() 设置向量基地址
intVecBaseGet() 得到向量基地址
intVecSet()   设置异常向量
intVecGet()   获得异常向量
中断服务程序禁止调用浮点协处理器函数。因为由intConnect()函数建立的中断驱动代码不能保存和恢复浮点寄存器。
*******************************************
四、输入/输出系统(1)
*******************************************
文件描述符(fd)的默认值:0=标准输入设备 1=标准输出设备 2=标准错误输出设备
下例将调用此函数的任务(ID = 表示该任务本身)标准输出文件(fd =1 )重定向到一个已打开的文件(fd = fileFd).
ioTaskStdSet(0,1,fileFd);
fd = open("name",flags,mode);
文件访问方式标志(flags)
O_RDONLY  0(hex)  以只读方式打开文件
O_WRONLY  1(hex)  以只写方式打开文件
O_RDWR    2(hex)  以读写方式打开文件
O_CREAT   200(hex)建立一个新文件
O_TRUNC   400(hex)删除该文件

基于多文件描述符的挂起操作:选择功能The Select Facility
选择功能中的宏函数
FD_ZERO  将所有标志位设置为“0”
FD_SET   对指定文件描述符的标志位设置为“1”
FD_CLR   对指定文件描述符的标志位设置为"0"
FD_ISSET 如果指定标志位值为“1”,则返回“1”;否则返回“0”

/* selServer.c - select example 
 * In this example, a server task uses two pipes: one for normal-priority 
 * requests, the other for high-priority requests. The server opens both 
 * pipes and blocks while waiting for data to be available in at least one 
 * of the pipes. 
 */
 
#include "vxWorks.h"
#include "selectLib.h"
#include "fcntl.h"
 
#define MAX_FDS 2
#define MAX_DATA 1024
#define PIPEHI   "/pipe/highPriority"
#define PIPENORM "/pipe/normalPriority"
 
/************************************************************************
* selServer - reads data as it becomes available from two different pipes 
*
* Opens two pipe fds, reading from whichever becomes available. The 
* server code assumes the pipes have been created from either another 
* task or the shell. To test this code from the shell do the following:
*  -> ld < selServer.o
*  -> pipeDevCreate ("/pipe/highPriority", 5, 1024)
*  -> pipeDevCreate ("/pipe/normalPriority", 5, 1024)
*  -> fdHi = open   ("/pipe/highPriority", 1, 0)
*  -> fdNorm = open ("/pipe/normalPriority", 1, 0)
*  -> iosFdShow
*  -> sp selServer
*  -> i
 
* At this point you should see selServer's state as pended. You can now
* write to either pipe to make the selServer display your message.
*  -> write fdNorm, "Howdy", 6
*  -> write fdHi, "Urgent", 7
*/
 
STATUS selServer (void)
    {
    struct fd_set readFds;      /* bit mask of fds to read from */
    int      fds[MAX_FDS];      /* array of fds on which to pend */
    int      width;             /* number of fds on which to pend */
    int      i;                 /* index for fd array */
    char     buffer[MAX_DATA];  /* buffer for data that is read */

/* open file descriptors */
    if ((fds[0] = open (PIPEHI, O_RDONLY, 0)) == ERROR)
        {
        close (fds[0]);
        return (ERROR);
        }
    if ((fds[1] = open (PIPENORM, O_RDONLY, 0)) == ERROR)
        {
        close (fds[0]);
        close (fds[1]);
        return (ERROR);
        }
/* loop forever reading data and servicing clients */
    FOREVER
        {
        /* clear bits in read bit mask */
        FD_ZERO (&readFds);
 
/* initialize bit mask */
        FD_SET (fds[0], &readFds);
        FD_SET (fds[1], &readFds);
        width = (fds[0] > fds[1]) ? fds[0] : fds[1];
        width++;
 
/* pend, waiting for one or more fds to become ready */
    if (select (width, &readFds, NULL, NULL, NULL) == ERROR)
        {
        close (fds[0]);
        close (fds[1]);
        return (ERROR);
        }
 
/* step through array and read from fds that are ready */
    for (i=0; i< MAX_FDS; i++)
        {
        /* check if this fd has data to read */
        if (FD_ISSET (fds[i], &readFds))
            {
            /* typically read from fd now that it is ready */
            read (fds[i], buffer, MAX_DATA);
            /* normally service request, for this example print it */
            printf ("SELSERVER Reading from %s: %s\n", 
                    (i == 0) ? PIPEHI : PIPENORM, buffer);
            }
        }
    }
    }
*******************************************
四、输入/输出系统(2)
*******************************************
异步输入输出操作(AIO)
能够在执行普通的内部处理时,同时执行输入/输出操作。
异步输入输出操作函数
aioPxLibInit()   初始化AIO功能函数库
aioShow()        显示AIO功能要求
aio_read()       初始化异步读操作
aio_write()      初始化异步写操作
aio_listio()     初始化个数最多为LIO_MAX的AIO功能请求
aio_error()      在一个AIO操作中寻找错误状态值
aio_return()     在一个已完成的AIO操作中寻找返回状态值
aio_cancel()     取消一个AIO操作
aio_suspend()    等待一个AIO操作完成、被中断或超时

vxWorks操作系统中的驱动程序
ttyDrv       终端设备驱动程序
ptyDrv       伪终端设备驱动程序
pipeDrv      管道设备驱动程序
memDrv       伪存储设备驱动程序
nfsDrv       NFS系统客户机驱动程序
netDrv       用于远程文件访问的网络驱动程序
ramDrv       用于创建RAM存储盘的RAM驱动程序
scsiLib      SCSI接口库

串行I/O设备(终端和伪终端设备)
vxWorks操作系统中的I/O设备是一种缓冲型的串行字节流设备。每个设备都有一个环形缓冲区用于输入和输出操作。
下例是在tty设备上设置除了OPT_MON_TRAP功能外的所有功能;
status = ioctl (fd, FIOSETOPTIONS, OPT_TERMINAL & ~OPT_MON_TRAP);
tty设备的可选项
OPT_LINE      选择线形传输模式
OPT_ECHO      向同一输出通道回应输入的字符
OPT_CRMOD     将输入的RETURN字符翻译成NEWLINE标志;将输出的NEWLINE标志翻译成RETURN-LINEFEED字符
OPT_TANDEM    响应软件流量控制字符CTRL+Q和CTRL+S(XON and XOFF)
OPT_7_BIT     从所有输入字节中取出最高位
OPT_MON_TRAP  使特殊ROM软中断监控程序字符有效,默认为CTRL+X
OPT_ABORT     使特殊目标机SHELL程序终止字符,默认为CTRL+Z
OPT_TERMINAL  将上述选项位设为“1”
OPT_RAW       不设置上述选项位

tyLib文件支持的I/O操作控制函数
FIOBAUDTATE    对指定参数设置传输速率
FIOCANCEL      取消一次读/写操作
FIOFLUSH       丢弃输入和输出缓冲区中的所有数据
FIOGETNAME     获取指定文件描述符对应的文件名
FIOGETOPTIONS  返回当前设备选项字内容
FIONREAD       获取输入缓冲区中的未读字节数
FIONWRITE      获取输出缓冲区中的字节数
FIOSETOPTIONS  设置设备选项字的内容

管道设备
一个任务可以向管道写信息,其他任务可以读取这些信息。
创建管道设备
status = pipeDevCreate("/pipe/name",maxMsgs, maxLength);
pipeDrv文件支持的I/O操作控制函数
FIOFLUSH     丢弃管道中所有信息
FIOGETNAME   获取指定文件描述符对应的管道名
FIONMSGS     获取管道中的信息数目
FIONREAD     获取管道中第一条信息的字节数

伪存储设备(memDrv)
memDrv驱动程序允许I/O系统访问存储器像访问伪I/O设备一样。
memDrv文件支持的I/O操作控制函数
FIOSEEK   在文件中设置当前字节偏移量
FIOWHERE  返回当前在文件中的位置

网络文件系统(NFS)设备
网络文件系统设备可以通过NFS协议存取远程主机上的文件。
下例函数在主机"mars"中安装名为“/usr”的系统,本地系统名为“/vxusr”。
nfsMount("mars","/usr","/vxusr");
nfsDrv文件支持的I/O控制函数
FIOFSTATGET    获取文件状态信息
FIOGETNAME     获取指定文件描述符对应的文件名
FIONREAD       获取文件中未读取字节数
FIOREADDIR     读取下一个目录入口信息
FIOSEEK        在文件中设置当前字节偏移量
FIOSYNC        向远程NFS文件刷新数据
FIOWHERE       返回文件中当前字节偏移量

非NFS网络设备
利用远程外壳协议(RSH)或文件传输协议(FTP)访问远程主机上的文件。这些网络设备需要netDrv驱动程序支持。

I/O系统的功能是将用户的I/O请求与相应驱动程序中的相应操作函数相连。I/O系统通过维护一个包括每个驱动程序中每个操作函数的地址表来完成上述工作。
drvnum = iosDrvInstall(xxCreat,0,xxOpen,0,xxRead,xxWrite,xxIoctl);

块存取设备
块存取设备的驱动程序不是与I/O系统直接相连,而是与文件系统相互作用,文件系统再与I/O系统相连.
块存取设备驱动程序中通用的操作包括:
1初始化硬件2分配并初始化数据结构3建立信号量4初始化中断向量5允许中断操作
文件系统将自己作为驱动程序装入驱动程序表中,并且通过使用存放于块存取设备结构BLK_DEV(直接访问的块存取设备)或SEQ_DEV(顺序访问的块存取设备)中的函数地址调用实际的驱动程序。

驱动程序支持库(对用户编写驱动程序有帮助)
errnoLib    错误状态函数库
ftpLib      ARPA文件传输协议函数库
ioLib       I/O接口函数库
iosLib      I/O系统函数库
intLib      中断支持函数库
remLib      远程命令函数库
rngLib      环形缓冲区子程序函数库
ttyDrv      终端驱动程序函数库
wdLib       看门狗定时器函数库

*******************************************
五、本地文件系统
*******************************************
dosFs文件系统:适用于块存取设备(磁盘)的实时操作,与MS-DOS文件系统兼容;
rawFs文件系统:提供了一种简单的原始文件系统。该文件系统将整个磁盘当作一个单独的大文件;
tapeFs文件系统: 适用于不使用标准文件或目录结构的磁带设备。实际上将磁带盘当作一个原始设备并将整个磁带盘当作一个大文件;
cdromFs文件系统:允许应用程序从按照ISO9660标准文件系统格式化的CD-ROM设备上读取数据;
TSFS目标服务器文件系统:通过使用Tornado软件中的目标服务器,使得目标机可以访问主机系统的文件。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
DataWorks百问百答65:如何获取数据源网络环境及独享资源绑定的专有网络信息?
如何获取数据源网络环境及独享资源绑定的专有网络信息
3303 0
DataWorks开通指南
开通地址:https://common-buy.aliyun.com/?commodityCode=dide_create_post#/buyDataWorks老客户选择Region,点击开通 完成此步骤,调度任务和DataWorks基础版功能已经开通,基础版功能如下:详细功能:https://help.
2093 0
离线计算中的幂等和DataWorks中的相关事项
概念 幂等这个词在软件研发中经常被提到。比如消息发送时不应该同时给同个用户推送多次相同的消息,针对同一笔交易的付款也不应该在重试过程中扣多次钱。曾见过一个案例,有个对于一个单据的确认模块没有考虑到幂等性,导致对应的单据有两条确认记录。
3084 0
vxWorks应用程序加载的另一种办法
<p>现在我们的工作中,应用程序一般都是和BSP联编,然后将vxworks_rom.bin烧到班子里。在BSP启动后,调用应用程序的函数的。</p> <p> </p> <p>但是这样有个问题,就是应用程序和BSP结合的太紧密了。BSP开发者得将BSP代码给应用程序开发者,或者应用程序开发者得将应用程序编译后的.a文件给BSP开发者,才能完成程序的升级!</p> <p> </p> <p>那么下面
2662 0
vxWorks驱动架构
<div>Vxworks内核驱动基本结构:</div> <div>三张表:1. 系统设备表  2. 系统驱动表 3. 文件描述符表</div> <div><br></div> <div>    Vxworks 内部对每个设备使用DEV_HDR 数据结构进行表示:</div> <div>Typedef struct</div> <div>{</div> <div>   DL_NO
1736 0
vxWorks 命令
<div class="bct fc05 fc11 nbw-blog ztag"> <p style="TEXT-ALIGN: left; TEXT-INDENT: -21.3pt; MARGIN: 0cm 0cm 0pt 21.3pt; mso-char-indent-count: -1.77;" align="left"><font color="#000000"><strong><sp
2766 0
纠结于数学,还能继续程序员的目标吗?
【来信】  我是今年毕业的大学生,杭州的一所专科学校,学的是计算机软件,从大三开始实习到现在一直在做as3,网页游戏客户端的开发,可是最近遇到一些算法的东西,像A*之类的,我突然发现我都不适合当程序员了,我感觉我的数学跟不上,然后看一些东西都看不进去,感觉都看不懂。我不知道我现在应该怎么办  【回复】  怎么能这样轻易否定自己呢?根据你的叙述,我觉得你还很充实地度过自己两年多的大学生活,应该也
964 0
+关注
毛毛虫的爹
好好学习,天天向上
文章
问答
文章排行榜
最热
最新
相关电子书
更多
小程序 大世界
立即下载
15分钟打造你自己的小程序
立即下载
《15分钟打造你自己的小程序》
立即下载