内容有:程序和进程概述、并行和并发、pcb进程信息管理、进程状态转换、进程相关命令、fork函数、父子进程用户区数据读写、多进程gdb调试、exec族函数、结束进程、孤儿进程、僵尸进程、wait、waitpid函数、进程间通信简介、匿名管道、有名管道、内存映射、信号、进程组和会话、守护进程。
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 并行和并发 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
1.5 进程控制块(PCB) 为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个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 进程状态的三态模型 运行态:进程占有处理器正在运行。
就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成。
2.2 进程状态的五态模型 新建态:进程刚被创建时的状态,尚未进入就绪队列。
终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。
2.3 进程相关命令 2.3.1 查看进程快照(静态) 1 2 3 4 5 6 7 8 > ps aux > ps ajx 参数的意义: a:显示终端上的所有进程 u:详细信息 x:显示没有控制终端的进程 j:列出与作业控制相关的信息
1 2 3 4 5 6 7 8 > ps aux得到如下头部 USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDTTY:表示当前终端,命令行输入> tty可查看当前终端。 START :开始运行的时间TIME:运行时间 STAT:状态,意义如下:
2.3.2 实时显示进程 1 2 > top #默认3s刷新一次 > top -d 5 #指定5s刷新一次
按以下键显示对结果的排序:
2.3.3 杀死进程 1 2 3 4 5 6 7 8 9 10 11 12 杀死进程 > kill [-signal] pid 列出所有信号signal > kill -l 强制杀死进程,9是宏SIGKILL的编号 > kill -SIGKILL pid > kill -9 pid 通过进程名杀死进程 > killall name
2.4 进程号和相关函数 每个进程都由进程号来标识,其类型为 pid_t(整型),进程号的范围:0~32767。 进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程, 对应的进程号称为父进程号(PPID)。
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各 种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当 前的进程组号。
进程号和进程组相关函数:
pid_t getpid(void);
pid_t getppid(void);
pid_t getpgid(pid_t pid);
3 进程创建 系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
3.1 进程创建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <sys/types.h> #include <unistd.h> pid_t fork (void ) ;函数的作用:用于创建子进程。 返回值: fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。 在父进程中返回创建的子进程的ID,在子进程中返回0 如何区分父进程和子进程:通过fork的返回值。 在父进程中返回-1 ,表示创建子进程失败,并且设置errno 失败的两个主要原因: 1. 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN2. 系统内存不足,这时 errno 的值被设置为 ENOMEM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main () { pid_t ret = fork(); if (ret == -1 ) { perror("fork" ); return -1 ; } if (ret > 0 ) { printf ("parent, pid: %d, ppid: %d\n" , getpid(), getppid()); } if (ret == 0 ) { printf ("child, pid: %d, ppid: %d\n" , getpid(), getppid()); } for (int i = 0 ; i < 3 ; i++) { printf ("i : %d , pid : %d\n" , i , getpid()); sleep(1 ); } return 0 ; }
从fork()函数开始,进程分出了一个子进程,两个进程同时运行代码。for循环可以看到,是交替运行的。
3.2 父子进程虚拟地址 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 父子进程之间的关系: 区别: 1.fork()函数的返回值不同 父进程中 : >0 返回的子进程的ID 子进程中 : =0 2.pcb中的一些数据 当前的进程的id pid 当前的进程的父进程的id ppid 信号集 共同点: 某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作 - 用户区的数据 - 文件描述符表 父子进程对变量是不是共享的? - 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。 - 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。
虚拟地址空间是物理地址的一个映射。
1 2 3 4 5 6 7 8 实际上,更准确来说,Linux 的 fork () 使用是通过写时拷贝 (copy- on-write ) 实现。 写时拷贝是一种可以推迟甚至避免拷贝数据的技术。 内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。 只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。 也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。 注意:fork 之后父子进程共享文件, fork 产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
3.3 GDB多进程调试
4 exec函数族 exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的 内容,换句话说,就是在调用进程内部执行一个可执行文件。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样, 颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵 魂。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。即,内核区的数据不变,用户区的数据会被替换。
因为使用exec()函数之后,原先进程的用户区数据都被替换了,所以一般不直接使用这个函数,而是fork()一个子进程,在子进程中使用exec()。
前六个函数都是标准C库的函数,最后一个是Linux系统函数,前六个都是最后一个的封装。
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 #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("/bin/ps" , "ps" , "aux" , NULL ); perror("execl" ); 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 ; }
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 <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 ; }
5 进程控制 5.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 <stdio.h> #include <stdlib.h> #include <unistd.h> int main () { printf ("hello\n" ); printf ("world" ); _exit(0 ); return 0 ; }
5.2 孤儿进程 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程 (Orphan Process)。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。
因此孤儿进程并不会有什么危害。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main () { pid_t pid = fork(); if (pid > 0 ) { printf ("i am parent process, pid : %d, ppid : %d\n" , getpid(), getppid()); } else if (pid == 0 ) { sleep(1 ); printf ("i am child process, pid : %d, ppid : %d\n" , getpid(),getppid()); } for (int i = 0 ; i < 3 ; i++) { printf ("i : %d , pid : %d\n" , i , getpid()); } return 0 ; }
5.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 #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main () { pid_t pid = fork(); if (pid > 0 ) { while (1 ) { printf ("i am parent process, pid : %d, ppid : %d\n" , getpid(), getppid()); sleep(1 ); } } else if (pid == 0 ) { printf ("i am child process, 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,看到子进程的stat是Z,表示是僵尸进程。
5.4 进程回收 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内 存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <sys/types.h> #include <sys/wait.h> #include <stdio.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(1 ); } } else if (pid == 0 ){ while (1 ) { printf ("child, pid = %d\n" ,getpid()); sleep(1 ); } exit (0 ); } 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 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 <sys/types.h> #include <sys/wait.h> #include <stdio.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(1 ); 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(1 ); } exit (0 ); } return 0 ; }
6 进程间通信 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间 的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程 间通信( IPC:Inter Processes Communication )。进程间通信的目的:
数据传输:一个进程需要将它的数据发送给另一个进程。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
6.1 匿名管道 管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式, 所有的 UNIX 系统都支持这种通信机制。
统计一个目录中文件的数目命令:ls | wc –l,为了执行该命令,shell 创建了两 个进程来分别执行 ls 和 wc。( |:管道符,连接两个进程)。
进程默认打开的三个文件:标准输入fd=0,标准输出fd=1,标准错误fd=2。
6.1.1 管道的特点 管道的特点:
管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。Linux下管道大小默认4k。
管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
6.1.2 为什么可以使用管道进行进程间通信? 管道只能在具有公共祖先的进程之间使用,所以管道的创建要在fork函数之前。使用fork函数之后,父子进程的文件符表是一样的,都指向同一个管道文件,所以可以通信。
6.1.3 管道的数据结构 管道的数据结构其实是一个循环数组,有读指针和写指针。
6.1.4 父子进程通过管道进行通信
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 #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 process, pid : %d\n" , getpid()); close(pipefd[1 ]); char buf[1024 ] = {0 }; while (1 ) { int len = read(pipefd[0 ], buf, sizeof (buf)); printf ("parent recv : %s, pid : %d\n" , buf, getpid()); } } else if (pid == 0 ){ printf ("i am child process, pid : %d\n" , getpid()); close(pipefd[0 ]); char buf[1024 ] = {0 }; while (1 ) { 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 # ulimit -a产看管道缓冲区大小 #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 ; }
6.1.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 #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 ] = {0 }; 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 ], STDOUT_FILENO); execlp("ps" , "ps" , "aux" , NULL ); perror("execlp" ); exit (0 ); } else { perror("fork" ); exit (0 ); } return 0 ; }
6.1.6 管道读写特点 管道的读写特点: 使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作) 1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。
总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
6.1.7 管道设置为非阻塞 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 <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 ); } pid_t pid = fork(); if (pid > 0 ) { printf ("i am parent process, pid : %d\n" , getpid()); close(pipefd[1 ]); char buf[1024 ] = {0 }; int flags = fcntl(pipefd[0 ], F_GETFL); flags |= O_NONBLOCK; fcntl(pipefd[0 ], F_SETFL, flags); while (1 ) { int len = read(pipefd[0 ], buf, sizeof (buf)); printf ("len : %d\n" , len); printf ("parent recv : %s, pid : %d\n" , buf, getpid()); memset (buf, 0 , 1024 ); sleep(1 ); } } else if (pid == 0 ){ printf ("i am child process, pid : %d\n" , getpid()); close(pipefd[0 ]); char buf[1024 ] = {0 }; while (1 ) { char * str = "hello,i am child" ; write(pipefd[1 ], str, strlen (str)); sleep(5 ); } } return 0 ; }
6.2 有名管道 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的 I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一 个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的 名称也由此而来:先入先出。
有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于: 1. FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。 2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。 3. FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。
6.2.1 创建有名管道 1 2 3 4 5 6 7 8 9 10 11 通过命令创建有名管道 mkfifo 名字 mkfifo fifo1 通过函数创建有名管道 #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() 等文件定位操作。
6.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 #include <stdio.h> #include <sys/types.h> #include <sys/stat.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 ; }
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 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.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 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 #include <stdio.h> #include <sys/types.h> #include <sys/stat.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 ; }
6.2.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 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <fcntl.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 ); ret = write(fdw, buf, strlen (buf)); if (ret == -1 ) { perror("write" ); exit (0 ); } memset (buf, 0 , 128 ); ret = read(fdr, buf, 128 ); if (ret <= 0 ) { perror("read" ); break ; } printf ("buf: %s\n" , buf); } close(fdr); close(fdw); 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 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 <stdlib.h> #include <fcntl.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 , 128 ); ret = read(fdr, buf, 128 ); if (ret <= 0 ) { perror("read" ); break ; } printf ("buf: %s\n" , buf); memset (buf, 0 , 128 ); fgets(buf, 128 , stdin ); ret = write(fdw, buf, strlen (buf)); if (ret == -1 ) { perror("write" ); exit (0 ); } } close(fdr); close(fdw); return 0 ; }
7 内存映射 内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改 内存就能修改磁盘文件。
1 2 3 #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) ;
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 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 <string.h> #include <stdlib.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 ; }
7.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 1 .如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?void * ptr = mmap(...); ptr++; 可以对其进行++操作 munmap(ptr, len); // 错误,要保存地址 2 .如果open 时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?错误,返回MAP_FAILED open ()函数中的权限建议和prot参数的权限保持一致。3 .如果文件偏移量为1000 会怎样?偏移量必须是4 K的整数倍,返回MAP_FAILED 4 .mmap什么情况下会调用失败? - 第二个参数:length = 0 - 第三个参数:prot - 只指定了写权限 - prot PROT_READ | PROT_WRITE 第5 个参数fd 通过open 函数时指定的 O_RDONLY / O_WRONLY 5 .可以open 的时候O_CREAT一个新文件来创建映射区吗? - 可以的,但是创建的文件的大小如果为0 的话,肯定不行 - 可以对新的文件进行扩展 - lseek() - truncate () 6 .mmap后关闭文件描述符,对mmap映射有没有影响? int fd = open ("XXX" ); mmap(,,,,fd,0 ); close (fd); 映射区还存在,创建映射区的fd被关闭,没有任何影响。 7 .对ptr越界操作会怎样?void * ptr = mmap(NULL, 100 ,,,,,); 4 K,跟磁盘的分页有关越界操作操作的是非法的内存 -> 段错误
7.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 #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); write(fd1, " " , 1 ); 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 ; }
7.4 匿名映射 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 <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> #include <sys/wait.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 ; }
8 信号 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也 称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号 可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程发送一个中断信号。
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给 相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的 内存区域。
系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
运行 kill 命令或调用 kill 函数。
使用信号的两个主要目的是:第一,让进程知道已经发生了一个特定的事情。第二,强迫进程执行它自己代码中的信号处理程序。
信号的特点:简单。不能携带大量信息。满足某个特定条件才发送。优先级比较高。
8.1 信号列表 查看系统定义的信号列表:kill -l
。前 31 个信号为常规信号,其余为实时信号。
8.2 信号的5种默认处理动作 查看信号的详细信息:man 7 signal
。信号的几种状态:产生、未决、递达。SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。信号的 5 中默认处理动作:
Term:终止进程。
Ign:当前进程忽略掉这个信号。
Core:终止进程,并产生一个Core文件。
Stop:暂停当前进程。
Cont:继续执行被暂停的进程。
8.3 Core文件的生成和调试 编写一个错误的程序,执行过后,发现没有生成Core文件。使用ulimit -a
查看当前进程的限制,发现core file size
是0,使用ulimit -c 1024
打开Core文件写的权限,再次运行错误的程序,就能得到一个Core文件了。
进入调试程序gdb,输入core-file core
可以得到错误信息的具体位置。
8.4 kill、raise、abort函数 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 #include <stdio.h> #include <sys/types.h> #include <signal.h> #include <unistd.h> int main () { pid_t pid = fork(); if (pid == 0 ) { int i = 0 ; for (i = 0 ; i < 5 ; i++) { printf ("child process\n" ); sleep(1 ); } } else if (pid > 0 ) { printf ("parent process\n" ); sleep(2 ); printf ("kill child process now\n" ); kill(pid, SIGINT); } return 0 ; }
8.5 alarm函数 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(5 ); printf ("seconds = %d\n" , seconds); sleep(2 ); seconds = alarm(2 ); printf ("seconds = %d\n" , seconds); while (1 ) { } return 0 ; }
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 ("%i\n" , i++); } return 0 ; }
8.6 setitimer函数 alarm函数只能定时一次,setitimer函数可以周期性定时。
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 #include <sys/time.h> #include <stdio.h> #include <stdlib.h> int main () { 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 ; }
发现只定时了一次程序就结束了,没有实现周期性定时的功能。因为没有捕捉信号,然后处理信号,所以进程受到一个信号函数就执行默认功能结束进程了。
8.7 signal信号捕捉函数 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 <sys/time.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> void myalarm (int num) { printf ("捕捉到了信号的编号是:%d\n" , num); printf ("xxxxxxx\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 ; }
8.8 信号集 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为 信号集的数据结构来表示,其系统数据类型为 sigset_t。
在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为 “未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我 们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数 来对 PCB 中的这两个信号集进行修改。
位图就是使用64位来表示一个集合,每一位都表示特定的功能,使用位运算可以实现修改、判断等功能。
信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号, 所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
8.9 阻塞信号集和未决信号集 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建) 2.信号产生但是没有被处理 (未决) - 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集) - SIGINT信号状态被存储在第二个标志位上 - 这个标志位的值为0, 说明信号不是未决状态 - 这个标志位的值为1, 说明信号处于未决状态 3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较 - 阻塞信号集默认不阻塞任何的信号 - 如果想要阻塞某些信号需要用户调用系统的API 4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了 - 如果没有阻塞,这个信号就被处理 - 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
8.10 用户自定义信号集 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 ("SIGINT 不阻塞\n" ); } else if (ret == 1 ) { printf ("SIGINT 阻塞\n" ); } sigaddset(&set , SIGINT); sigaddset(&set , SIGQUIT); ret = sigismember(&set , SIGINT); if (ret == 0 ) { printf ("SIGINT 不阻塞\n" ); } else if (ret == 1 ) { printf ("SIGINT 阻塞\n" ); } ret = sigismember(&set , SIGQUIT); if (ret == 0 ) { printf ("SIGQUIT 不阻塞\n" ); } else if (ret == 1 ) { printf ("SIGQUIT 阻塞\n" ); } sigdelset(&set , SIGQUIT); ret = sigismember(&set , SIGQUIT); if (ret == 0 ) { printf ("SIGQUIT 不阻塞\n" ); } else if (ret == 1 ) { printf ("SIGQUIT 阻塞\n" ); } return 0 ; }
8.11 用自定义信号集修改系统信号集 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 #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++) { if (sigismember(&pendingset, i) == 1 ) { printf ("1" ); }else if (sigismember(&pendingset, i) == 0 ) { printf ("0" ); }else { perror("sigismember" ); exit (0 ); } } printf ("\n" ); sleep(1 ); if (num == 10 ) { sigprocmask(SIG_UNBLOCK, &set , NULL ); } } return 0 ; }
阻塞时,按下Ctrl+c不能结束进程。
在运行可执行文件时后面输入&号,可以让程序在后台运行。./app &
。
程序占用前台,不能输入其他命令,程序在后台的时候可以输入其他命令。程序在后台依旧可以输出内容。
8.12 sigaction信号捕捉函数 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 <sys/time.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> void myalarm (int num) { printf ("捕捉到了信号的编号是:%d\n" , num); printf ("xxxxxxx\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 ; }
8.13 内核实现信号捕捉的过程
8.14 SIGCHLD信号 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 <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 ); sigprocmask(SIG_UNBLOCK, &set , NULL ); while (1 ) { printf ("parent process pid : %d\n" , getpid()); sleep(2 ); } } else if ( pid == 0 ) { printf ("child process pid : %d\n" , getpid()); } return 0 ; }
9 共享内存 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于 一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介 入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其 他所有共享同一个段的进程可用。与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据 从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
9.1 使用步骤
调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其 他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存, 程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间 中该共享内存段的起点的指针。
调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存 了。这一步是可选的,并且在进程终止时会自动完成这一步。
调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之 后内存段才会销毁。只有一个进程需要执行这一步。
9.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 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 共享内存相关的函数 #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' 问题1 :操作系统如何知道一块共享内存被多少个进程关联? - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch - shm_nattach 记录了关联的进程个数 问题2 :可不可以对共享内存进行多次删除 shmctl - 可以的 - 因为shmctl 标记删除共享内存,不是直接删除 - 什么时候真正删除呢? 当和共享内存关联的进程数为0 的时候,就真正被删除 - 当共享内存的key为0 的时候,表示共享内存被标记删除了 如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。 共享内存和内存映射的区别 1. 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外) 2. 共享内存效果更高 3. 内存 所有的进程操作的是同一块共享内存。 内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。 4. 数据安全 - 进程突然退出 共享内存还存在 内存映射区消失 - 运行进程的电脑死机,宕机了 数据存在在共享内存中,没有了 内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。 5. 生命周期 - 内存映射区:进程退出,内存映射区销毁 - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0 ),或者关机 如果一个进程退出,会自动和共享内存进行取消关联。
9.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 #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 ; }
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 #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 = "helloworld" ; memcpy (ptr, str, strlen (str) + 1 ); printf ("按任意键继续\n" ); getchar(); shmdt(ptr); shmctl(shmid, IPC_RMID, NULL ); return 0 ; }
9.4 共享内存操作命令
10 守护进程 10.1 终端 在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成 为 shell 进程的控制终端(Controlling Terminal),进程中,控制终端是 保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进 程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指 向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准 错误输出写也就是输出到显示器上。
在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl + C 会产 生 SIGINT 信号,Ctrl + \ 会产生 SIGQUIT 信号。
10.2 进程组 进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合, 会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽 象概念,用户通过 shell 能够交互式地在前台或后台运行命令。
进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一 个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID,新进程 会继承其父进程所属的进程组 ID。
进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个 成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入 了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。
10.3 会话 会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会 话 ID。新进程会继承其父进程的会话 ID。
一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终 端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为 后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终 端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。
10.4 进程、进程组、会话、控制终端之间的关系
10.5 进程组、会话的操作函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pid_t getpgrp (void ) ;获取组id pid_t getpgid (pid_t pid) ;获取指定进程的组id int setpgid (pid_t pid, pid_t pgid) ;设置指定进程的组id pid_t getsid (pid_t pid) ;获取会话id pid_t setsid (void ) ;创建新的会话,id设置为当前进程的id
10.6 守护进程 守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周 期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
守护进程具备下列特征:生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进 程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)。Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd, Web 服务器 httpd 等。
10.7 守护进程的创建步骤 ◼ 执行一个 fork(),之后父进程退出,子进程继续执行。
◼ 子进程调用 setsid() 开启一个新会话。
◼ 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。
◼ 修改进程的当前工作目录,通常会改为根目录(/)。
◼ 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
◼ 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/null 并使用dup2() 使所有这些描述符指向这个设备。
◼ 核心业务逻辑
10.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 75 76 77 78 79 80 81 #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/time.h> #include <signal.h> #include <time.h> #include <stdlib.h> #include <string.h> void work (int num) { time_t tm = time(NULL ); struct tm * loc = localtime(&tm); char * str = asctime(loc); int fd = open("time.txt" , O_RDWR | O_CREAT | O_APPEND, 0664 ); write(fd ,str, strlen (str)); close(fd); } int main () { pid_t pid = fork(); if (pid > 0 ) { exit (0 ); } setsid(); umask(022 ); chdir("/home/nowcoder/" ); 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 ; }