![](https://img-blog.csdnimg.cn/20201014180756923.png?x-oss-process=image/resize,m_fixed,h_64,w_64)
Linux系统编程
包括静态/动态库制作,makefile基础,进程学习,进程间通信,信号,守护进程,线程学习和多线程API等等
Mango酱
这个作者很懒,什么都没留下…
展开
-
04Linux下C语言锁的学习之条件变量配合锁实现生产者与消费者模型
04Linux下C语言锁的学习之条件变量配合锁实现生产者与消费者模型概述:生产者与消费者模型比较简单,就是生产者不断往一个共享队列(大小不断变化)中生产数据,消费者不断消费。由于是不断也就是轮询,为防止占用过高CPU所以需要使用条件变量,为防止队列中的数据混乱所以需要锁。条件变量不懂的去看我上一篇文章即可。锁大家基本都知道为什么要使用了吧。1 消费者和生产者的代码基本步骤消费者:1 访问数据之前先加锁2 判断数据是否存在,存在则消费,不存在则阻塞在条件变量,并且解锁等待生产者生产。3 访问消费原创 2020-12-13 20:19:15 · 241 阅读 · 0 评论 -
03Linux下C语言锁的学习之条件变量
03Linux下C语言锁的学习之条件变量1 为何要使用条件变量?通过上两篇对互斥锁和读写锁的学习,我们知道锁是用来保证共享数据的访问混乱,实现多线程同步。但是你有没有想过,当这个共享数据不是固定的一个,而是会变化的呢。何为变化,就是说不断有人生产数据,不断有人消费数据,也就是我们经典的生产者消费者模型。而非上两篇例子中共享数据为固定的一个全局共享变量。按照我们前两篇的做法,各个线程使用锁把这个装着共享数据的容器(size不断变化)锁住,然后不断轮询访问这个容器(假设为队列),即类似下面的代码:原创 2020-12-13 19:58:39 · 343 阅读 · 0 评论 -
02Linux下C语言锁的学习之Linux下的读写锁
02C/C++锁的学习之Linux下的读写锁概述:下面的锁的意思均是代表读写锁。读写锁的特性:1)若一把锁被一个线程以读方式锁住,当其它线程以读方式上锁的话,那么可以上锁成功。2)若一把锁被一个线程以写方式锁住,当其它线程以读或者写方式上锁的话,都会被阻塞。要注意的是:多个线程同时请求锁时,请求读方式的线程会被放在请求写方式的线程后面;若不是同时请求上锁,读方式先请求的则先等已经上锁的线程处理后再处理(当然如果该线程以读方式上锁可以同时进行),即使后面再有写请求也要等其完成才能上锁。原创 2020-12-10 12:42:39 · 510 阅读 · 0 评论 -
01Linux下C语言锁的学习之Linux下的互斥锁
01C/C++锁的学习之Linux下的互斥锁概述:为什么需要锁,因为当我们多个线程访问全局变量时,同时操作的话可能存在问题,例如小明和小红同时去拿家里的银行账号里的钱,小明看到时为100块,小红也是,但是小明先拿到,若不加锁,那么小红再拿100的时候就会报错。所以必须在小明操作的时候限制小红操作。不考虑相应初始化下,实际上所有的锁的使用步骤都是上锁,操作,解锁。锁的粒度:即你上锁和解锁中间的操作,步骤越少越好。注意:下面的互斥锁和题目一样,只是针对于Linux下使用,Windows下有自己相应的原创 2020-12-08 21:47:54 · 838 阅读 · 0 评论 -
14LinuxC线程学习之查看当前pthread库版本和线程使用的注意事项(重要)
1 查看当前pthread库版本1)查看当前pthread库版本。getconf GNU_LIBPTHREAD_VERSION2)NPTL名词了解:NPTL实现机制(POSIX),Native POSIX Thread Library3)使用线程库时gcc链接 –lpthread库。2 线程使用注意事项1)主线程退出其他线程不退出,主线程应调用pthread_exit。2)避免僵尸线程的相关方法(函数)。pthread_join、pthread_detach、pthread_c原创 2021-01-07 21:06:01 · 1375 阅读 · 1 评论 -
13LinuxC线程学习之利用pthread_create设置线程分离属性和相关属性解释
1 线程属性1)本节作为指引性介绍,linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。2)设置线程分离的方法有两种,一个是本节的调用pthread_create的参数设置,另一个是上几节的detach。C语言设置分离建议前者,因为不需要每次都调用detach。直接设置属性即可。看个人。下面给原创 2021-01-07 20:48:34 · 2873 阅读 · 0 评论 -
12LinuxC线程学习之进程、线程控制原语对比(即相应的函数比较)
1 控制原语对比进程 线程fork pthread_create -->创建exit/return pthread_exit/return -->退出wait pthread_join -->回收kill pthread_cancel -->杀死getpid pthread_self -->获取对应的进程id,线程id//注意:进程的wait,waitpid是不能回收兄弟进程,只能是父子关系的进程...原创 2021-01-07 19:57:59 · 135 阅读 · 0 评论 -
11LinuxC线程学习之pthread_equal函数
1 pthread_equal函数int pthread_equal(pthread_t t1, pthread_t t2);/* 功能:比较两个线程ID是否相等。 有可能Linux在未来线程ID pthread_t 类型被修改为结构体实现。*/原创 2021-01-07 19:53:36 · 943 阅读 · 0 评论 -
10LinuxC线程学习之pthread_detach函数,错误返回值分析及其案例
1 pthread_detach函数int pthread_detach(pthread_t thread); /* 功能: 1)实现线程分离,不再受主线程管理,由系统接任。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。 对比进程:进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。 对比pthread_create:也可使用pthread_create函数参2(线程属性)原创 2021-01-06 23:11:02 · 1704 阅读 · 0 评论 -
09LinuxC线程学习之pthread_cancel函数及其案例
1 pthread_cancel函数int pthread_cancel(pthread_t thread); /* 功能:杀死(取消)线程,其作用,对应进程中kill()函数。 成功:0;失败:错误号。 参1:要杀死线程的tid。*/2 pthread_cancel函数案例2.1 案例1#include <pthread.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h&g原创 2021-01-06 22:41:10 · 1182 阅读 · 2 评论 -
08LinuxC线程学习之pthread_join函数以及根据参2获取返回值的案例
1 pthread_join函数int pthread_join(pthread_t thread, void **retval); /* 功能:阻塞等待线程退出,获取线程退出状态。其作用,对应进程中 waitpid() 函数。 成功:0;失败:错误号。 参1:线程ID。 参2:存储线程结束状态。 注意:linux下回收子线程主要是回收pcb,而pcb保存着函数的返回值和其它相关信息。*/2 pthread_join函数使用案例#include <stdio.h>#inc原创 2021-01-05 22:14:18 · 653 阅读 · 0 评论 -
07LinuxC线程学习之pthread_exit函数和总结exit、return、pthread_exit,pthread_cancel各自退出效果和join,detach的作用
1 pthread_exit函数void pthread_exit(void *retval); /* 功能:退出当前子线程。与在某个函数中返回区别一下。 参1:retval表示线程退出状态,通常传NULL。*/2 简单总结exit、return、pthread_exit,pthread_join各自退出效果1)return:返回到调用者那里去。注意,在主线程退出时效果与exit,_exit一样。2)pthread_exit():退出当前子线程。注意:在主线程退出时,其它线程不会结束。同原创 2021-01-05 21:58:12 · 6608 阅读 · 1 评论 -
06LinuxC线程学习之线程间共享全局变量(对比进程间读时共享,写时复制)
注:1)父子进程共享的内容文章。https://blog.csdn.net/weixin_44517656/article/details/1094424242)测试父子进程用户区共享内容读时共享写时复制的习题(实际是对全局变量的说明)https://blog.csdn.net/weixin_44517656/article/details/1094429481 线程间共享全局变量和进程间读时共享,写时复制1)我们只需要记住: 线程间共享全局变量,而进程间读时共享,写时复制。更详原创 2021-01-05 20:37:16 · 930 阅读 · 0 评论 -
05LinuxC线程学习之练习循环创建多个子线程和线程传参大坑(可以对比之前循环创建多个子进程)
注:1)之前循环创建多个子进程的文章:https://blog.csdn.net/weixin_44517656/article/details/1094330602)wait回收子进程并且获取正常退出的返回值和异常退出的信号值的文章:https://blog.csdn.net/weixin_44517656/article/details/1094538893)waitpid回收指定子进程并分析回收pid出错原因:https://blog.csdn.net/weixin_445原创 2021-01-05 20:22:15 · 297 阅读 · 0 评论 -
04LinuxC线程学习之pthread_create创建线程
1 pthread_self函数pthread_t pthread_self(void); /* 功能:获取线程ID。其作用对应进程中 getpid() 函数。 返回值:成功:0; 失败:无! 两个进程间,线程ID允许相同。*/2 pthread_create函数int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);/*原创 2021-01-05 19:49:43 · 600 阅读 · 1 评论 -
03LinuxC线程学习之线程共享和非共享
1 线程共享和非共享1.1 线程共享资源1)文件描述符表。由于线程间共享进程间的内容,而文件描述符表在主线程的PCB当中,各个线程可以直接去请求访问,所以线程间通信就不需要像进程那样通过管道这些方式通信。2)每种信号的处理方式。即当某个信号发送到该进程时,该进程有多个线程,那么哪个线程捕捉到就谁处理。但mask是不共享的,所以可以通过这个指定某个线程接收。但不建议信号与线程混用,难度很大。3)当前工作目录。物理内存地址相同,所以工作目录相同。因为工作目录的物理地址是唯一的。只有物理内存地址相同才能原创 2021-01-04 21:33:18 · 349 阅读 · 0 评论 -
02LinuxC线程学习之三级映射并借其解释进程和线程的根本区别
注:我们之前将线程时,讲过MMU映射虚拟内存到物理内存的关系,实际上当时只是简单的说,稍微有点瑕疵或者错误,下面才是完整的正确的。附上之前的链接:https://blog.csdn.net/weixin_44517656/article/details/1093745261 三级映射下面我们根据图片来理解。1)首先看右边,0-3G的用户虚拟内存,是先通过内核的PCB处理,因为它里面存储这与虚拟内存地址相关的信息。然后通过小圆圈的三级映射后,已经直接将虚拟内存和物理内存建立好了关系,而MMU只原创 2021-01-04 21:11:35 · 485 阅读 · 0 评论 -
01LinuxC线程学习之线程概念,简述与进程区别和查看LWP轻量级线程号(不是PID也不是TID))
1 线程1.1 线程概念1)线程概念:每个线程有各自的PCB,但没有独立的地址空间(共享)。注:该地址空间指的是以进程为单位,不是指栈。而进程拥有独立地址空间,拥有PCB。2)LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)。1.2 进程与线程的区别1)在于是否共享地址空间。线程是最小的执行单位,进程是最小分配资源单位,可看成是只有一个线程的进程。2 查看线程号LWP的方法LWP,你可以看作线程,但操作系统认它为进程,所以看你怎么理解就原创 2021-01-04 20:39:50 · 291 阅读 · 0 评论 -
03守护进程学习之创建守护进程的代码例子
03守护进程学习之创建守护进程的代码例子1 守护进程的例子#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <sys/stat.h>#include <unistd.h>void mydaemond(void){ //1 创建子进程,父进程退出 pid_t pid = fork(); if (pid > 0) {原创 2020-10-28 22:50:25 · 204 阅读 · 0 评论 -
02守护进程学习之创建守护进程的七步骤及其分析
02守护进程学习之创建守护进程的七步骤及其分析1 守护进程的概念1)脱离终端,运行于inux中的后台服务进程,不能直接和用户交互。不受用户登录、注销的影响。2)在后台周期执行任务或等待处理某些事件。一般采用以d结尾的名字。如:预读入缓输出机制的实现,httpd,sshd,ftpd服务器等。2 守护进程与会话的区别1)从程序步骤来看:会话在子进程调用setsid()就已经成功创建,而守护进程仍需继续处理。2)从范围来看:会话为进程组的集合,而守护进程只是单独一个进程,会话更为广泛。某个普通进程原创 2020-10-28 22:25:39 · 388 阅读 · 0 评论 -
01守护进程学习之会话的概念和创建会话(包含Linux下相应id的总结一览)
01守护进程学习之会话的概念和创建会话(包含进程id,进程组id,会话id)1 会话的概念会话是由多个进程组组成的,而进程组又是由多个进程组成的。2 创建会话前需要注意的地方1)进程组组长是不能创建新会话的,因为创建新会话该进程会成为新的进程组组长,由于它之前是进程组组长,所以这会导致之前的进程组无人管理,这是不允许的;2)对应第一点1),所以创建新会话的时候,fork()函数之后,只能在pid=0的子进程调用setsid(void)创建新会话,并且创建后3d合一(子进程id=进程组id=会话is原创 2020-10-27 22:56:01 · 483 阅读 · 0 评论 -
14信号学习之使用信号回收子进程案例(对进程和信号学习的总结,最重要的文章)
概述:1)本节案例可以说是信号和进程学习最重要的一节,因为它的知识完全覆盖了进程和信号,也是工作中的思维。1 使用信号回收子进程案例这个案例很重要,我已经在代码中注释了需要注意的5个点,大家去理解就行。例子完全是可以使用在工作项目当中,在对应的内容接着自己的逻辑代码即可。看代码不懂时可以按照注意点一点一点的理解。#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <er原创 2021-01-24 18:35:07 · 874 阅读 · 1 评论 -
13信号学习之内核实现信号捕捉过程
1 内核实现信号捕捉过程看图即可。依照顺序看,红色字体代表每一点的解释。原创 2021-01-24 12:23:53 · 191 阅读 · 0 评论 -
12信号学习之信号捕捉特性及其多个测试案例(并总结临时屏蔽字和系统屏蔽字的作用域关系)-->(非常重要)
1 信号捕捉特性1)进程正常运行时,默认PCB中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。2)XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。3)阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)本篇可以与上一篇对照理解。https://blog.csdn.net/weixi原创 2021-01-24 12:17:14 · 327 阅读 · 0 评论 -
11信号学习之sigaction函数及使用其实现信号捕捉案例(信号最重要的一节)
1 sigaction函数1)上一篇我们说的signal函数捕捉信号,但是他不是POSIX标准,在不同的操作系统上可能执行不了。所以我们项目开发时需要使用本节接下来所讲的sigaction函数捕捉。2)并且需要强调下面struct sigaction结构体的成员sigset_t sa_mask,它的作用域是只在回调函数生效,其余不生效,即其余时间系统的mask生效。而sigprocmask函数的参2const sigset_t *set也是屏蔽字,但是它代表的意思是程序的整个生存期,它的意思和我前原创 2021-01-24 10:23:58 · 1146 阅读 · 0 评论 -
10信号学习之signal函数及使用其实现信号捕捉案例
1 signal函数功能:该函数注册一个信号捕捉函数。对比上一篇的案例和相关函数,都是针对于信号集操作的,而这个函数是针对处理动作来操作的,你可以利用此函数将捕捉到的信号按自己的方式执行,例如你可以捕捉段错误的信号后执行打印hello world。typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);/* 功能:该函数注册一个信号捕捉函数,针对于信号的处理动作层面。所以这就涉及原创 2021-01-23 23:21:36 · 671 阅读 · 0 评论 -
09信号学习之信号集相关函数的使用案例(重要)
1 信号集相关函数的使用案例#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>void printset(sigset_t *ped){ int i; for(i = 1; i < 32; i++){ if((sigismember(ped, i) == 1)){ putchar('1'); } else { putc原创 2021-01-23 23:05:26 · 491 阅读 · 0 评论 -
08信号学习之信号集(屏蔽集和未决信号集)相关函数(重要)
概述:1)屏蔽信号集(字)在线程间不共享,这一点可以看我线程相关的文章。2)要想熟悉调用本节内的相关函数,必须熟悉信号从产生到处理的过程,否则很难搞明白怎么使用。文章在我前面信号相关的文章。3)由于未决信号集系统不让我们处理,但是我们可以通过处理信号屏蔽集影响未决信号集。1 信号集相关函数内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。所以我们可以在应用程序中自定义信号集(set)来改变mask,以达到屏蔽指定信号的目的。sigset_t类型的本质是位图。原创 2021-01-23 22:07:36 · 232 阅读 · 0 评论 -
07信号学习之setitimer函数及其案例
1 setitimer函数该函数可以代替上一篇文章的alarm定时器函数,作用都是在一定时间后发送相应的信号给进程。int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);/* 功能:设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。 成功:0;失败:-1,设置errno。 参数1:which:指定定时方式。 ① 自然定时:默认发送ITIMER_原创 2021-01-23 20:19:17 · 931 阅读 · 0 评论 -
06信号学习之alarm函数
1 alarm函数该函数是设置定时器的函数,每个进程都有且只有唯一个定时器。并且定时与进程状态无关(自然定时法),例如就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。unsigned int alarm(unsigned int seconds); /* 功能:设置定时器(闹钟),在指定seconds后,内核会给当前进程发送编号为14的SIGALRM信号,进程收到该信号,默认动作终止。若时间未到,返回剩余的秒数,若时间到了或者超了返回0,无失败返回。 需要注意原创 2021-01-23 15:00:14 · 1540 阅读 · 0 评论 -
05信号学习之raise和abort函数
1 raise和abort函数这两个函数不是很常用,简单介绍一下即可。1.1 raise函数int raise(int sig); /* 功能:给当前进程发送指定信号(自己给自己发)。 成功:0,失败非0值。 参1:信号值。 一般通过判断来使用,例如raise(signo) == kill(getpid(), signo);*/1.2 abort函数 void abort(void); /* 功能:给自己发送异常终止信号,即编号6的SIGABRT 信号,终止并产生cor原创 2021-01-23 14:31:15 · 492 阅读 · 0 评论 -
04信号学习之kill函数
1 kill函数kil函数和kill命令完全一样,只不过kill命令在命令行操作,kill函数再程序操作。 int kill(pid_t pid, int sig); /* 功能:给指定进程发送信号去执行某个动作,注意不一样杀死,别被字面意思kill误解。 成功:0;失败:-1 (可能是pid,信号值非法,普通用户杀init进程等权级问题),设置errno。 参1:进程pid。 pid > 0: 发送信号给指定的进程。 pid = 0: 发送信号给与调用kill函数进程所属同一原创 2021-01-23 14:26:25 · 759 阅读 · 0 评论 -
03信号学习之信号一览及其解释和信号四要素
1 信号一览不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。可以使用kill –l命令查看当前系统可使用的信号有哪些。1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR111) SIGSEGV 12) SIGUS原创 2021-01-23 13:47:54 · 1042 阅读 · 0 评论 -
02信号学习之信号的产生方式,所处状态,处理方式,信号相关的集合和处理流程(很重要的概念和理解)
1 信号的产生方式,信号所处状态,信号的处理方式相关概念1.1 信号的产生方式1)1. 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\。2)系统调用产生,如:kill、raise、abort3)软件条件产生,如:定时器alarm4)硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)。5)命令产生,如:kill命令1.2 信号所处状态1)递达:递送并且到达进程。注意一定是到达了进程(实际是内核)。2)未决:产生和递达之间的状态,更详细的描述应原创 2021-01-23 12:38:50 · 775 阅读 · 0 评论 -
01信号学习之信号的概念于机制
1 信号的相关认知1)信号的概念:传播信息的方法,所以它是信息发送的标志。2)信号的机制:A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。3)信号的特质:延时性。信号是通过软件方法实现,其实针对于计算机,现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉,所以在用户视角可以忽略不计。4)信号的处理方式:这是最重要原创 2021-01-23 11:44:31 · 1563 阅读 · 0 评论 -
16LinuxC进程间通信之mmap创建匿名映射区
1 mmap创建匿名映射区创建匿名映射区非常简单,只需要加上MAP_ANONYMOUS即可,参数len长度可以随便大小,fd没有传-1即可。open这些函数可以不需要了,并且匿名映射实际上就是解决中间创建的文件问题。#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/mman.h>#include <sys原创 2021-01-11 21:45:27 · 652 阅读 · 2 评论 -
15LinuxC进程间通信之mmap无血缘进程间通信
1 mmap无血缘进程间通信1)写文件:#include <stdio.h>#include <sys/stat.h>#include <unistd.h>#include <stdlib.h>#include <fcntl.h>#include <string.h>#include <sys/mman.h>#include <errno.h>typedef struct Student{原创 2021-01-11 21:36:20 · 217 阅读 · 0 评论 -
14LinuxC进程间通信之mmap父子进程通信
1 mmap父子进程通信要想通过mmap进行通信,mmap函数的标志位必须是共享,这样才能同步到文件中,假设为私有的话,是不能通信的,因为内存(buf)在进程间是无法共享的,和不同函数内的变量一样,它们的值互不影响。#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/mman.h>#include <sys原创 2021-01-11 21:04:36 · 228 阅读 · 0 评论 -
13LinuxC进程间通信之mmap的注意事项总结(重要)
1 mmap的注意事项11 可以open的时候O_CREAT一个新文件来创建映射区吗?可以,但是:1)当创建映射区的文件为0时,mmap的len为非0,出总线错误SIGBUS。2)当创建映射区的文件为0时,mmap的len也为0,出无效参数错误Invaild xxx。2 如果open时O_RDONLY, mmap时PROT参数指定PROT_READ|PROT_WRITE会怎样?1)结果是出无效参数错误Invaild xxx,因为open的fd根本没有写权限,mmap无法使用。所以这里应该m原创 2021-01-11 20:47:23 · 1222 阅读 · 0 评论 -
12LinuxC进程间通信之mmap案例
1 mmap案例直接看代码即可,注释已经写清楚了。#include <stdio.h>#include <string.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <stdlib.h>#include <sys/mman.h>void sys_err(char *str){ perror(str);原创 2021-01-10 22:16:39 · 222 阅读 · 0 评论