线程的应用

来源:互联网 发布:年度网络安全检查报告 编辑:程序博客网 时间:2024/06/05 21:59
上节中,我们成功地编译运行了一个linux模 块。可惜的是,它只有两个函数,hello_init在模块加载时调用,hello_exit 在模块卸载时调用。这样下去,模块纵使有天大的本事,也只能压缩在这两个


函数中。为了避免这种悲剧发生,本节就来学习一种让模块在加载后能一直运行下去的 方法——内核线程。 收藏于 2011-10-10阅读数:


公众公开
好文推荐
要创建一个内核线程有许多种方法,我们这里要学的是最简单的一种。打开 include/linux/kthread.h,你就看到了它全部的API,一共三个函数。节日零食之坚果排名



1. struct task_struct kthread_run(int (*threadfn)(void *data),
2.     void *data, const char namefmt[],...);  
3. int kthread_stop(struct task_struct *k);  
4. int kthread_should_stop(void);  

      kthread_run()负责内核线程的创建,参数包括入口函数 threadfn,参数data,线程名称namefmt。可以看到线程的名字可以是类似sprintf方式组成的字符串。如果实际看到 kthread.h文件,就


会发现kthread_run实际是一个宏定义,它由kthread_create()和wake_up_process() 两部分组成,这样的好处是用kthread_run()创建的线程可以直接运行,使用方便。


汽车养护专家:谨防爱车错误养护...
有一种感觉叫心有灵犀
      kthread_stop()负责结束创建的线程,参数是创建时返回的task_struct指针。kthread设置标志should_stop,并等 待线程主动结束,返回线程的返回值。线程可能在kthread_stop()调用前就结


束。(经过实际验证,如果线程在kthread_stop()调用 之前就结束,之后kthread_stop()再调用会发生可怕地事情—调用kthread_stop()的进程crash!!之所以如此,是由于 kthread实现上的弊端,


之后会专门写文章讲到) 28个非常精美的室内装修风格
女人要精致地活着
      kthread_should_stop()返回should_stop标志。它用于创建的线程检查结束标志,并决定是否退出。线程完全可以在完成自己的 工作后主动结束,不需等待should_stop标志。【图】揭



    下面就来尝试一下运行不息的内核线程吧。
1、把上节建立的hello子目录,复制为新的kthread子目录。

2、修改hello.c,使其内容如下。

1. #include <linux/init.h>  
2. #include <linux/module.h>  
3. #include <linux/kthread.h>  

5. MODULE_LICENSE("Dual BSD/GPL");  
6
7. static struct task_struct *tsk;  
8
9. static int thread_function(void *data)  
10. {  
11.     int time_count = 0;  
12.     do {  
13.         printk(KERN_INFO "thread_function: %d times", ++time_count);  
14.         msleep(1000);  
15.     }while(!kthread_should_stop() && time_count<=30);  
16.     return time_count;  
17. }  
18
19. static int hello_init(void)  
20. {  
21.     printk(KERN_INFO "Hello, world!\n");  
22
23.     tsk = kthread_run(thread_function, NULL, "mythread%d", 1);  
24.     if (IS_ERR(tsk)) {  
25.         printk(KERN_INFO "create kthread failed!\n");  
26.     }  
27.     else {  
28.         printk(KERN_INFO "create ktrhead ok!\n");  
29.     }  
30.     return 0;  
31. }  
32
33. static void hello_exit(void)  
34. {  
35.     printk(KERN_INFO "Hello, exit!\n");  
36.     if (!IS_ERR(tsk)){  
37.         int ret = kthread_stop(tsk);  
38.         printk(KERN_INFO "thread function has run %ds\n", ret);  
39.     }  
40. }  
41
42. module_init(hello_init);  
43. module_exit(hello_exit);  

为了不让创建的内核线程一直运行浪费CPU,代码中采用周期性延迟的方式,每次循 环用msleep(1000)延迟1s。为了防止线程一直运行下去,代码中使用了两个结束条件:一个是模块要求线程结束,一


个是打印满一定次数,后者是为 了防止printk输出信息太多。最后在hello_exit中结束线程,并打印线程运行的时间。

这里要注意的是kthread_run的返回值tsk。不能用tsk是否为 NULL进行检查,而要用IS_ERR()宏定义检查,这是因为返回的是错误码,大致从0xfffff000~0xffffffff。 

3、编译运行模块,步骤参照前例。在运行过程中使用ps -e命令,可以看到有名字位mythread1的内核线程在运行。

经过本节,我们学习了内核线程的创建使用方法,现在要创建一大堆的线程在内核中已 经易如反掌。你会逐渐相信,我们模块的拓展空间是无限的。

附注:

      我们的重点在模块编程,不断学习内核API的使用。但如果能知其然,而知其所以然就更好了。所以有了文章后的附注部分。在附注部分,我们会尽量解释内核 API的实现原理,对相关linux内核


代码做简单的分析,以帮助大家学习理解相关的代码。分析的代码包含在linux-2.6.32中,但这些代码在相 近版本中都变化不大。作者水平有限,请大家见谅。




      kthread的实现在kernel/kthread.c中,头文件是include/linux/kthread.h。内核中一直运行一个线程 kthreadd,它运行kthread.c中的kthreadd函数。在kthreadd()中,不断检查一个


kthread_create_list 链表。kthread_create_list中的每个节点都是一个创建内核线程的请求,kthreadd()发现链表不为空,就将其第一个节点退出链 表,并调用create_kthread()创建相应的线程。


create_kthread()则进一步调用更深层的kernel_thread()创建 线程,入口函数设在kthread()中。

      外界调用kthread_run创建运行线程。kthread_run是个宏定义,首先调用kthread_create()创建线程,如果创建成功,再 调用wake_up_process()唤醒新创建的线程。kthread_create()根据参数


向kthread_create_list中发送一 个请求,并唤醒kthreadd,之后会调用wait_for_completion(&create.done)等待线程创建完成。新创建的线 程开始运行后,入口在kthread(),kthread()调用


complete(&create->done)唤醒阻塞的模块进程,并 使用schedule()调度出去。kthread_create()被唤醒后,设置新线程的名称,并返回到kthread_run中。 kthread_run调用wake_up_process()重新唤


醒新创建线程,此时新线程才开始运行kthread_run参数中的入口函数。

      外界调用kthread_stop()删除线程。kthread_stop首先设置结束标志should_stop,然后调用 wake_for_completion(&kthread->exited)上,这个其实是新线程task_struct上的 vfork_done,会在


线程结束调用do_exit()时设置。



上节中我们已经掌握了创建大量内核线程的能力,可惜线程之间还缺乏配合。要知道学习ITC(inter thread communication),和学习IPC(inter process communication)一样,不是件简单的事情。本节


就暂且解释一种最简单的线程同步手段—completion。

打开include/linux/completion.h,你就会看到 completion使用的全部API。这里简单介绍一下。



1. struct completion{  
2.     unsigned int done;  
3.     wait_queue_head_t wait;  
4. };  
5
6. void init_completion(struct completion *x);  
7. void wait_for_completion(struct completion *x);  
8. void wait_for_completion_interruptible(struct completion *x);  
9. void wait_for_completion_killable(struct completion *x);  
10. unsigned long wait_for_completion_timeout(struct completion *x,  
11.     unsigned long timeout);  
12. unsigned long wait_for_completion_interruptible_timeout(struct completion *x,  
13.     unsigned long timeout);  
14. bool try_wait_for_completion(struct completion *x);  
15. bool completion_done(struct completion *x);  
16. void complete(struct completion *x);  
17. void complete_all(struct completion *x);  
struct completion{ unsigned int done; wait_queue_head_t wait; }; void init_completion(struct completion *x); void wait_for_completion(struct completion *x); void 


wait_for_completion_interruptible(struct completion *x); void wait_for_completion_killable(struct completion *x); unsigned long wait_for_completion_timeout(struct completion *x, 


unsigned long timeout); unsigned long wait_for_completion_interruptible_timeout(struct completion *x, unsigned long timeout); bool try_wait_for_completion(struct completion *x); 


bool completion_done(struct completion *x); void complete(struct completion *x); void complete_all(struct completion *x);






       首先是struct completion的结构,由一个计数值和一个等待队列组成。我们就大致明白,completion是类似于信号量的东西,用 completion.done来表示资源是否可用,获取不到的线程会阻塞


在completion.wait的等待队列上,直到其它线程释放 completion。这样理解在实现上不错,但我认为completion不是与具体的资源绑定,而是单纯作为一种线程间同步的机制,它在概念上要比信 号量


清晰得多。以后会逐渐看到,线程间事件的同步大多靠completion,而资源临界区的保护大多靠信号量。所以说,completion是一种线程 间的约会。




           init_completion初始化 completion结构。初此之外,linux当然还有在定义变量时初始化的方法,都在completion.h中。

      wait_for_completion等待在completion上。如果加了interruptible,就表示线程等待可被外部发来的信号打断;如 果加了killable,就表示线程只可被kill信号打断;如果加了timeout,表示


等待超出一定时间会自动结束等待,timeout的单位是系统 所用的时间片jiffies(多为1ms)。

      try_wait_for_completion则是非阻塞地获取completion。它相当于 wait_for_completion_timeout调用中的timeout值为0。

      completion_done检查是否有线程阻塞在completion上。但这个API并不准确,它只是检查completion.done是否为 0,为0则认为有线程阻塞。这个API并不会去检查实际的等待队列,所以用时要注


意。

      complete唤醒阻塞在completion上的首个线程。

      complete_all唤醒阻塞在completion上的所有线程。它的实现手法很粗糙,把completion.done的值设为 UINT_MAX/2,自然所有等待的线程都醒了。所以如果complete_all之后还要使用这个


completion,就要把它重新初始化。




      好,completion介绍完毕,下面就来设计我们的模块吧。

      我们模拟5个周期性线程的运行。每个周期性线程period_thread的周期各不相同,但都以秒为单位,有各自的completion变量。period_thread每个周期运行一次,然后等待在 自己的completion


变量上。为了唤醒period_thread,我们使用一个watchdog_thread来模拟时钟,每隔1s watchdog_thread就会检查哪个period_thread下一周期是否到来,并用相应的completion唤醒线程。




下面就动手实现吧。

1、把上节建立的kthread子目录,复制为新的completion子目录。

2、修改hello.c,使其内容如下。

1. #include <linux/init.h>  
2. #include <linux/module.h>  
3. #include <linux/kthread.h>  
4. #include <linux/completion.h>  
5
6. MODULE_LICENSE("Dual BSD/GPL");  
7
8. #define PERIOD_THREAD_NUM 5  
9
10. static int periods[PERIOD_THREAD_NUM] =  
11.     { 1, 2, 4, 8, 16 };  
12
13. static struct task_struct *period_tsks[PERIOD_THREAD_NUM];  
14
15. static struct task_struct watchdog_tsk;  
16
17. static struct completion wakeups[PERIOD_THREAD_NUM];  
18
19
20. static int period_thread(void *data)  
21. {  
22.     int k = (int)data;  
23.     int count = -1;  
24
25.     do{  
26.         printk("thread%d: period=%ds, count=%d\n", k, periods[k], ++count);  
27.         wait_for_completion(&wakeups[k]);  
28.     }while(!kthread_should_stop());  
29.     return count;  
30. }  
31
32. static int watchdog_thread(void *data)  
33. {  
34.     int k;  
35.     int count = 0;  
36
37.     do{  
38.         msleep(1000);  
39.         count++;  
40.         for(k=0; k<PERIOD_THREAD_NUM; k++){  
41.             if (count%periods[k] == 0)  
42.                 complete(&wakeups[k]);  
43.         }  
44.     }while(!kthread_should_stop());  
45.     return count;  
46. }  
47
48. static int hello_init(void)  
49. {  
50.     int k;  
51
52.     printk(KERN_INFO "Hello, world!\n");  
53
54.     for(k=0; k<PERIOD_THREAD_NUM; k++){  
55.         init_completion(&wakeups[k]);  
56.     }  
57
58.     watchdog_tsk = kthread_run(watchdog_thread, NULL, "watchdog_thread");  
59
60.     if(IS_ERR(watchdog_tsk)){  
61.         printk(KERN_INFO "create watchdog_thread failed!\n");  
62.         return 1;  
63.     }  
64
65.     for(k=0; k<PERIOD_THREAD_NUM; k++){  
66.         period_tsks[k] = kthread_run(period_thread, (void*)k, "period_thread%d", k);  
67.         if(IS_ERR(period_tsks[k]))  
68.             printk(KERN_INFO "create period_thread%d failed!\n", k);  
69.     }  
70.     return 0;  
71. }  
72
73. static void hello_exit(void)  
74. {  
75.     int k;  
76.     int count[5], watchdog_count;  
77
78.     printk(KERN_INFO "Hello, exit!\n");  
79.     for(k=0; k<PERIOD_THREAD_NUM]; k++){  
80.         count[k] = 0;  
81.         if(!IS_ERR(period_tsks[k]))  
82.             count[k] = kthread_stop(period_tsks[k]);  
83.     }  
84.     watchdog_count = 0;  
85.     if(!IS_ERR(watchdog_tsk))  
86.         watchdog_count = kthread_stop(watchdog_tsk);  
87
88.     printk("running total time: %ds\n", watchdog_count);  
89.     for(k=0; k<PERIOD_THREAD_NUM; k++)  
90.         printk("thread%d: period %d, running %d times\n", k, periods[k], count[k]);  
91. }  
92
93. module_init(hello_init);  
94. module_exit(hello_exit);  

3、编译运行模块,步骤参照前例。为保持模块的简洁性,我们仍然使用了 kthread_stop结束线程,这种方法虽然简单,但在卸载模块时等待时间太长,而且这个时间会随线程个数和周期的增长而增长




4、使用统一的exit_flag标志来表示结束请求,hello_exit发送 completion信号给所有的周期线程,最后调用kthread_stop来回收线程返回值。这样所有的周期线程都是在被唤醒后看到 exit_flag,自


动结束,卸载模块时间大大缩短。下面是改进过后的hello.c,之前的那个姑且叫做hello-v1.c好了。

1. #include <linux/init.h>   
2. #include <linux/module.h>  
3. #include <linux/kthread.h>  
4. #include <linux/completion.h>  
5
6. MODULE_LICENSE("Dual BSD/GPL");  
7
8. #define PERIOD_THREAD_NUM 5  
9
10. static int periods[PERIOD_THREAD_NUM] =  
11.     { 1, 2, 4, 8, 16 };  
12
13. static struct task_struct *period_tsks[PERIOD_THREAD_NUM];  
14
15. static struct task_struct watchdog_tsk;  
16
17. static struct completion wakeups[PERIOD_THREAD_NUM];  
18
19. static int exit_flag = 0;  
20
21. static int period_thread(void *data)  
22. {  
23.     int k = (int)data;  
24.     int count = -1;  
25
26.     do{  
27.         printk("thread%d: period=%ds, count=%d\n", k, periods[k], ++count);  
28.         wait_for_completion(&wakeups[k]);  
29.     }while(!exit_flag);  
30.     return count;  
31. }  
32
33. static int watchdog_thread(void *data)  
34. {  
35.     int k;  
36.     int count = 0;  
37
38.     do{  
39.         msleep(1000);  
40.         count++;  
41.         for(k=0; k<PERIOD_THREAD_NUM; k++){  
42.             if (count%periods[k] == 0)  
43.                 complete(&wakeups[k]);  
44.         }  
45.     }while(!exit_flag);  
46.     return count;  
47. }  
48
49. static int hello_init(void)  
50. {  
51.     int k;  
52
53.     printk(KERN_INFO "Hello, world!\n");  
54
55.     for(k=0; k<PERIOD_THREAD_NUM; k++){  
56.         init_completion(&wakeups[k]);  
57.     }  
58
59.     watchdog_tsk = kthread_run(watchdog_thread, NULL, "watchdog_thread");  
60
61.     if(IS_ERR(watchdog_tsk)){  
62.         printk(KERN_INFO "create watchdog_thread failed!\n");  
63.         return 1;  
64.     }  
65
66.     for(k=0; k<PERIOD_THREAD_NUM; k++){  
67.         period_tsks[k] = kthread_run(period_thread, (void*)k, "period_thread%d", k);  
68.         if(IS_ERR(period_tsks[k]))  
69.             printk(KERN_INFO "create period_thread%d failed!\n", k);  
70.     }  
71.     return 0;  
72. }  
73
74. static void hello_exit(void)  
75. {  
76.     int k;  
77.     int count[5], watchdog_count;  
78
79.     printk(KERN_INFO "Hello, exit!\n");  
80.     exit_flag = 1;  
81.     for(k=0; k<PERIOD_THREAD_NUM]; k++)  
82.         complete_all(&wakeups[k]);  
83
84.     for(k=0; k<PERIOD_THREAD_NUM]; k++){  
85.         count[k] = 0;  
86.         if(!IS_ERR(period_tsks[k]))  
87.             count[k] = kthread_stop(period_tsks[k]);  
88.     }  
89.     watchdog_count = 0;  
90.     if(!IS_ERR(watchdog_tsk))  
91.         watchdog_count = kthread_stop(watchdog_tsk);  
92
93.     printk("running total time: %ds\n", watchdog_count);  
94.     for(k=0; k<PERIOD_THREAD_NUM; k++)  
95.         printk("thread%d: period %d, running %d times\n", k, periods[k], count[k]);  
96. }  
97
98. module_init(hello_init);  
99. module_exit(hello_exit);  

5、编译运行改进过后的模块。可以看到模块卸载时间大大减少,不会超过1s。

经过本节,我们学会了一种内核线程间同步的机制—completion。线程们已 经开始注意相互配合,以完成复杂的工作。相信它们会越来越聪明的。




附注:

completion的实现在kernel/sched.c中。这里的每个API 都较短,实现也较为简单。completion背后的实现机制其实是等待队列。等待队列的实现会涉及到较多的调度问题,这里先简单略过。





通过之前几节,我们已经了解了内核线程的创建方法kthread,内核同步的工具completion。现在我们就来学学内核线程传递消息的方法 list。或许大家会说,list不是链表吗。不错,list是链表,但


它可以变成承担消息传递的消息队列。消息的发送者把消息放到链表上,并通过同步工 具(如completion)通知接收线程,接收线程再从链表上取回消息,就这么简单。linux内核或许没有给我们定制


好的东西,但却给了我们可随意变 换的、基础的工具,把这些工具稍加组合就能完成复杂的功能。list又是这些万能工具中最常用的。








前 面两篇文章的惯例是先对新增的功能做出介绍,并解释要用到的API。但我感觉这种既要解释原理,又要分析代码,又要写应用样例的十全文章,写起来实在吃 力,而且漏洞百出。与其如此,我还不


如把两部分分开,这里的模块编程就专心设计模块,编写内核API的组合使用样例;而原理介绍、API代码分析的部分, 会转到linux内核部件分析的部分。这样我一方面能安心设计样例,一方面能把API


介绍地更全面一些。

模块设计

本模块目的是展示list作为消息队列使用时的情况。所以会创建一个全局链表work_list,定义一种消息的结构struct work_event,并创建两个内核线程work_thread和watchdog_thread。work_thread是


消息的接收者,它循环检查 work_list,如果其上有消息,就将其取出并执行,否则阻塞。watchdog_thread是消息的发送者,它周期性地发送消息到 work_list,并唤醒work_thread。




模块实现

1、建立list子目录。

2、编写list.c,使其内容如下。

1. #include <linux/init.h>  
2. #include <linux/module.h>  
3. #include <linux/list.h>  
4. #include <linux/completion.h>  
5. #include <linux/kthread.h>  
6
7. MODULE_LICENSE("Dual BSD/GPL");  
8
9. static struct task_struct *work_tsk;  
10
11. static struct task_struct *watchdog_tsk;  
12
13. static DECLARE_SPINLOCK(work_list_lock);  
14
15. static LIST_HEAD(work_list);  
16
17. static DECLARE_COMPLETION(work_wait);  
18
19. enum WORK_EVENT_TYPE {  
20.     EVENT_TIMER,  
21.     EVENT_EXIT  
22. };  
23
24. struct work_event {  
25.     enum WORK_EVENT_TYPE type;  
26.     int need_free;  
27.     list_head list;  
28. };  
29
30. static int work_thread(void *data)  
31. {  
32.     int count = 0;  
33
34.     while(1){  
35.         if(list_empty(&work_list))  
36.             wait_for_completion(&work_wait);  
37.         spin_lock(&work_list_lock);  
38.         while(!list_empty(&work_list)){  
39.             struct work_event *event;  
40.             event = list_entry(work_list.next, struct work_event, list);  
41.             list_del(&event->list);  
42.             spin_unlock(&work_list_lock);  
43
44.             if(event->type == EVENT_TIMER){  
45.                 printk(KERN_INFO "event timer: count = %d\n", ++count);  
46.             }  
47.             else if (event->type == EVENT_EXIT){  
48.                 if(event->need_free)  
49.                     kfree(event);  
50.                 goto exit;  
51.             }  
52
53.             if(event->need_free)  
54.                 kfree(event);  
55.             spin_lock(&work_list_lock);  
56.         }  
57.     }  
58. exit:  
59.     return count;  
60. }  
61
62. static int watchdog_thread(void *data)  
63. {  
64.     int count = 0;  
65.     while(!kthread_should_stop()){  
66.         msleep(1000);  
67.         count++;  
68.         if(count%5 == 0){  
69.             struct work_event *event;  
70.             event = kmalloc(sizeof(struct work_event), GFP_KERNEL);  
71.             if(event == NULL){  
72.                 printk(KERN_INFO "watchdog_thread: kmalloc failed!\n");  
73.                 break;  
74.             }  
75.             event->type = EVENT_TIMER;  
76.             event->need_free = 1;  
77.             spin_lock(&work_list_lock);  
78.             list_add_tail(&event->list, &work_list);  
79.             spin_unlock(&work_list_lock);  
80.             complete(&work_wait);  
81.         }  
82.     }  
83.     return count;  
84. }  
85
86. static int list_init()  
87. {  
88.     printk(KERN_INFO "list_init()\n");  
89
90.     watchdog_tsk = kthread_run(watchdog_thread, NULL, "watchdog_thread");  
91.     if(IS_ERR(watchdog_tsk))  
92.         goto err1;  
93
94.     work_tsk = kthread_run(work_thread, NULL, "work_thread");  
95.     if(IS_ERR(work_tsk))  
96.         goto err2;  
97
98.     return 0;  
99
100. err2:  
101.     kthread_stop(watchdog_tsk);  
102. err1:  
103.     return 1;  
104. }  
105
106. static void list_exit()  
107. {  
108.     printk(KERN_INFO "list_exit()\n");  
109
110.     if(!IS_ERR(watchdog_tsk)){  
111.         int count = kthread_stop(watchdog_tsk);  
112.         printk(KERN_INFO "watchdog_thread: running for %ss\n", count);  
113.     }  
114.     if(!IS_ERR(work_tsk)){  
115.         get_task_struct(&work_tsk);  
116.         struct work_event event;  
117.         event.type = EVENT_EXIT;  
118.         event.need_free = 0;  
119.         spin_lock(&work_list_lock);  
120.         list_add(&event.list, &work_list);  
121.         spin_unlock(&work_list_lock);  
122.         complete(&work_wait);  
123
124.         int count = kthread_stop(work_tsk);  
125.         printk(KERN_INFO "work_thread: period 5s, running %d times\n", count);  
126.     }  
127. }  
128
129. module_init(list_init);  
130. module_exit(list_exit);  

整个模块较为简单,work_thread只是接收work_list中的消息并处理,所以在list_exit退出时也要给它发 EVENT_EXIT类型的消息,使其退出。至于在list_exit发消息之前调用的get_task_struct,实


在是无奈之举。因为我们发送 EVENT_EXIT消息后work_thread会在kthread_stop调用前就马上结束,导致之后的kthread_stop错误。所以要先 get_task_struct防止work_thread退出后释放任务结构中的


某些内容,虽然有对应的put_task_struct,但我们并未使 用,因为put_task_struct并未引出到符号表。当然,这种情况只是一时之举,等我们学习了更强大的线程同步机制,或者更灵活的线程管理方


法, 就能把代码改得更流畅。

      注意到代码中使用spin_lock/spin_unlock来保护work_list。如果之前不了解spin_lock,很容易认为它不足以保护。实 际上spin_lock不止是使用自旋锁,在此之前还调用了preempt_disable来


禁止本cpu任务调度。只要同步行为只发生在线程之 间,spin_lock就足以保护,如果有中断或者软中断参与进来,就需要用spin_lock_irqsave了。

3、 编译运行模块。

可能本节介绍的list显得过于质朴,但只有简单的东西才能长久保留。不久之后,我们或许不再用kthread,不再用completion,但 list一定会一直用下去。
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 妹妹初中毕业谈恋爱怎么办 原告的证据造假怎么办 慕课考试不及格怎么办 大学高数不及格怎么办 对方拒绝司法调解怎么办? cas授权已过期怎么办 英法巡航南海中国怎么办 wps文件不能改怎么办 wps不能删除内容怎么办 word文件被锁定怎么办 word批注不显示怎么办 wps怎么办把修订取消 审阅密码忘了怎么办 psv关机后怎么办刷 被螃蟹扎了怎么办 被海鲜划伤出血怎么办 海域使用证缴纳金没交怎么办 海峡中线 金门海域怎么办 对工作失去热情怎么办 取款机多出钱怎么办 风扇声音很响怎么办 稳压器输出没电怎么办 稳压器不稳10压怎么办 dnf凯蒂不见了怎么办 马桶里掉进塑料瓶盖怎么办 塑料瓶子盖子打不开怎么办 按压瓶盖坏了怎么办 瓶盖拧错位了怎么办 红酒盖子开不了怎么办 胶盖罐头打不开怎么办 玻璃瓶的塑料盖打不开怎么办 香水按压不出来怎么办 电高压锅盖子打不开怎么办 杯子螺口错位怎么办 散粉盖子扭不开怎么办 玻璃瓶饮料盖子打不开怎么办 玻璃瓶玻璃盖子打不开怎么办 美甲没有胶水怎么办 按压式瓶盖打不开怎么办 睫毛胶水瓶盖打不开怎么办 玻璃杯盖子滑丝怎么办