Web Server2
第二章 Linux 多进程开发
0. 解决 su 认证失败问题
1. 进程概述
1.1 程序和进程
程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:
◼二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释文件中的其他信息。(ELF可执 行连接格式)
◼机器语言指令:对程序算法进行编码。
◼程序入口地址:标识程序开始执行时的起始指令位置。
◼数据:程序文件包含的变量初始值和程序使用的字面量值(比如字符串)。
◼符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和运行时的符号解析(动态链 接)。
◼共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态连接器的路径名。
◼其他信息:程序文件还包含许多其他信息,用以描述如何创建进程。
◼进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
◼可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。
1.2 单道、多道程序设计
◼单道程序,即在计算机内存中只允许一个的程序运行。
◼多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态, 这些程序共享计算机系统资源。引入多道程序设计技术的根本目的是为了提高 CPU 的利用率。
◼对于一个单 CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU 上运行的程序只有一个。
◼在多道程序设计模型中,多个进程轮流使用 CPU。而当下常见 CPU 为纳秒级,1秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
1.3 时间片
◼ 时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。事实上,虽然一台计算机通常可能有多个 CPU,但是同一个 CPU 永远不可能真正地同时运行多个任务。在
只考虑一个 CPU 的情况下,这些进程“看起来像”同时运行的,实则是轮番穿插地运行,由于时间片通常很短(在 Linux 上为 5ms-800ms),用户不会感觉到。
◼ 时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
1.4 并行和并发
1.5 进程控制块
◼ 为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是 task_struct
结构体。
◼ 在 /usr/src/linux-headers-xxx/include/linux/sched.h
文件中可以查看 struct task_struct
结构体定义。其内部成员有很多,我们只需要掌握以下部分即可:
进程id:系统中每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
进程的状态:有就绪、运行、挂起、停止等状态
进程切换时需要保存和恢复的一些CPU寄存器
描述虚拟地址空间的信息
描述控制终端的信息
当前工作目录(Current Working Directory)
umask 掩码
文件描述符表,包含很多指向 file 结构体的指针
和信号相关的信息
用户 id 和组 id
会话(Session)和进程组
进程可以使用的资源上限(Resource Limit)
2. 进程状态转换
2.1 进程的状态
2.2 进程相关命令
2.3 进程号和相关函数
3. 创建进程
父子进程之间的关系:
区别:
1. fork() 函数的返回值不同
父进程中:>0 返回子进程的ID
子进程中:=0
2. pcb中的一些数据
当前的进程的id pid
当前的进程的父进程的id ppid
信号集
共同点:
某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
父子进程对变量是不是共享的?
刚开始的时候,是一样的,共享的。如果修改了数据,就不共享了。
读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main () { pid_t pid = fork(); if (pid > 0 ) { printf ("pid = %d\n" , pid); printf ("I am parent process, pid : %d, ppid = %d\n" , getpid(), getppid()); } else if (pid == 0 ) { printf ("I am child process, pid : %d, ppid : %d\n" , getpid(), getppid()); } for (int i = 0 ; i < 5 ; i++) { printf ("%d, pid = %d\n" , i, getpid()); sleep(1 ); } return 0 ; }
运行程序:可以看出,在两个进程在不同的时间片内运行,忽快忽慢,总体上呈现交替运行。
利用命令 ps aux
查看进程,发现父进程的父进程(63135)是一个 -bash
终端,即程序执行是由命令行终端打开的子进程。
4. 父子进程虚拟地址空间情况
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
具体演示如下:
执行fork()
时,子进程只拷贝内核区,用户区共享
当父进程写入时,会将元数据拷贝一份,指向新的数据,并修改。子进程仍然指向元数据
子进程修改数据时同理,拷贝一份,指针指向新数据。这样就互不影响了
5. 父子进程关系及GDB多进程调试
5.1 GDB 多进程调试命令
一个小插曲:fork()
函数是递归执行呢?还是只执行一次呢?
显然是后者,如果是前者,那 fork()
一调用程序就得崩溃。
即 子进程和父进程继续执行fork()调用之后的指令 。
看下面这个例子方便理解。
5.2 GDB 多进程调试过程(上)
下面进行 gdb 多进程调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main () { printf ("begin\n" ); if (fork() > 0 ) { printf ("I am parent process, pid : %d, ppid = %d\n" , getpid(), getppid()); for (int i = 0 ; i < 10 ; i++) { printf ("i = %d\n" , i); sleep(1 ); } } else { printf ("I am child process, pid : %d, ppid : %d\n" , getpid(), getppid()); for (int j = 0 ; j < 10 ; j++) { printf ("j = %d\n" , j); sleep(1 ); } } return 0 ; }
首先编译程序,记得带上编译指令 -g
在父进程和子进程分别打上一个断点,然后 run
,可见 GDB 默认跟踪父进程。
输入 show follow-fork-mode
查看当前默认跟踪的是父进程
输入 set follow-fork-mode child
修改为跟踪子进程
在输入 show follow-fork-mode
检查是否修改成功
首先查看当前默认的调试模式 show detach-on-fork
,当前为 on
,即除被调试的进程,其余进程均正常执行。
修改调试模式 set detach-on-fork off
,off
状态下,其余进程被挂起,等待执行
查看是否修改成功 show detach-on-fork
出现了!!!GDB 这里的多线程调试有 bug !!!
还是乖乖的装 gdb 9.x 吧。看来我的 7.x 和 教程中的 8.x 都不好使。
5.3 GDB 10.2 安装方法
下面是 Ubuntu18.04 安装 gdb 10.2 的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 首先进入管理员模式,否则后面一堆 sudo 很麻烦,如果这一步有问题,移步文章开头 su # 安装一些必要的包,不然后面会报错 apt-get install python3.6-dev apt install texinfo # cd 到目录 /usr/localcd /usr/local apt remove gdb apt-get install python-dev wget http://ftp.gnu.org/gnu/gdb/gdb-10.2.tar.gz tar -zxvf gdb-10.2.tar.gz cd gdb-10.2/ mkdir build cd build ../configure --with-python=/usr/bin/python3 --enable-targets=all make
make
的过程可能比较艰难,遇到问题解决问题即可,我遇到的问题如下:
问题1:
1 2 3 4 5 6 7 8 9 10 11 12 configure: WARNING: MPFR is missing or unusable; some features may be unavailable. checking whether to use python... /usr/bin/python3 checking for python... no configure: error: no usable python found at /usr/bin/python3 Makefile:10369: recipe for target 'configure-gdb' failed make[1]: *** [configure-gdb] Error 1 make[1]: 离开目录“/usr/local/gdb-10.2/build” Makefile:853: recipe for target 'all' failed make: *** [all] Error 2 // 解决方法: apt-get install python3.6-dev
问题2:
1 2 3 4 5 6 7 8 9 10 11 12 WARNING: 'makeinfo' is missing on your system. You should only need it if you modified a '.texi' file, or any other file indirectly affecting the aspect of the manual. You might want to install the Texinfo package: <http://www.gnu.org/software/texinfo/> The spurious makeinfo call might also be the consequence of using a buggy 'make' (AIX, DU, IRIX), in which case you might want to install GNU make: <http://www.gnu.org/software/make/> // 解决方法: apt install texinfo
安装完成!!!
5.4 GDB 多进程调试过程(下)
重启 gdb,继续调试:
1 2 3 4 set detach-on-fork off b 11 b 19 r
停在父进程处,子进程此时被挂起
查看进程 info inferiors
切换到2号进程(注意这里不是进程id) inferior 2
继续调试,到断点处 c
继续执行完父进程 c
查看进程状态 info inferiors
查看发现子进程的 Description 是 <null>
,即此时子进程已执行结束
切换回父进程 inferior 1
继续调试父进程 n
c
此时再查看发现子进程已经被关闭,父进程刚刚结束
还有一个有趣的命令,可以在调试的过程中让挂起或者正在调试的程序恢复执行状态。
set follow-fork-mode child
set detach-on-fork off
r
切换来调试父进程 inferior 1
n
让父进程脱离调试,继续执行 detach inferiors 1
父进程执行结束后,继续输入 n
,提示该程序已经运行结束
由于前面没有重启 gdb,所以此时创建的子进程的调试进程号是 3,info inferiors
detach inferiors 3
让子进程继续执行至结束
6. exec 函数族
函数族:一系列功能相同或者相似的函数。
调用 exec()
函数可以在执行的进程内部开启另一个可执行程序,与fork()
不同,exec()
不会开启一个新的进程,而是将部分当前进程的数据替换为新开启可执行文件的数据,然后开始执行新的程序。这样显然不太合理,因为原来的程序也要执行。所以一般会先fork()
一个子进程,然后在子进程中调用exec()
。
1 2 3 4 5 6 7 8 9 10 11 ◼ int execl (const char *path, const char *arg, ...) ; ◼ int execlp (const char *file, const char *arg, ... ) ; ◼ int execle (const char *path, const char *arg, ...) ; ◼ int execv (const char *path, char *const argv[]) ; ◼ int execvp (const char *file, char *const argv[]) ; ◼ int execvpe (const char *file, char *const argv[], char *const envp[]) ; ◼ int execve (const char *filename, char *const argv[], char *const envp[]) ; l(list ) 参数地址列表,以空指针结尾 v(vector ) 存有各参数地址的指针数组的地址 p(path) 按 PATH 环境变量指定的目录搜索可执行文件 e(environment) 存有环境变量字符串地址的指针数组的地址
6.1 execl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <unistd.h> #include <stdio.h> int main () { pid_t pid = fork(); if (pid > 0 ) { printf ("I am parent process, pid : %d\n" , getpid()); sleep(1 ); } else if (pid == 0 ) { execl("hello" , "hello" , NULL ); printf ("I am child process, pid : %d\n" , getpid()); } for (int i = 0 ; i < 3 ; i++) { printf ("i = %d, pid = %d\n" , i, getpid()); } return 0 ; }
执行程序可以看出,父进程正常执行。创建一个子进程,子进程调用可执行文件 hello
,子进程的内容不执行,因为替换成 hello
的用户区内容了。
但是出现了命令行错乱,这个是 孤儿进程 导致的,具体细节后面讨论。
只需在父进程第一条语句处添加一个 sleep(1)
即可解决问题。
6.2 execlp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <unistd.h> #include <stdio.h> int main () { pid_t pid = fork(); if (pid > 0 ) { printf ("I am parent process, pid : %d\n" , getpid()); sleep(1 ); } else if (pid == 0 ) { execlp("ps" , "ps" , "aux" , NULL ); printf ("I am child process, pid : %d\n" , getpid()); } for (int i = 0 ; i < 3 ; i++) { printf ("i = %d, pid = %d\n" , i, getpid()); } return 0 ; }
6.3 execv
1 2 3 4 int execv (const char *path, char *const argv[]) ; argv是需要的参数的一个字符串数组 char * argv[] = {"ps" , "aux" , NULL }; execv("/bin/ps" , argv);
6.4 execve
1 2 3 int execve (const char *filename, char *const argv[], char *const envp[]) ; char * envp[] = {"/home/nowcoder" , "/home/bbb" , "/home/aaa" };
7. 进程控制
7.1 进程退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /* #include <stdlib.h> void exit(int status); #include <unistd.h> void _exit(int status); status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。 */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { printf("hello\n"); printf("world"); exit(0); // _exit(0); return 0; }
使用 exit(0)
可以看到 world 被正常打印了,因为该函数调用时刷新了 I/O 缓冲。
改用 _exit(0)
,可以看到只打印了 hello
因为 \n
清空了缓冲区,而 world 被放在了缓冲区中,程序直接结束,所以没有打印出来。
一般用 exit()
不用 _exit()
。
7.2 孤儿进程
◼ 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程 (Orphan Process)。
◼ 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。
◼ init 进程会回收孤儿进程的数据,因此孤儿进程并不会有什么危害。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main () { pid_t pid = fork(); if (pid > 0 ) { printf ("I am parent, pid : %d, ppid : %d\n" , getpid(), getppid()); } else if (pid == 0 ) { sleep(1 ); printf ("I am child, pid : %d, ppid : %d\n" , getpid(), getppid()); } for (int i = 0 ; i < 3 ; i++) { printf ("i = %d, pid = %d\n" , i, getpid()); } return 0 ; }
这里的子进程就是一个孤儿进程。
7.3 僵尸进程
◼ 每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
◼ 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
◼ 僵尸进程不能被 kill -9
杀死,这样就会导致一个问题,如果父进程不调用 wait()
或 waitpid()
的话,那么保留的那段信息就不会释 放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统 不能产生新的进程,此即为僵尸进程的危害,应当避免。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main () { pid_t pid = fork(); if (pid > 0 ) { while (1 ) { printf ("I am parent, pid : %d, ppid : %d\n" , getpid(), getppid()); } } else if (pid == 0 ) { sleep(1 ); printf ("I am child, pid : %d, ppid : %d\n" , getpid(), getppid()); } for (int i = 0 ; i < 3 ; i++) { printf ("i = %d, pid = %d\n" , i, getpid()); } return 0 ; }
执行程序后,子程序执行完了,但是父进程一值在卡在循环里,没办法回收子进程,这时子进程就成了一个僵尸进程。
执行 ps aux
查看发现,子进程成了一个僵尸进程
使用 kill -9 120653
发现杀不掉僵尸进程。
那怎么办呢?一种使用 wait()
函数回收。简单点的话可以将其父进程杀死,让 init 回收僵尸进程。
ctrl + C
终止父进程,查看 ps aux
发现,僵尸进程已经被回收了。
8. wait 函数
8.1 wait() 函数
◼ 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
◼ 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
◼ wait()
和 waitpid()
函数的功能一样,区别在于,wait()
函数会阻塞,waitpid()
可以设置不阻塞,waitpid()
还可以指定等待哪个进程结束。
◼ 注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main () { pid_t pid; for (int i = 0 ; i < 5 ; i++) { pid = fork(); if (pid == 0 ) { break ; } } if (pid > 0 ) { while (1 ) { printf ("parent, pid : %d\n" , getpid()); int ret = wait(NULL ); if (ret == -1 ) { break ; } printf ("child die, pid : %d\n" , ret); sleep(2 ); } } else if (pid == 0 ) { while (1 ) { printf ("child, pid : %d\n" , getpid()); sleep(2 ); } } return 0 ; }
执行程序,可以看到一直在执行 child 进程,而父进程阻塞在 wait()
函数中:
手动杀死一个子进程 kill -9 121590
可以在运行程序中看出,child 进程死亡,父进程检测到了,并且进入下一个 wait()
的阻塞状态:
继续杀死其他的子进程,最后,父进程均检测到,所有子进程 kill 后,父进程收到 -1,跳出 wait()
的进程回收循环。
8.2 退出信息相关的宏函数
◼ WIFEXITED(status) 非0,进程正常退出
◼ WEXITSTATUS(status) 如果上宏为真,获取进程退出的状态(exit的参数)
◼ WIFSIGNALED(status) 非0,进程异常终止
◼ WTERMSIG(status) 如果上宏为真,获取使进程终止的信号编号
◼ WIFSTOPPED(status) 非0,进程处于暂停状态
◼ WSTOPSIG(status) 如果上宏为真,获取使进程暂停的信号的编号
◼ WIFCONTINUED(status) 非0,进程暂停后已经继续运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main () { pid_t pid; for (int i = 0 ; i < 5 ; i++) { pid = fork(); if (pid == 0 ) { break ; } } if (pid > 0 ) { while (1 ) { printf ("parent, pid : %d\n" , getpid()); int st; int ret = wait(&st); if (ret == -1 ) { break ; } if (WIFEXITED(st)) { printf ("退出的状态码: %d\n" , WEXITSTATUS(st)); } if (WIFSIGNALED(st)) { printf ("被那个信号干掉了:%d\n" , WTERMSIG(st)); } printf ("child die, pid : %d\n" , ret); sleep(2 ); } } else if (pid == 0 ) { printf ("child, pid : %d\n" , getpid()); sleep(2 ); exit (0 ); } return 0 ; }
测试正常退出:
1 2 3 4 5 6 7 8 9 10 ... } else if (pid == 0 ) { printf ("child, pid : %d\n" , getpid()); sleep(2 ); exit (0 ); } ...
执行程序后:
测试异常退出(信号终止):
1 2 3 4 5 6 7 8 9 ... } else if (pid == 0 ) { while (1 ) { printf ("child, pid : %d\n" , getpid()); sleep(2 ); } } ...
查看进程 id
使用 kill -9 process_id
将 5 个子进程全部杀死,结果如下
9. waitpid 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 code... int st; int ret = waitpid(-1 , &st, WNOHANG); ...code
把 wait 的代码复用过来,用 waitpid(-1, &st, 0)
代替 wait(&st)
实现对所有子进程的阻塞回收。
下面来看非阻塞的例子:
非阻塞的情况下,父进程可以继续执行自己的任务,完成一轮回过头继续检查有没有需要回收的子进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main () { pid_t pid; for (int i = 0 ; i < 5 ; i++) { pid = fork(); if (pid == 0 ) { break ; } } if (pid > 0 ) { while (1 ) { printf ("parent, pid : %d\n" , getpid()); sleep(2 ); int st; int ret = waitpid(-1 , &st, WNOHANG); if (ret == -1 ) { break ; } else if (ret == 0 ) { continue ; } else if (ret > 0 ) { if (WIFEXITED(st)) { printf ("退出的状态码: %d\n" , WEXITSTATUS(st)); } if (WIFSIGNALED(st)) { printf ("被那个信号干掉了:%d\n" , WTERMSIG(st)); } printf ("child die, pid : %d\n" , ret); } } } else if (pid == 0 ) { while (1 ) { printf ("child, pid : %d\n" , getpid()); sleep(2 ); } } return 0 ; }
执行程序,可以看出父进程没有被阻塞,和子进程在交替执行:
我们杀死所有子进程
可以看到,父进程每次执行到 waitpid()
都能检测到子进程死亡,并回收。当所有子进程都被回收后,再次遇到 waitpid
检测到 -1
说明没有可以回收的子进程了,父进程结束。
10. 进程间通信
◼ 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
◼ 但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC:Inter Processes Communication )。
◼ 进程间通信的目的:
◼ 数据传输:一个进程需要将它的数据发送给另一个进程。
◼ 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
◼ 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
◼ 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异 常,并能够及时知道它的状态改变。
10.1 匿名管道
10.1.1 基本知识
◼ 管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。
◼ 统计一个目录中文件的数目命令:ls | wc –l
,为了执行该命令,shell 创建了两个进程来分别执行 ls 和 wc。
|
相当于一个管道。
◼ 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
◼ 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对 管道进行操作。
◼ 一个管道是一个字节流,使用管道时不存在消息(包含文件头和数据的结构)或者消息边界的概念,从管道读取数据的进程可以读取 任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
◼ 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
◼ 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
单工:只能向一个方向通信和传输数据。遥控器可以发送信号给电视机,但是反过来不行。
双工:可以双向通信和双向同时传输数据。打电话,可以双向通信,并且同时发送数据(我说你能听,你说我也能听)。
半双工:可以双向通信,但是某一时刻只有一个通信方向可以传输数据。比如对讲机,我说话的时候你只能听,你说话的时候我只能听。
◼ 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机 的访问数据。
◼ 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
匿名管道用于有关系的进程,比如父子关系。
有名管道用于没有关系的进程。
子进程 fork() 父进程后,复制文件描述符,可以通过匿名管道通信:
管道数据结构:
10.1.2 匿名管道的使用:
◼ 创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
◼ 查看管道缓冲大小命令
ulimit –a
◼ 查看管道缓冲大小函数
#include <unistd.h>
long fpathconf(int fd, int name);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main () { int pipefd[2 ]; int ret = pipe(pipefd); if (ret == -1 ) { perror("pipe" ); exit (0 ); } pid_t pid = fork(); if (pid > 0 ) { char buf[1024 ] = {0 }; read(pipefd[0 ], buf, sizeof (buf)); printf ("parent recv : %s, pid : %d\n" , buf, getpid()); } else if (pid == 0 ) { char *str = "hello, I am child" ; write(pipefd[1 ], str, strlen (str)); } return 0 ; }
执行程序,父进程收到来自子进程的消息:
管道默认是阻塞的,给子进程的数据发送一个延时,那么父进程一直等到管道读取到数据才执行后面的程序:
下面看一个一发一收的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main () { int pipefd[2 ]; int ret = pipe(pipefd); if (ret == -1 ) { perror("pipe" ); exit (0 ); } pid_t pid = fork(); if (pid > 0 ) { printf ("I am parent, pid : %d\n" , getpid()); char buf[1024 ] = {0 }; while (1 ) { read(pipefd[0 ], buf, sizeof (buf)); printf ("parent recv : %s, pid : %d\n" , buf, getpid()); char *str = "hello, I am parent" ; write(pipefd[1 ], str, strlen (str)); sleep(2 ); } } else if (pid == 0 ) { printf ("I am child, pid : %d\n" , getpid()); char buf[1024 ] = {0 }; while (1 ) { char *str = "hello, I am child" ; write(pipefd[1 ], str, strlen (str)); sleep(2 ); read(pipefd[0 ], buf, sizeof (buf)); printf ("child recv : %s, pid : %d\n" , buf, getpid()); } } return 0 ; }
可以看到代码执行的过程并不是我们想的一发一收,而是有错乱,比如一个进程发送的数据立即被自己接收了。
即便在 write()
之后加了延时,也不能解决这个问题,因为在某个整数倍时间后,可能又会出现在自己的时间片内收到自己写入管道的数据这样的现象。
首先项目里肯定不能用 sleep()
,而且通常让一方发送时,另一方只能接收,所以让子进程写管道,关闭读。让父进程读管道,关闭写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 int main () { int pipefd[2 ]; int ret = pipe(pipefd); if (ret == -1 ) { perror("pipe" ); exit (0 ); } pid_t pid = fork(); if (pid > 0 ) { printf ("I am parent, pid : %d\n" , getpid()); close(pipefd[1 ]); char buf[1024 ] = {0 }; while (1 ) { read(pipefd[0 ], buf, sizeof (buf)); printf ("parent recv : %s, pid : %d\n" , buf, getpid()); } } else if (pid == 0 ) { printf ("I am child, pid : %d\n" , getpid()); close(pipefd[0 ]); while (1 ) { char *str = "hello, I am child\n" ; write(pipefd[1 ], str, strlen (str)); } } return 0 ; }
代码执行情况如下,可以看到,写入太快,一次可以读出很多数据,但是执行正常。
管道缓冲大小:
通过命令 ulimit -a
查看管道缓冲的大小:
也可以通过 ulimit -p
指令修改管道大小
也可以用函数查看管道缓冲大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main () { int pipefd[2 ]; int ret = pipe(pipefd); long size = fpathconf(pipefd[0 ], _PC_PIPE_BUF); printf ("pipe size = %ld\n" , size); return 0 ; }
可以看到默认大小是 8 * 512 = 4k
10.1.3 匿名管道通信案例
实现:ps aux | grep xxx
父子进程间通信
不完整版,只同过管道通信实现了 pa aux
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <wait.h> int main () { int fd[2 ]; int ret = pipe(fd); if (ret == -1 ) { perror("pipe" ); exit (0 ); } pid_t pid = fork(); if (pid > 0 ) { close(fd[1 ]); char buf[1024 ]; int len = -1 ; while ((len = read(fd[0 ], buf, sizeof (buf) - 1 )) > 0 ) { printf ("%s" , buf); memset (buf, 0 , 1024 ); } wait(NULL ); } else if (pid == 0 ) { close(fd[0 ]); dup2(fd[1 ], STDERR_FILENO); execlp("ps" , "ps" , "aux" , NULL ); perror("execlp" ); } else { perror("fork" ); exit (0 ); } return 0 ; }
编译运行即可看到进程信息,但是管道缓存只有 4k
,可能信息不完整,加个循环,多次向管道写数据,并读出来即可。
10.1.4 管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。
总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
read() 阻塞与非阻塞样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> int main () { int pipefd[2 ]; int ret = pipe(pipefd); if (ret == -1 ) { perror("pipe" ); exit (0 ); } int flags = fcntl(pipefd[0 ], F_GETFL); flags |= O_NONBLOCK; fcntl(pipefd[0 ], F_SETFL, flags); pid_t pid = fork(); if (pid > 0 ) { printf ("I am parent process, pid = %d\n" , getpid()); close(pipefd[1 ]); char buf[1024 ] = {0 }; while (1 ) { int len = read(pipefd[0 ], buf, sizeof (buf)); printf ("len = %d\n" , len); printf ("parent recv : %s, pid = %d\n" , buf, getpid()); printf ("size = %ld\n" , sizeof (buf)); memset (buf, 0 , sizeof (buf)); sleep(1 ); } } else if (pid == 0 ) { printf ("I am child process, pid = %d\n" , getpid()); close(pipefd[0 ]); while (1 ) { char *str = "hello, I am child" ; write(pipefd[1 ], str, strlen (str)); sleep(5 ); } } else { perror("fork" ); exit (0 ); } return 0 ; }
默认执行情况:read() 会等待 write() 发送
设置为非阻塞的情况:read() 一直读取管道,没有写入数据则收到 -1
10.2 有名管道
10.2.1 基本知识
◼ 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。
◼ 有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
◼ 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。
◼ 有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:
FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。
当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。
◼ 通过命令创建有名管道
mkfifo 名字
◼ 通过函数创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
◼ 一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/O 函数都可用于 fifo。如:close、read、write、unlink 等。
◼ FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek()等文件定位操作。
10.2.2 有名管道的使用
命令行:
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main () { int ret = access("fifo1" , F_OK); if (ret == -1 ) { printf ("管道不存在,创建管道\n" ); ret = mkfifo("fifo1" , 0664 ); if (ret == - 1 ) { perror("mkfifo" ); exit (0 ); } } return 0 ; }
有名管道的文件读写:
write()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> int main () { int ret = access("test" , F_OK); if (ret == -1 ) { printf ("管道不存在,创建管道\n" ); ret = mkfifo("test" , 0664 ); if (ret == - 1 ) { perror("mkfifo" ); exit (0 ); } } int fd = open("test" , O_WRONLY); if (fd == -1 ) { perror("open" ); exit (0 ); } for (int i = 0 ; i < 100 ; i++) { char buf[1024 ]; sprintf (buf, "hello, %d\n" , i); printf ("write data : %s\n" , buf); write(fd, buf, strlen (buf)); sleep(1 ); } close(fd); return 0 ; }
read()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> int main () { int fd = open("test" , O_RDONLY); if (fd == -1 ) { perror("open" ); exit (0 ); } while (1 ) { char buf[1024 ] = {0 }; int len = read(fd, buf, sizeof (buf)); if (len == 0 ) { printf ("写端断开连接了...\n" ); break ; } printf ("recv buf : %s\n" , buf); } close(fd); return 0 ; }
无论先执行写,还是先执行读管道,都会阻塞,等待另一方执行,然后通信。
首先杀死写进程。
如果先杀死读管道的进程,那么写管道的进程会导致管道破裂,发出异常信号,让进程中断。
有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道
读管道:
10.2.3 有名管道实现简单版聊天功能
chatA.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> int main () { int ret = access("fifo1" , F_OK); if (ret == -1 ) { printf ("文件不存在,创建对应的有名管道\n" ); ret = mkfifo("fifo1" , 0664 ); if (ret == -1 ) { perror("mkfifo" ); exit (0 ); } } ret = access("fifo2" , F_OK); if (ret == -1 ) { printf ("文件不存在,创建对应的有名管道\n" ); ret = mkfifo("fifo2" , 0664 ); if (ret == -1 ) { perror("mkfifo" ); exit (0 ); } } int fdw = open("fifo1" , O_WRONLY); if (fdw == -1 ) { perror("open" ); exit (0 ); } printf ("打开管道fifo1成功,等待写入...\n" ); int fdr = open("fifo2" , O_RDONLY); if (fdr == -1 ) { perror("open" ); exit (0 ); } printf ("打开管道fifo2成功,等待读取...\n" ); char buf[128 ]; while (1 ) { memset (buf, 0 , 128 ); fgets(buf, 128 , stdin ); write(fdw, buf, strlen (buf)); if (ret == -1 ) { perror("write" ); exit (0 ); } memset (buf, 0 , sizeof (buf)); ret = read(fdr, buf, sizeof (buf)); if (ret <= 0 ) { perror("read" ); break ; } printf ("buf : %s\n" , buf); } close(fdw); close(fdr); return 0 ; }
chatB.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <string.h> int main () { int ret = access("fifo1" , F_OK); if (ret == -1 ) { printf ("文件不存在,创建对应的有名管道\n" ); ret = mkfifo("fifo1" , 0664 ); if (ret == -1 ) { perror("mkfifo" ); exit (0 ); } } ret = access("fifo2" , F_OK); if (ret == -1 ) { printf ("文件不存在,创建对应的有名管道\n" ); ret = mkfifo("fifo2" , 0664 ); if (ret == -1 ) { perror("mkfifo" ); exit (0 ); } } int fdr = open("fifo1" , O_RDONLY); if (fdr == -1 ) { perror("open" ); exit (0 ); } printf ("打开管道fifo1成功,等待读取...\n" ); int fdw = open("fifo2" , O_WRONLY); if (fdw == -1 ) { perror("open" ); exit (0 ); } printf ("打开管道fifo2成功,等待写入...\n" ); char buf[128 ]; while (1 ) { memset (buf, 0 , sizeof (buf)); ret = read(fdr, buf, sizeof (buf)); if (ret <= 0 ) { perror("read" ); break ; } printf ("buf : %s\n" , buf); memset (buf, 0 , 128 ); fgets(buf, 128 , stdin ); write(fdw, buf, strlen (buf)); if (ret == -1 ) { perror("write" ); exit (0 ); } } close(fdw); close(fdr); return 0 ; }
10.3 内存映射
10.3.1 基本知识
内存映射也是一种通信的方式,由于直接对内存进行操作,所以该方式效率较高。
◼ 内存映射(Memory-mapped I/O)是将磁盘 文件 的数据映射到内存,用户通过修改内存就能修改磁盘文件。
10.3.2 内存映射相关系统调用
◼ #include <sys/mman.h>
◼ void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
◼ int munmap(void *addr, size_t length);
10.3.3 案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <wait.h> int main () { int fd = open("test.txt" , O_RDWR); int size = lseek(fd, 0 , SEEK_END); void *ptr = mmap(NULL , size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); if (ptr == MAP_FAILED) { perror("mmap" ); exit (0 ); } pid_t pid = fork(); if (pid > 0 ) { wait(NULL ); char buf[64 ]; strcpy (buf, (char *)ptr); printf ("read data : %s\n" , buf); } else if (pid == 0 ) { strcpy ((char *)ptr, "nihao a, son!!!" ); } munmap(ptr, size); return 0 ; }
10.3.4 问题探究
如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(…);
ptr++; 可以对其进行++操作(但是不建议)
munmap(ptr, len); // 错误,要保存地址,释放时使用
如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限保持一致。(port权限要小于等于open()的权限)
如果文件偏移量为1000会怎样?
偏移量必须是4K的整数倍,返回MAP_FAILED
mmap什么情况下会调用失败?
第二个参数:length = 0
第三个参数:prot
只指定了写权限
prot PROT_READ | PROT_WRITE 第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
可以open的时候O_CREAT一个新文件来创建映射区吗?
可以的,但是创建的文件的大小如果为0的话,肯定不行
可以对新的文件进行扩展
mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("XXX");
mmap(,,,,fd,0);
// 相当于对传进来的 fd 进行了拷贝,所以不会出问题
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响。
对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,,,,,);
操作 4K 的位置
越界操作操作的是非法的内存 -> 段错误
10.3.5 内存映射实现文件拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <stdio.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main () { int fd = open("english.txt" , O_RDWR); if (fd == -1 ) { perror("open" ); exit (0 ); } int len = lseek(fd, 0 , SEEK_END); int fd1 = open("cpy.txt" , O_RDWR | O_CREAT, 0664 ); if (fd1 == -1 ) { perror("open" ); exit (0 ); } truncate("cpy.txt" , len); void *ptr = mmap(NULL , len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); void *ptr1 = mmap(NULL , len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0 ); if (ptr == MAP_FAILED) { perror("mmap" ); exit (0 ); } if (ptr1 == MAP_FAILED) { perror("mmap" ); exit (0 ); } memcpy (ptr1, ptr, len); munmap(ptr1, len); munmap(ptr, len); close(fd1); close(fd); return 0 ; }
10.3.6 匿名映射实现进程间通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/wait.h> #include <stdio.h> int main () { int len = 4096 ; void *ptr = mmap(NULL , len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1 , 0 ); if (ptr == MAP_FAILED) { perror("mmap" ); exit (0 ); } pid_t pid = fork(); if (pid > 0 ) { strcpy ((char *)ptr, "hello, world" ); wait(NULL ); } else if (pid == 0 ) { sleep(1 ); printf ("%s\n" , (char *)ptr); } int ret = munmap(ptr, len); if (ret == -1 ) { perror("munmap" ); exit (0 ); } return 0 ; }
10.4 信号
10.4.1 信号的概念
◼ 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
◼ 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程发送一个中断信号。
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
运行 kill 命令或调用 kill 函数。
◼ 使用信号的两个主要目的是:
让进程知道已经发生了一个特定的事情。
强迫进程执行它自己代码中的信号处理程序。
◼ 信号的特点:
简单
不能携带大量信息
满足某个特定条件才发送
优先级比较高
◼ 查看系统定义的信号列表:kill –l
◼ 前 31 个信号为常规信号,其余为实时信号。
共 62 个,前31个通用且关键。
◼ 查看信号的详细信息:man 7 signal
◼ 信号的 5 中默认处理动作
◼ 信号的几种状态:产生、未决、递达
◼ SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
使用 core 文件查看错误信息
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> #include <string.h> int main () { char *buf; strcpy (buf, "hello" ); return 0 ; }
为了使用 gdb 查看生成错误信息,使用 gcc core.c -g
执行程序后发生一个段错误(核心已转存),这个核心里存放的就是我们的错误信息
首先,查看 core
文件的大小:ulimit -a
核心文件的大小最大为 1024,还可以设置为 unlimited ,即随意值。ulimit -c 1024
为了正常生成 core 文件,输入 sudo service apport stop
然后运行文件 ./a.out
,可以看到在当前文件夹下生成了一个 core 核心文件
使用 gdb 调试:
gdb a.out
core-file core
终于知道 (核心已转存)
是什么了!!!开心!
10.4.2 信号相关函数
kill、raise、abort
◼ int kill(pid_t pid, int sig);
◼ int raise(int sig);
◼ void abort(void);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <stdio.h> #include <sys/types.h> #include <signal.h> #include <unistd.h> int main () { pid_t pid = fork(); if (pid > 0 ) { printf ("I am parent\n" ); sleep(2 ); printf ("I will kill child process\n" ); kill(pid, SIGINT); } else if (pid == 0 ) { for (int i = 0 ; i < 5 ; i++) { printf ("I am child\n" ); sleep(1 ); } } return 0 ; }
执行程序,用父进程给子进程发送一个 SIGINT 信号。
alarm 函数
◼ unsigned int alarm(unsigned int seconds);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <stdio.h> #include <unistd.h> int main () { int seconds = alarm(6 ); printf ("seconds = %d\n" , seconds); sleep(2 ); seconds = alarm(2 ); printf ("seconds = %d\n" , seconds); while (1 ) { } return 0 ; }
尝试打印 1s 内,数据能自增几次:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <unistd.h> int main () { alarm(1 ); int i = 0 ; while (1 ) { printf ("%d\n" , i++); } return 0 ; }
直接输出到终端,耗费时间长,只输出了 511088 次。
重定向到文件中,省去了标准输出得时间,i 自增了 10143368 次。
setitimer 定时器函数
◼ int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);
可以看到 setitimer 定时器无阻塞的输出了如下提示:
然后经过 3s 的延迟后,发送 SIGALRM 信号,终止程序。
但是由于还没有捕捉信号,所以体现不出周期性定时。
10.5 信号捕捉
10.5.1 signal 信号捕捉函数
首先解释一下 typedef void (*sighandler_t)(int);
其实就是 把 sighandler_t 定义成一个类型(向 int 那样)。
像 typedef long long ll;
我觉得写成下面这样可能更好理解(当然这样肯定不对) :
typedef void (*sighandler_t)(int) sighandler_t;
#define 在预处理的时候拷贝,typedef 在编译的时候会参与,所以二者并不等价。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <signal.h> void myalarm (int num) { printf ("捕捉到的信号的编号是:%d\n" , num); printf ("xxxxxx\n" ); } int main () { signal(SIGALRM, myalarm); struct itimerval new_value ; new_value.it_interval.tv_sec = 2 ; new_value.it_interval.tv_usec = 0 ; new_value.it_value.tv_sec = 3 ; new_value.it_value.tv_usec = 0 ; int ret = setitimer(ITIMER_REAL, &new_value, NULL ); printf ("定时器开始了...\n" ); if (ret == -1 ) { perror("setitimer" ); exit (0 ); } getchar(); return 0 ; }
signal(SIGALRM, SIG_IGN);
忽略信号,执行程序后,SIGALRM 信号被捕获并忽略,所以程序没有终止,停在了 getchar()
signal(SIGALRM, SIG_DFL);
使用信号默认的行为,延迟三秒后,捕获信号,执行信号 SIGALRM 默认含义,终止进程
signal(SIGALRM, myalarm);
使用回调函数 myalarm 捕获信号后,执行回调函数:可以看出是周期性执行,打印捕获到的信号
10.5.2 信号集及相关函数
◼ 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t
。
◼ 在 PCB 中有两个非常重要的信号集。一个称之为“ 阻塞信号集 ”,另一个称之为 “ 未决信号集 ” 。这两个信号集都是内核使用位图(二进制位)机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
◼ 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
◼ 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
◼ 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态
这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
- 阻塞信号集默认不阻塞任何的信号
- 如果想要阻塞某些信号需要用户调用系统的API
在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
- 如果没有阻塞,这个信号就被处理
- 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
sigset.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include <signal.h> #include <stdio.h> int main () { sigset_t set ; sigemptyset(&set ); int ret = sigismember(&set , SIGINT); if (ret == 0 ) { printf ("被阻塞\n" ); } else if (ret == 1 ) { printf ("不阻塞\n" ); } sigaddset(&set , SIGINT); sigaddset(&set , SIGQUIT); ret = sigismember(&set , SIGINT); if (ret == 0 ) { printf ("被阻塞\n" ); } else if (ret == 1 ) { printf ("不阻塞\n" ); } ret = sigismember(&set , SIGQUIT); if (ret == 0 ) { printf ("被阻塞\n" ); } else if (ret == 1 ) { printf ("不阻塞\n" ); } sigdelset(&set , SIGINT); ret = sigismember(&set , SIGINT); if (ret == 0 ) { printf ("被阻塞\n" ); } else if (ret == 1 ) { printf ("不阻塞\n" ); } return 0 ; }
10.5.3 sigprocmask 函数使用
上面的几个函数只能操作自定义信号集。但是 sigprocmask
可以操作内核信号集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> int main () { sigset_t set ; sigemptyset(&set ); sigaddset(&set , SIGINT); sigaddset(&set , SIGQUIT); sigprocmask(SIG_BLOCK, &set , NULL ); int num = 0 ; while (1 ) { num++; sigset_t pendingset; sigemptyset(&pendingset); sigpending(&pendingset); for (int i = 1 ; i <= 31 ; i++) { int ret = sigismember(&pendingset, i); if (ret == 1 ) { printf ("1" ); }else if (ret == 0 ) { printf ("0" ); }else { perror("sigismember" ); exit (0 ); } } printf ("\n" ); sleep(1 ); if (num == 10 ) { sigprocmask(SIG_UNBLOCK, &set , NULL ); } } return 0 ; }
可以看到执行过程中,键盘输入信号 SIGINT 和 SIGQUIT,成功被设置为阻塞,是未决信号,但是由于内核阻塞标志位被置为1,所以暂不执行。打印到第十次时,解除 SIGINT,和 SIGQUIT 的阻塞。此时,未决信号集中的 SIGINT 和 SIGQUIT 执行,进程终止。
简单介绍一下前台和后台:
上面的例子中注释掉解除阻塞的部分:
查看进程发现没有终止
10.5.4 sigaction 信号捕捉函数
SIGKILL 和 SIGSTOP 信号不能被捕捉,否则,黑客可以利用这一点来写木马,不停的捕捉 SIGKILL 信号,跳转到回调函数去执行木马,还没办法杀死这个进程,这很危险。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <signal.h> void myalarm (int num) { printf ("捕捉到的信号的编号是:%d\n" , num); printf ("xxxxxx\n" ); } int main () { struct sigaction act ; act.sa_flags = 0 ; act.sa_handler = myalarm; sigemptyset(&act.sa_mask); sigaction(SIGALRM, &act, NULL ); struct itimerval new_value ; new_value.it_interval.tv_sec = 2 ; new_value.it_interval.tv_usec = 0 ; new_value.it_value.tv_sec = 3 ; new_value.it_value.tv_usec = 0 ; int ret = setitimer(ITIMER_REAL, &new_value, NULL ); printf ("定时器开始了...\n" ); if (ret == -1 ) { perror("setitimer" ); exit (0 ); } while (1 ); return 0 ; }
可以看到信号捕捉正常:
三个需要注意的点:
信号捕捉处理的过程中会使用了临时的阻塞信号集,信号处理完之后恢复到系统内核PCB的信号阻塞集。
在执行某个回调函数期间,再次收到某个信号,会默认屏蔽掉,等执行完再执行。
阻塞的常规信号不支持排队,因为标志位只有0,1不能记录一个信号发送了几次。
10.5.5 SIGCHLD 信号
◼ SIGCHLD信号产生的条件
子进程终止时
子进程接收到 SIGSTOP 信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时
◼ 以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号
下面的代码实现了:在子进程成为暂停(僵尸进程)时,要给父进程发送 SIGCHLD 信号,捕获该信号,并利用 waitpid 回收僵尸进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <signal.h> #include <sys/wait.h> void myFun (int num) { printf ("捕捉到的信号 :%d\n" , num); while (1 ) { int ret = waitpid(-1 , NULL , WNOHANG); if (ret > 0 ) { printf ("child die, pid : %d\n" , ret); } else if (ret == 0 ) { break ; } else if (ret == -1 ) { break ; } } } int main () { sigset_t set ; sigemptyset(&set ); sigaddset(&set , SIGCHLD); sigprocmask(SIG_BLOCK, &set , NULL ); pid_t pid; for (int i = 0 ; i < 20 ; i++) { pid = fork(); if (pid == 0 ) { break ; } } if (pid > 0 ) { struct sigaction act ; act.sa_flags = 0 ; act.sa_handler = myFun; sigemptyset(&act.sa_mask); sigaction(SIGCHLD, &act, NULL ); } else if (pid == 0 ) { printf ("child process pid : %d\n" , getpid()); } return 0 ; }
使用 ps aux
查看,此时没有相关的子进程是僵尸进程。
为什么一开始将信号集设置为阻塞可以回收所有的僵尸进程呢?未决信号集和阻塞信号集不是不能排队吗?只有一个标志位,怎么能知道具体有多少个僵尸进程呢?
我们分开考虑 信号集 和 waitpid ,信号集设为阻塞,收到一个信号,将对应未决信号集标志位置1,等到阻塞信号集设为非阻塞时,开始处理该未决信号(进入回调函数处理)。开始处理信号时,waitpid 才开始工作。waitpid 的功能是检查当前有多少个僵尸进程,一次回收一个,所以放在 while 循环里,就可以回收所有已死亡待回收的进程。
信号集收到信号是为了触发回调函数来处理。waitpid 会自动寻找僵尸进程,并逐个回收。
10.6 内存共享
10.6.1 共享内存
◼ 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
◼ 与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
10.6.2 共享内存使用步骤
获取 shmget()
关联 shmat()
使用
分离 shmdt()
删除 shmctl()
◼ 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
◼ 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
◼ 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
◼ 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
◼ 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 共享内存相关的函数 #include <sys/ipc.h> #include <sys/shm.h> int shmget (key_t key, size_t size, int shmflg) ; - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。 新创建的内存段中的数据都会被初始化为0 - 参数: - key : key_t 类型是一个整形,通过这个找到或者创建一个共享内存。 一般使用16 进制表示,非0 值 - size: 共享内存的大小 - shmflg: 属性 - 访问权限 - 附加属性:创建/判断共享内存是不是存在 - 创建:IPC_CREAT - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用 IPC_CREAT | IPC_EXCL | 0664 - 返回值: 失败:-1 并设置错误号 成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。 void *shmat (int shmid, const void *shmaddr, int shmflg) ; - 功能:和当前的进程进行关联 - 参数: - shmid : 共享内存的标识(ID),由shmget返回值获取 - shmaddr: 申请的共享内存的起始地址,指定NULL ,内核指定 - shmflg : 对共享内存的操作 - 读 : SHM_RDONLY, 必须要有读权限 - 读写: 0 - 返回值: 成功:返回共享内存的首(起始)地址。 失败(void *) -1 int shmdt (const void *shmaddr) ; - 功能:解除当前进程和共享内存的关联 - 参数: shmaddr:共享内存的首地址 - 返回值:成功 0 , 失败 -1 int shmctl (int shmid, int cmd, struct shmid_ds *buf) ; - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。 - 参数: - shmid: 共享内存的ID - cmd : 要做的操作 - IPC_STAT : 获取共享内存的当前的状态 - IPC_SET : 设置共享内存的状态 - IPC_RMID: 标记共享内存被销毁 - buf:需要设置或者获取的共享内存的属性信息 - IPC_STAT : buf存储数据 - IPC_SET : buf中需要初始化数据,设置到内核中 - IPC_RMID : 没有用,NULL key_t ftok (const char *pathname, int proj_id) ; - 功能:根据指定的路径名,和int 值,生成一个共享内存的key - 参数: - pathname:指定一个存在的路径 /home/nowcoder/Linux/a.txt / - proj_id: int 类型的值,但是这系统调用只会使用其中的1 个字节 范围 : 0 -255 一般指定一个字符 'a'
通过共享内存实现简单的通信(即访问同一块内存)
write_shm.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> int main () { int shmid = shmget(100 , 4096 , IPC_CREAT | 0664 ); printf ("shmid = %d\n" , shmid); void *ptr = shmat(shmid, NULL , 0 ); char *str = "hello,world" ; memcpy (ptr, str, strlen (str) + 1 ); printf ("按任意键继续\n" ); getchar(); shmdt(ptr); shmctl(shmid, IPC_RMID, NULL ); return 0 ; }
read_shm.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <stdio.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> int main () { int shmid = shmget(100 , 0 , IPC_CREAT); printf ("shmid = %d\n" , shmid); void *ptr = shmat(shmid, NULL , 0 ); printf ("%s\n" , (char *)ptr); printf ("按任意键继续\n" ); getchar(); shmdt(ptr); shmctl(shmid, IPC_RMID, NULL ); return 0 ; }
开启一个终端,执行 ./write_shm
创建一个共享内存,并建立关联。
再开启一个终端,执行 ./read_shm
获取上面创建的共享内存,并建立关联。
使用 ipcs -m
查看当前存在的共享内存:
然后逐个关闭进程,看到连接数在减少,当两个进程均关闭时,共享内存被删除:
问题1:操作系统如何知道一块共享内存被多少个进程关联?
共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
shm_nattach 记录了关联的进程个数
共享内存操作命令:
◼ ipcs 用法
ipcs -a // 打印当前系统中所有的进程间通信方式的信息
ipcs -m // 打印出使用共享内存进行进程间通信的信息
ipcs -q // 打印出使用消息队列进行进程间通信的信息
ipcs -s // 打印出使用信号进行进程间通信的信息
◼ ipcrm 用法
ipcrm -M shmkey // 移除用shmkey创建的共享内存段
ipcrm -m shmid // 移除用shmid标识的共享内存段
ipcrm -Q msgkey // 移除用msqkey创建的消息队列
ipcrm -q msqid // 移除用msqid标识的消息队列
ipcrm -S semkey // 移除用semkey创建的信号
ipcrm -s semid // 移除用semid标识的信号
问题2:可不可以对共享内存进行多次删除 shmctl
可以的
因为shmctl 标记删除共享内存,不是直接删除
什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除
当共享内存的key为0的时候,表示共享内存被标记删除了
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。
共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全
进程突然退出
共享内存还存在
内存映射区消失(在进程里)
运行进程的电脑死机,宕机了
数据存在在共享内存中,没有了
内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
5.生命周期
内存映射区:进程退出,内存映射区销毁
共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。
10.7 守护进程
10.7.1 终端
◼在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端(Controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端。
查看当前终端进程的 ID echo $$
显示终端机连接标准输入设备的文件名称 tty
,可以看到和上面是一致的
新开一个终端,查看 echo $$
,又不一样了
◼默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
◼在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl + C 会产生 SIGINT 信号,Ctrl + \ 会产生 SIGQUIT 信号。
执行程序时,后面加上 &
进入后台执行,后台进程没有控制终端,无法在控制终端控制后台进程。
10.7.2 进程组
◼进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。
◼进程组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID,新进程会继承其父进程所属的进程组 ID(自身id不变,组id继承)。
◼进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。
10.7.3 会话
◼会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。
◼一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
◼在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
◼当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。
10.7.4 终端、进程组、会话之间的关系
10.7.5 进程组、会话操作函数
◼pid_t getpgrp(void);
◼pid_t getpgid(pid_t pid);
◼int setpgid(pid_t pid, pid_t pgid);
◼pid_t getsid(pid_t pid);
◼pid_t setsid(void);
10.7.6 守护进程
◼守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
◼守护进程具备下列特征:
生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)。
◼Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。
SSH 连接也是由守护进程执行的。
10.7.7 守护进程的创建步骤
◼ 执行一个 fork()
,之后父进程退出,子进程继续执行。
如果不退出父进程,可能程序执行过程中,冷不丁冒出一个命令行提示符,这很诡异
子进程继续执行,因为它有自己的pid,有继承父进程的组ID,用于正常使用会话
◼ 子进程调用 setsid()
开启一个新会话。
开启一个新的会话,只要控制终端不与该会话建立连接,该会话就不会有控制终端(但是有终端),符合守护进程的特点。
子进程创建一个新的会话,新会话ID与该进程pid相同,否则,如果用父进程或者进程组创建会话,就会导致会话的ID和之前父进程所在会话的ID冲突。
◼ 清除进程的 umask
以确保当守护进程创建文件和目录时拥有所需的权限。
◼ 修改进程的当前工作目录,通常会改为根目录(/)。
由于守护进程要一致执行到程序结束,所以要放在一个稳定的文件夹下。
◼ 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
◼ 在关闭了文件描述符0、1、2之后,守护进程通常会打开 /dev/null
并使用 dup2()
使所有这些描述符指向这个设备。
因为会有功能进程需要调用文件描述符0,1,2,如果这时发现0,1,2是关闭的,就会发生错误。重定向到 /dev/null
之后,访问文件描述符0,1,2,可以正常访问,数据会被写入 /dev/null
,然后被丢弃。
◼ 核心业务逻辑
10.7.8 守护进程实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/time.h> #include <signal.h> #include <stdlib.h> #include <time.h> void work (int num) { time_t tim = time(NULL ); struct tm *loc = localtime(&tim); char buf[1024 ]; sprintf (buf, "%d-%d-%d %d:%d:%d\n" ,loc->tm_year,loc->tm_mon ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec); printf ("%s\n" , buf); } int main () { pid_t pid = fork(); if (pid > 0 ) { exit (0 ); } setsid(); umask(022 ); chdir("/home/fanqiyuan/" ); int fd = open("/dev/null" , O_RDWR); dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); struct sigaction act ; act.sa_flags = 0 ; act.sa_handler = work; sigemptyset(&act.sa_mask); sigaction(SIGALRM, &act, NULL ); struct itimerval val ; val.it_value.tv_sec = 2 ; val.it_value.tv_usec = 0 ; val.it_interval.tv_sec = 2 ; val.it_interval.tv_usec = 0 ; setitimer(ITIMER_REAL, &val, NULL ); while (1 ) { sleep(10 ); } return 0 ; }
执行程序后:发现什么也看不出来,也没有输出;ps | grep daemon
也不起作用
用 ps aux
看到确实有进程在后台运行
我们用 kill
命令手动杀死该进程。
那么输出为什么没了?
因为这里将标准输出重定向到 /dev/null
里了
把这部分注释掉再试一次:
下面重写 work()
函数,让结果写入文件:
记得把重定向解注释
1 2 3 4 5 6 7 8 9 10 void work (int num) { time_t tim = time(NULL ); struct tm *loc = localtime(&tim); char *str = asctime(loc); int fd = open("time.txt" , O_RDWR | O_CREAT | O_APPEND, 0664 ); write(fd, str, strlen (str)); close(fd); }
cd 到守护进程的工作目录,可以看到创建了一个 time.txt
文件,打开一看,守护进程正在奋力工作,每两秒写入一次时间。手动 kill
守护进程,让它休息休息。
进程部分结束啦,继续加油!(❁´◡`❁)