目录
1.1.6进程线程的状态转换图 什么时候阻塞,什么时候就绪⭐⭐⭐
1.2.2线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?⭐⭐⭐⭐
1.2.3并发,同步,异步,互斥,阻塞,非阻塞的理解⭐⭐⭐⭐⭐
1.1 进程线程的基本概念
-
1.1.1 什么是进程,线程,彼此有什么区别⭐⭐⭐⭐⭐
答:进程是资源(CPU、内存等)分配的基本单位,线程是CPU调度和分配的基本单位(程序执行的最小单位),线程也称为轻量级进程。
1)当我们运行一个程序的时候,系统就会创建一个进程,并分配地址空间和其他资源,最后把进程加入就绪队列直到分配到CPU时间就可以正式运行了。
2)线程是进程的一个执行流,有一个初学者可能误解的概念,进程就像一个容器一样,包括程序运行的程序段、数据段等信息,但是进程其实是不能用来运行代码的,真正运行代码的是进程里的线程。
3)那么,来看看我们最熟悉的main()函数,我们既可以认为这是一个进程,也可以认为是一个线程。我们都知道,在C/C++中main函数是程序入口,所以准确来说main函数是程序的主线程。然而很神奇的地方在于,当系统在执行main函数的时候,main函数又是一个独立的进程,我们可以在main函数里创建子进程,也可以创建子线程。
4)在main函数里创建的多个子线程中,每个线程有自己的堆栈和局部变量,但多个线程也可共享同个进程下的所有共享资源,因此我们经常可以创建多个线程实现并发操作,实现更加复杂的功能。
-
1.1.2多进程、多线程的优缺点⭐⭐⭐⭐
答:
1.)多进程优点
①每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
②通过增加CPU,就可以容易扩充性能;
③可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
④每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
2) 多进程缺点
①逻辑控制复杂,需要和主程序交互,每次新建一个进程都需要为其分配存储空间;
②需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
③多进程调度开销比较大;需要从用户空间切换到内核空间。
3)多线程的优点
①无需跨进程边界;
②程序逻辑和控制方式简单;
③所有线程可以直接共享内存和变量等;
④线程方式消耗的总资源比进程方式好;
4)多线程缺点
①每个线程与主程序共用地址空间,受限于2GB地址空间;
②线程之间的同步和加锁(互斥)控制比较麻烦;
③一个线程的崩溃可能影响到整个程序的稳定性;
④到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
⑤线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU
-
1.1.3什么时候用进程,什么时候用线程⭐⭐⭐
1)创建和销毁较频繁使用线程,因为创建进程花销大嘛。
2)需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。
3)并行操作使用线程。线程是为了实现并行操作的一个手段,也就是刚才说的需要多个并行操作“合作完成大事”,当然是使用线程啦。
4)最后可以总结为:安全稳定选进程;快速频繁选线程;
5)因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
-
1.1.4多进程、多线程同步(通讯)的方法⭐⭐⭐⭐⭐
答:进程间通讯:
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道 (namedpipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
信号量( semophore ) :信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( messagequeue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号 ( sinal ) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存( sharedmemory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
注意:临界区则是一种概念,指的是访问公共资源的程序片段,并不是一种通信方式。
线程通讯:
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
信号机制(Signal): 类似进程间的信号处理
-
1.1.5进程的空间模型⭐⭐⭐⭐
Linux下使用虚拟内存空间给每一个进程,32位操作系统下,每个进程都有独立的4G虚拟内存空间
其中包括:
1)内核区:用户代码不可见的区域,页表就存放在这个区域中
2)用户区:
a、代码段:只可读,不可写,程序代码段
b、数据段:保存全局变量,静态变量的区域
c、堆区:就是动态内存,通过malloc、new申请内存,有一个堆指针,可以通过brk系统调用调整堆指针
d、文件映射区域:通过mmap系统调用,如动态库,共享内存等映射物理空间的内存区域。可以单独释放,不会产生内存碎片
e、栈区:用于维护函数调用的上下文空间,用ulimit -s查看。一般默认为8M。
-
1.1.6进程线程的状态转换图 什么时候阻塞,什么时候就绪⭐⭐⭐
-
1.1.7父进程、子进程的关系以及区别⭐⭐⭐⭐
fork之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...
父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
1、fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。子进程是父进程的副本,获得了父进程数据空间、堆和栈的副本;父子进程并不共享这些存储空间,共享正文段(即代码段);因此子进程对变量的所做的改变并不会影响父进程。一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。
-
1.1.8什么是进程上下文、中断上下文⭐⭐
答:进程上下文就是表示进程信息的一系列东西,包括各种变量、寄存器以及进程的运行的环境。这样,当进程被切换后,下次再切换回来继续执行,能够知道原来的状态;中断上下文就是中断发生时,原来的进程执行被打断,那么就要把原来的那些变量保存下来,以便中断完成后再恢复。
-
1.1.9一个进程可以创建多少线程,和什么有关⭐⭐
答:进程的虚拟内存空间上限,因为创建一个线程,操作系统需要为其分配一个栈空间,如果线程数量越多,所需的栈空间就要越大,那么虚拟内存就会占用的越多。
系统参数限制,虽然 Linux 并没有内核参数来控制单个进程创建的最大线程个数,但是有系统级别的参数来控制整个系统的最大线程个数。
1.2 并发,同步,异步,互斥,阻塞,非阻塞的理解
-
1.2.1什么是线程同步和互斥⭐⭐⭐⭐⭐
答:线程同步:每个线程之间按预定的先后次序进行运行,协同、协助、互相配合。可以理解成“你说完,我再做”。有了线程同步,每个线程才不是自己做自己的事情,而是协同完成某件大事。
线程互斥:当有若干个线程访问同一块资源时,规定同一时间只有一个线程可以得到访问权,其它线程需要等占用资源者释放该资源才可以申请访问。线程互斥可以看成是一种特殊的线程同步。
-
1.2.2线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?⭐⭐⭐⭐
同步是个过程,阻塞是线程的一种状态。多个线程操作共享变量时可能会出现竞争。这时需要同步来防止两个以上的线程同时进入临界区,在这个过程中,后进入临界区的线程将阻塞,等待先进入的线程走出临界区。
线程同步不一定发生阻塞!!!线程同步的时候,需要协调推进速度,互相等待和互相唤醒会发生阻塞。
同样,阻塞也不一定同步。
-
1.2.3并发,同步,异步,互斥,阻塞,非阻塞的理解⭐⭐⭐⭐⭐
并发(concurrency):指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以在同一时间间隔内运行。并行是真正的同时执行。
互斥:同一个资源同一时间只有一个访问者可以进行访问,其他访问者需要等前一个访问者访问结束才可以开始访问该资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:分布在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。所以同步就是在互斥的基础上,通过其它机制实现访问者对资源的有序访问。
同步:同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。
异步:异步和同步是相对的,异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。
阻塞和非阻塞是当进程在访问数据时,根据IO操作的就绪状态不同而采取的不同处理方式,比如主程序调用一个函数要读取一个文件的内容,阻塞方式下主程序会等到函数读取完再继续往下执行,非阻塞方式下,读取函数会立刻返回一个状态值给主程序,主程序不等待文件读取完就继续往下执行。
1.3 孤儿进程、僵尸进程、守护进程的概念
-
1.3.1基本概念⭐⭐⭐⭐⭐
孤儿进程是指父进程已经退出或终止,而它的子进程尚未退出或终止的进程,在这种情况下,子进程将被init进程接管,成为init进程的子进程,init进程对孤儿进程进行回收处理,以释放他们占用的系统资源,并确保它们的退出状态被正确处理,防止孤儿进程变成僵尸进程。
僵尸进程是一个进程fork()创建子进程,当子进程退出时,父进程未使用wait或waitpid函数回收子进程的状态信息,那么子进程的进程描述符仍然存在系统中。
守护进程是linux中的后台服务进程,通常用于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。
-
1.3.2如何创建守护进程:⭐⭐
1)调用umask设置进程创建文件的权限屏蔽字(umask),便于守护进程创建文件,umask通常设置为0,如果调用库函数创建文件,可设置为007。
2)调用fork,父进程exit
因为要调用setsid创建会话,需要确保调用进程(子进程)不是进程组组长,fork子进程可以确保这点。
3)调用setsid创建新会话
子进程调用setsid,成为新会话首进程,新进程组组长,断开终端连接
4)再次调用fork,父进程exit(可选)
主要是为了保证进程无法通过open /dev/tty再次获得终端。
5)调用chdir,将当前工作目录更改为根目录
6)调用close,关闭所有不需要的文件描述符
7)打开/dev/null文件,让文件描述符0,1,2指向该文件
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <string.h>
4. #include <fcntl.h>
5. #include <sys/resource.h>
6. #include <sys/time.h>
7. #include <errno.h>
8. #include <unistd.h>
9. #include <syslog.h>
10. #include <signal.h>
11. #include <sys/types.h>
12. #include <sys/stat.h>
13. #include <stdarg.h>
14.
15. #define MAXLINE 200
16.
17. static void err_doit(int errnoflag, int error, const char *fmt, va_list ap) {
18. char buf[MAXLINE];
19.
20. // 将格式化串fmt (参数ap) 转换成字符串存放到buf
21. vsnprintf(buf, MAXLINE - 1, fmt, ap);
22. if (errnoflag) {
23. // snprintf最后一个参数是const char*, vsnprintf最后一个参数是va_list, 功能一样
24. snprintf(buf + strlen(buf), MAXLINE - strlen(buf) - 1, ": %s", strerror(error));
25. }
26.
27. strcat(buf, "\n"); // buf末尾粘贴 "\n", 并以'\0'结束
28. fflush(stdout); // 以防stdout, stderr是相同设备, 先冲刷stdout
29. fputs(buf, stderr);
30. }
31.
32. /**
33. * 与系统调用相关的致命错误
34. * 打印消息和终止程序
35. */
36. static void err_quit (const char *fmt, ...) {
37. va_list ap;
38.
39. // va_start, va_end 配对获取可变参数..., 存放到va_list ap中
40. va_start(ap, fmt);
41. err_doit(0, 0, fmt, ap);
42. va_end(ap);
43.
44. exit(1);
45. }
46.
47. /**
48. * 将一个进程转换为守护进程
49. * 步骤:
50. * 1. 调用umask设置创建文件权限的umask
51. * 2. 调用fork, 让父进程退出, 子进程成为孤儿进程
52. * 3. 调用setsid, 创建新会话, 子进程成为新会话首进程以及进进程组组长, 断开控制终端
53. * 4. 再次调用fork, 并让父进程退出, 子进程不是会话首进程(防止再次获得控制终端)
54. * 5. 将当前工作目录修改为根目录, 防止无法卸载目录
55. * 6. 关闭不需要的文件描述符
56. * 7. 打开/dev/null, 使其具有文件描述符0,1,2
57. */
58. void daemonize(const char *pname) {
59. int i, fd0, fd1, fd2;
60. pid_t pid;
61. struct rlimit rl;
62. struct sigaction sa;
63.
64. // 1. 调用umask修改创建文件权限的umask
65. umask(0);
66.
67. if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
68. // <=> sysconf(_SC_OPEN_MAX);
69. err_quit("%s: getrlimit error", pname);
70. }
71.
72. // 2. 调用fork, 父进程退出, 子进程称为孤儿被init进程收养
73. if ((pid = fork()) < 0)
74. err_quit("%s: fork error", pname);
75. else if (pid > 0) { // 父进程
76. exit(0);
77. }
78.
79. // 3. 调用setsit, 创建新会话, 子进程成为首进程
80. setsid();
81. /* 发送SIGHUP信号情形:
82. 1)终端关闭时, 信号被发送到session首进程, 以及作为job提交的进程(shell 以&方式运行的进程)
83. 2)session首进程退出时, 该信号被发送到同session的所有前台进程
84. 3)若父进程退出, 导致进程组成为孤儿进程组, 且该进程组中有进程处于停止状态(收到SIGSTOP信号或SIGSTP), 该信号会被发送到进>程组每个成员
85. */
86. sa.sa_handler = SIG_IGN;
87. sigemptyset(&sa.sa_mask);
88. sa.sa_flags = 0;
89.
90. if (sigaction(SIGHUP, &sa, NULL) < 0)
91. err_quit("%s: can't ignore SIGHUP", pname);
92.
93. // 4. 再次调用fork
94. if ((pid = fork()) < 0)
95. err_quit("%s: fork error", pname);
96. else if (pid > 0)
97. exit(0);
98.
99. // 5. 改变工作目录为 "/"(根目录)
100. if (chdir("/") < 0)
101. err_quit("%s: can't change directory to /", pname);
102.
103. // 6. 关闭不需要的文件描述符
104. if (rl.rlim_max == RLIM_INFINITY)
105. rl.rlim_max = 1024;
106. for (i = 0; i < rl.rlim_max; ++i)
107. close(i);
108.
109. #if 1
110. // 7. 附加文件描述符0,1,2到 /dev/null
111. // 因为前面已经关闭了所有文件描述符, 因此重新open, dup得到的文件描述符是递增的
112. fd0 = open("/dev/null", O_RDWR);
113. fd1 = dup(0);
114. fd2 = dup(0);
115. #else
116. open("/dev/null", O_RDONLY);
117. open("/dev/null", O_RDWR);
118. open("/dev/null", O_RDWR);
119. #endif
120.
121. /* 建立到syslog的连接
122. LOG_CONS: 若无法发送到syslogd守护进程则登记到控制台
123. LOG_DAEMON: 标识消息发送进程的类型为 系统守护进程 */
124. openlog(pname, LOG_CONS, LOG_DAEMON);
125.
126. /* 文件描述符异常检查: 正常情况fd0, fd1, fd2应该分别是0,1,2 */
127. if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
128. syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
129. exit(1);
130. }
131. }
132.
1. #include <stdio.h>
2. #include <unistd.h>
3.
4. #include "daemonize.h"
5.
6. int main() {
7. char *s = "mydaemonize";
8.
9. printf("ready to convert a normal process into a daemonize process\n");
10. daemonize(s);
11.
12. while(1)
13. sleep(1);
14. return 0;
15. }
16.
-
1.3.3正确处理僵尸进程的方法⭐⭐⭐⭐
1)使用waitpid函数让父进程停下来等待子进程退出,并回收子进程。
1. #include<sys/wait.h>
2.
3. pid_t wait(int *stat_loc);
4. pid_t waitpid(pid_t pid, int *stat_loc, int options)
5. /*
1、第一个参数:准备回收的子进程pid
如果填写的是子进程pid,那么会等待回收指定的进程pid
如果填写的是-1,那么会等待回收所有进程
2、第二个参数:输出型参数,获取子进程的退出状态
3、第三个参数:进程等待方式
阻塞等待:执行waitpid后,父进程啥都不用干,等回收完成再继续循行
非阻塞等待:执行waitpid后,每隔一段时间,检测一下进程是否退出,若退出就回收,没退出就继续干自己的事情。
*/
2)子进程在退出的时候,会给父进程发送一个SIGCHLD信号,这个时候我们可以使用处理信号的知识来解决这个问题。
方式一:在信号处理函数中,使用wait回收进程
1. #include<unistd.h>
2. #include<stdlib.h>
3. #include<sys/types .h>
4. #include<sys/wait.h>
5.
6. void handler(int signo)
7. {
8. wait(NULL);
9. printf("process %d 收到信号: %d\n",getpid(),signo);
10. }
11. int main()
12. {
13. signal(17,handler);
14. pid t id = fork() ;
15. if(id == 0)
16. {
17. //子进程
18. printf("child process %d运行中,3s以后退出\n",getpid())
19. sleep(1);
20. exit(0);
21. }
22. //父进程
23. while(1)
24. {
25. printf("father process %d 正在干自己的事\n",getpid());
26. sleep(1);
27. return 0;
28. }
29. }
30.
方式二:设置信号处理方式为SIG_IGN(仅适用Linux)
signal(17, SIG_IGN);
3)将父进程杀掉