c++webserver/第二章 多进程开发

本文详细介绍了Linux下的进程管理,包括状态转换、创建、退出、信号处理、进程间通信(IPC)等。重点讲解了匿名管道、有名管道、内存映射和信号在进程间通信中的应用,以及孤儿进程和僵尸进程的处理。此外,还阐述了守护进程的创建步骤和特点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.Linux下的PCB

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YcLbORpy-1645290246154)(第二种 多进程开发.assets/image-20220216000346333.png)]

2.进程状态转换

1.三态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EqjbeFw6-1645290246155)(第二种 多进程开发.assets/image-20220216001528866.png)]

2.五态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7097jIur-1645290246155)(第二种 多进程开发.assets/image-20220216001720840.png)]

3.ps命令.

//静态查看进程
ps aux(查看当前全部进程) | ajx

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-blzxSzFs-1645290246156)(第二种 多进程开发.assets/image-20220216002356429.png)]

4.top命令

//动态查看进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rLSa0svJ-1645290246156)(第二种 多进程开发.assets/image-20220216003042146.png)]

5.kill命令

//杀死进程
kill -9/SIGKILL	pid		强制杀死进程
killall -name 进程名	  根据进程名杀死  

6.进程关系

ppid	父进程
pid		当前进程
pgid	进程组号

7.进程函数

1.创建进程
pid_t fork(void);
/*
	创建一个进程
	返回值:fork返回两次,一次是父进程,一个是子进程(区分父和子进程)
		  父进程中返回创建的子进程ID(大于0),-1表示失败,置errno.(进程数满|内存不足)
		  子进程返回0
	从函数执行开始,复制创建出一个子进程,两个进程交叉执行!
	因为为复制,所以代码相同,变量值相同,但是变量对于的!物理地址不同!
	用getpid()可以查看当前进程pid;
*/

#include <stdio.h>
#include <unistd.h>
int main()
{
    pid_t pid = fork();
    if (pid > 0)
    {
        printf("当前为父进程,pid = %d,其子进程的pid = %d\n", getpid(), pid);
    }
    else if (pid == 0)
    {
        printf("当前为子进程,pid = %d,其父进程为:%d\n", getpid(), getppid());
    }
    else if (pid < 0)
    {
        printf("创建进程失败");
    }
    return 0;
}

!!!真实过程是读时共享,写时拷贝,也就是说父子进程是共用同一个内存空间,只有涉及到写入时用户区才会整个复制一份.

内核区一定是复制一份;

不同部分:

  1. fork()返回值不同

  2. 数据:

  • 当前进程的pid
  • 当前进程的父进程的pid,即ppid
  • 信号集

共同点:

如果没有执行写操作

  • 文件描述符表
  • 用户区数据
2.进程退出
void exit(int status); //c库
/*
	参数
	1.	退出时的状态(子进程退出之后状态发给父进程,帮助回收资源等);
*/
void _exit(int status); //linux系统
// !!退出时不会刷新IU缓冲,如果打印时没有\n.可能不能显示!!

区别:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kTVtCvH8-1645290246157)(第二种 多进程开发.assets/image-20220217215042130.png)]

8.GDB调试多进程

默认调试只是父进程,子进程会一直运行

命令作用
set-follow-fork-mode[parent | child]设置跟踪的进程
show-follow-fork-mode显示现在执行的进程
set-detach-on-fork[on | off]默认on,是否脱离父进程运行,off挂起(8.0可能有bug)
info inferiors查看当前程序进程信息
inferior id切换进程,调试(需要子进程挂起)
detach inferiors id使程序的某个进程脱离GDB调试,自己运行

9.exec函数族

  • exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件.
  • EXEC函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,只有调用失败了,它们才会返回-1,从原程序的调用点接着往下执行.
  • 原先进程的用户区内容被新文件的用户区内容替换.
  • 一般都是父进程创建一个子进程,然后用exec函数来执行文件或者命令
!int execl(const char *path,const char *arg,.../* (char *) NULL*/) ;	//常用,c库
/*
	参数:
		1.	可执行文件路径,也可以是shell命令(命令文件)
		2.	执行可执行文件需要的参数列表(可以多个)
			第一个参数一般无作用,写执行程序的名字即可.第二个开始写参数,以NULL结束
	返回值:成功无返回(因为代码也被替换了),失败返回-1,继续执行下面代码;
*/
!int execlp(const char *file,const char *arg, ... /* (char *)NULL*/);	//常用,c库
/*
	- 会到环境变量中找指定的可执行文件,找到执行,找不到就执行不成功(ps命令可以不写/bin)
	- 不包括当前路径
	参数
		1.	执行的文件名  
	其它一样
*/
int execle(const char*path,const char*arg,./*,(char*)null,char*const envp[]*/)//l(list)		参数地址列表,以空指针结尾
int execv(const char*path,char*const argv[])int execvp(Const char*file,char*const argv[])//v (vector)	存有各参数地址的指针数组的地址(用一个字符串数组)
int execvpe(const char*file,char*const argv[]char*const envp[]);
//p(path)	按PATH环境变量指定的目录搜索可执行文件
!int execve(const char*filenename,char*const argv[]char*const envp[]);//linux内核函数
//e (environment)	存有环境变量字符串地址的指针数组的地址

10.孤儿进程(没有父亲)

父进程运行结果,但是子进程还在运行(未运行结束).

每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init(pid = 1) ,而init进程会循环地wait()它的已经退出的子进程。当孤儿进程生命周期结束的时候, init 进程就会处理它的一切善后工作。所以不会产生危害

11.僵尸进程(有躯壳没灵魂)

  • 每个进程结束之后,都会释放自己地址空间中的用户区数据, 内核区的 PCB没有办法自己释放掉,需要父进程去释放。
  • 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸( Zombie)进程。
  • 僵尸进程不能被kill -9杀死。
  • 这样就会导致一个问题,如果父进程不调用wait()或waitpid()的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

12.进程回收

  • 父进程调用wait() 或者waitpid() 得到进程的退出状态同时彻底清楚该进程(主要清理僵尸进程)
  • wait()和 waitpid()函数的功能一样,区别在于, wait()函数会阻塞,waitpid ( )可以设置不阻塞, waitpid ()还可以指定等待哪个子进程结束。
  • 注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
pid_t wait(int *status);
/*
	参数:
	1.	进程退出时的状态信息,传入的是一个int类型的地址,参数是一个传出参数;
	2.	返回值:成功返回子进程id,失败-1(所有子进程结束,调用函数失败);
	调用wait()函数的进程被阻塞,知道一个子进程结束或者收到一个不能被忽略的信号时才被唤醒
	如果没有子进程或者子进程都已经结束,返回-1;
*/
pid_t waitpid(pid_t pid, int *status, int options);
/*
	收回指定pid的进程,
	参数
	1.	>0 子进程的pid	 				=0  回收当前进程组的所有子进程
		-1 回收所有的子进程(相当于wait())  <-1 某个进程组的组id的绝对值,回收个id进程组的子进程
	2.	进程退出时的状态信息,传入的是一个int类型的地址,参数是一个传出参数;
	3.	设置阻塞(0)或者非阻塞(WNOHANG)
	返回值:>0.返回该进程id | =0	option = WNOHANG时,表示还有子进程 | <0 错误,或者没有子进程
*/
waitpid(-1,&status,0) = wait(&status);

status状态类型

WIFEXITED(status)		//非0,进程正常退出(判断程序是否正常退出)
WEXITSTATUS(status)	//如果上宏为真,获取进程退出的状态 (exit的参数)
    
WIFSIGNALED(status)	//非0,进程异常终止(判断为什么异常退出)
WTERMSIG(status)		//如果上宏为真,获取使进程终止的信号编号
    
WIFSTOPPED(status)		//非0,进程处于暂停状态
WSTOPSIG(status)		//如果上宏为真,获取使进程暂停的信号的编号
    
WIFCONTINUED (status)	//非0,进程暂停后已经继续运行

13.进程间通信(IPC)

目的:
  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 通知事件:一个进程需要向另一个或一组进程发送消息通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I1X29hnN-1645290246157)(第二种 多进程开发.assets/image-20220217231627248.png)]

14.匿名管道(环形队列)

前一个命令的输出作为下一个命令的输入

特点:
  • 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
  • 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
  • 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念(相当于没有协议),从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
  • 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
  • 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的(同一时间只能一边发给另外一边)
  • 从管道读数据是一次性操作.数据一旦被读走,它就从管道中被抛弃.释放空间以便写更多的数据,在管道中无法使用lseek ()来随机的访问数据。
  • 匿名管道只能在具有公共祖先的进程(父进程与子进程或者两个兄弟进程,具有亲缘关系)之间使用。因为有相同的文件描述符,所有对于通过文件描述符和管道两端可以进行通信.

使用:
//创建匿名管道,用于进程通信
int pipe (int pipefd[ 2]);
 /*
 	参数
 	1.	int pipefd[2]是个传出参数.
 		[0]对应读端(read),[1]对应写端(write )
 	返回值:成功返回0,失败返回-1;
 	注意:!!!创建管道一定要在fork()之前!!!,创建之后才能对于同一个管道(文件描述符对于内容一样)
 		 管道默认是阻塞的,如果没有数据,read阻塞.如果管道满了,write阻塞
 */
//查看管道缓冲大小命令
ulimit -a  //(pipe size 一块的大小.多少块).可修改
//查看管道缓冲大小函数
long fpathconf(int fd,int name);
/*
	long size = fpathconf(pipefd[0],_PC_PIPE_BUFF);	//size就为大小
*/

//设置非阻塞状态
fcnl(pipefd[],flag);

父进程和子进程的读写不能同时,也就是父进程读先,那么子进程一定是写先.否则两个进程均阻塞,程序互锁;

读写中间要停顿,不然会出现同一个进程读自己.如果只需一个(读.写)功能,关闭另外一个功能. close(pipefd[]);

向屏幕输出可以转向管道的写端: stdout->fileno -> pipefd[1]; dup2(pipefd[1],stdout_fileno)

读写特点(阻塞I/O的特殊情况):
  1. 所有的指向管道写端的文件描述符都关闭了**(管道写端引用计数为0**),有进程从管道的读端读取数据,管道剩余数据被读取完后,再次read放回0(非-1.就不是错误,正常退出).就像读到文件末尾;
  2. 如果指向管道的读端文件描述符都关闭**(管道读端引用计数为0**),这时候如果向管道写数据,那么该进程会收到一个SIGPIPE,通常会导致进程异常终止;
  3. 如果指向管道写端的文件描述符没有关闭**(管道写端引用计数不为0**),那么管道中没有数据时,执行read就会阻塞,知道管道有数据才读取数据并返回.
  4. 如果指向管道读端的文件描述符没有关闭**(管道读端引用计数不为0**).这时候管道写数据,那么在管道被写满时,调用write()会阻塞,直到有空位再写入并放回
总结:
  1. 读管道;
    • 管道中有数据,read返回实际读到的字节数。
    • 管道中无数据:
      • 写端被全部关闭,read返回0(相当于读到文件的末尾)
      • 写端没有完全关闭,read阻塞等待
  2. 写管道:
    • 管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
    • 管道读端没有全部关闭:
      • 管道已满,write阻塞
      • 管道没有满,write将数据写入,并返回实际写入的字节数

15.有名管道(命名管道,FIFO文件)

提供文件(管道文件),只要进程能访问到该路径就能进行通信;管道与匿名管道一样.先入先出

1.与匿名管道不同
  • FIFO 在文件系统中作为一个特殊文件存在,但FIFO中的内容却存放在内存中。
  • 当使用FIFO 的进程退出后, FIFO文件将继续保存在文件系统中以便以后使用。
  • FIFO有名字,不相关的进程可以通过打开有名管道进行通信。
2.有名管道使用
//通过命令创建有名管道
mkfifo	名字

//通过函数创建有名管道
int mkfifo (const char *pathname,mode_t mode);
/*
	参数
	1.	管道文件路径
	2.	文件权限(open相同,会与~umask按位与)
	返回值:成功返回0,失败返回-1, 置errno;
*/

/*
- 一旦使用mkfifo创建了一个FIFo,就可以使用open打开它,常见的文件I/o函数都可用于fifo。如: close、read、write、unlink等。
- FIFO严格遵循先进先出 (First in First out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作
- 打开一端时如果没有另外一端打开,那么管道会变成阻塞状态.直到有两端都打开.
- 如果在运行的时候,一段断开,另外一端也断开
*/
3.特殊点
  1. 读管道;
    • 管道中有数据,read返回实际读到的字节数。
    • 管道中无数据:
      • 写端被全部关闭,read返回0(相当于读到文件的末尾)
      • 写端没有完全关闭,read阻塞等待
  2. 写管道:
    • 管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
    • 管道读端没有全部关闭:
      • 管道已满,write阻塞
      • 管道没有满,write将数据写入,并返回实际写入的字节数

16.内存映射

1.定义

内存映射(Memory-mapped I/0) 是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。

2.通信原理:

可以对映射部门进行操作,映射内容修改是实时同步到文件中,多个进程读取同一个文件时.有相同的修改内容.所以能可以实现通信;

3.使用
//映射内存文件到内存中
void *mmap(void *addr, size_ t length, int prot, int flags,int fd,off t offset);
/*
	参数:
    1.	映射内存的首地址(传入NULL,由内核指定).
    2.	映射数据长度,不能为0.建议使用文件长度(按页分,页的整数倍大小)
    3.	申请内存映射区的操作权限(PROT_EXEC(执行)| READ | WRIED | NONE(无))
    	要操作映射区,一定要有读权限.如果要写就或上
    4.	[ MAP_SHARED(与磁盘文件进行同步) | MAP_PRIVATE(与文件不同步,重新创建一个新文件) | MAP_ANONYMOUS(匿名映射,不需要文件,fd指定-1,搭配用) ];
    5.	文件描述符,文件大小不能为0,open权限不能与前面有冲突(这里没有的,上面也不能有);
    6.	偏移量.一般不用,必须指定4K整数倍.0表示不偏移.如果不是会错误
    返回值:成功返回映射区首地址,失败返回MAP_FAILED;
*/

//释放内存映射
int munmap (void *addr, size t length) ;
/*
	参数
	1.	要释放内存大小
	2.	释放内存大小,与nmap的length一样
*/
4.使用对象
  1. 有关系的进行(父子进程) --还没有子进程的时候,通过父进程创建映射区.再创建子进程时就可以共享内存映射区
  2. 没有关系的进程
    1. 准备大小不为0的文件
    2. 进程1:通过磁盘文件创建内存映射 -得到一个操作这块内存的指针
    3. 进程2:通过磁盘文件创建内存映射 -得到一个操作这块内存的指针
    4. 使用内存映射区通信
  3. 内存映射区通信,非阻塞
5.注意事项
  1. 可以对指针进行++操作,但是不建议.因为释放内存会麻烦;
  2. 如果 flagprot 参数不一致,mmap时会失败并返回MAP_FAILED;
  3. mmap后把文件描述符指针关闭,不会产生影响
  4. ptr越界产生段错误
  5. 它还可以用于拷贝文件(速度快) -内存拷贝,太大内存装不下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NZZ03bz7-1645290246157)(第二种 多进程开发.assets/image-20220218235606883.png)]

6.匿名映射

不需要文件实体,进行内存映射

只能用于父子进程,因为无实体文件

17.信号

1.定义

信号是 Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。
  • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被o除,或者引用了无法访问的内存区域。
  • 系统状态变化,比如alarm定时器到期将引起SIGALRM 信号,进程执行的 CPU时间超限,或者该进程的某个子进程退出。
  • 运行kill 命令或调用kill 函数。
2.使用信号的两个主要目的是:
  • 让进程知道已经发生了一个特定的事情。强迫进程执行它自己代码中的信号处理程序。
3.信号的特点:
  • 简单
  • 不能携带大量信息
  • 满足某个特定条件才发送优先级比较高
4.信号列表

查看系统定义的信号列表:kill -l,前31 个信号为常规信号,(34-64)其余为实时信号。

查看信号意义 man 7 信号

5.信号的处理动作
  1. 信号的5中默认处理动作

    • Term 终止进程

    • Ign 当前进程忽略掉这个信号

    • Core 终止进程,并生成一个core文件(异常退出的错误信息)

    • stop 暂停当前进程

    • cont 继续执行当前被暂停的进程

  2. 信号的几种状态:产生、未决、递达

  3. SIGKILL和SIGSTOP信号不能被捕捉、阻塞或者忽略,只能执行默认动作。

//查找错误信息函数

ulimit -c 1024;	//指定core文件大小,默认为0.不生成
gcc -g ___.c -o ___;	//得到可执行文件
./___				//执行可执行文件,得到core文件
gdb __;				//进入调试状态
core-file core		//得到错误信息
6.信号相关的函数
//给某个进程发送任何信号sig
int kill(pid_t pid,int sig);
/*
	参数:
	1.	发送进程的pid
		>0	发送给指定进程
		=0,	向当前进程组中全部进程发送
		=-1	给每一个有权限接受的进程发送
		<-1	向取反的pgid进程组发送(-123向pgid = 123发送)
	2.	信号名称sig(用宏值)
*/


//给当前进程发送信号
int raise (int sig) ;
/*
	参数:
	1.	发送的信号
	返回值:成功0,失败非0;
*/

//!!!实际的时间 = 内核时间 + 用户时间 + 消耗的时间(IO操作)!!!

//发送SIGABRT信号给当前的进程,杀死当前进程
void abort (void) ;

//设置定时器,单位秒,开始调用时开始倒计时,为0是函数会给当前进程发送一个信号:SIGALARM.实际时间
unsigned int alarm(unsigned int seconds);
/*
	不会阻塞;无论进程什么状态,都会执行
	参数
	1.	seconds: 倒计时的时长.单位:秒.如果参数为0,定时器无效;
				 取消一个定时器,通过alarm;
	返回值:之前没有定时器,返回0;
		  之前有定时器,返回倒计时剩余的时间;
	SIGALARM: 默认终止当前的进程,每一个进程 有且只有唯一 的一个定时器(重复调用会覆盖上面的定时器);
*/

//周期性定时器,可以替代alarm函数,精度也比较高.单位微妙
int setitimer(int which,const struct itimerval *new_val,struct itimerval *old_value);
{
/*
	参数
	1.	指定时钟类型[	ITIMER_REAL(真实时间,正常时间3个.) | ITIMER_VTRTUAL(虚拟时间,用户时间) | ITIMER_PROF(用户时间,内核时间)]
	2.	定时器的结构体; //延迟 it_value后开始,然后每间隔it_interval执行一次
	  	 struct itimerval 
	  	 {
	  	 	    struct timeval it_interval; 	//每个阶段的时间,间隔时间(延迟后每几秒执行一次)
              struct timeval it_value;    //延迟多长时间执行定位器(开始)
			
              struct it_interval/it_value		//时间的结构体
              {
            		time_t      tv_sec;        //秒数
            		suseconds_t tv_usec;       //微妙
        	  };
         };
   3.	上一个定时器的属性(传出参数),直接置null即可;
*/
}


//信号捕捉,SIGKILL SIGSTOP不能被捕捉和忽略.!信号捕捉一定要在信号产生前面!
sighandler_t signal(int signum,sighandler_t handler);
/*
	参数
	1.	要捕捉的信号
	2.	捕捉到信号后如何处理
		- SIG_IGN:	忽略信号(定时器用)
		- SIG_DFT:	使用信号默认行为
		- 回调函数:	  内核调用,程序员只负责写.
	返回值:成功返回上一次注册的信号处理函数地址,第一次返回NULL
		  失败返回SIG_ERR,设置errno
*/

//检查或者改变信号的处理.建议使用
int sigaction (int signum,const struct sigaction *act,struct sigaction *oldact) ;
/*
	1.	需要步骤的信号的编号或者宏值;
	2.	捕捉到信号后的动作
		   struct sigaction {
               void     (*sa_handler)(int);		回调函数;
               void     (*sa_sigaction)(int, siginfo_t *, void *);	函数指针,多了一部分信息.不常用
               sigset_t   sa_mask;				临时阻塞信号集(可以临时阻塞一些信号)						
               int        sa_flags;				指定用第一个还是第二个回调处理动作[0 | SA_SIGINFO]
               void     (*sa_restorer)(void);	废弃.置null
           };
	3.	上一次对信号捕捉相关的设置,一般不使用.置null;
	返回值:成功0,失败-1;
*/

7.回调函数
void (*sighandler_t)(int); //函数指针
//int类型参数表示捕捉到的信号的值;

//例子
void myalarm(int num)
{
}

18.信号集

1.定义

阻塞信号集可以被设置,未决信号集不能被设置

  • 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为sigset_t。
  • 在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为"未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB 中的这两个信号集进行修改。
  • 信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间
  • 信号的“阻塞”是一个开关动作指的是阻止信号被处理,但不是阻止信号产生。信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
2.阻塞信号集和未决信号集工作流程

信号产生但是没有被处理(未决)

  • 在内核中将所有的没有被处理的信号存储在一个集合中(未决信号集,本质就是一个数组),SIGINT信号状态被存储在第二个标志位上.
    • 这个标志位的值为0,说明信号不是未决状态
    • 这个标志位的值为1,说明信号处于未决状态
  • 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
    • 阻塞信号集默认不阻塞任何的信号 (为0)
    • 如果想要阻塞某些信号需要用户调用系统的API (阻塞信号集的对应信号为1)
  • 在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了(比较过程)
    • 如果没有阻塞,这个信号就被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
函数作用
//创建一个信号集
sigset_set set;
//以下 函数只能对自己定义的信号集生效
//清空信号集的数据
int sigemptyset(sigset_t *set);
/*
	参数
	1.	传出参数,需要操 作的信号集.
	返回值:成功返回0,失败返回-1;
*/

//将信号集中的所有的标志位置1
int sigfillset(sigset_t *set);
/*
	参数
	1.	传出参数,需要操作的信号集.
	返回值:成功返回0,失败返回-1;
*/

//设置信号集中的某一个信号位为1
int sigaddset(sigset_t *set, int signum);
/*
	1.	传出参数,需要操作的信号集.
	2.	需要设置阻塞的信号
	返回值:成功返回0,失败返回-1;
*/

//设置信号集中的某一个信号位为0
int sigdelset(sigset_t *set, int signum);
/*
	1.	传出参数,需要操作的信号集.
	2.	需要设置阻塞的信号
	返回值:成功返回0,失败返回-1;
*/

//判断某个信号是否阻塞
int sigismember(const sigset_t *set, int signum);
/*
	1.	传出参数,需要操作的信号集.
	2.	需要设置阻塞的信号
	返回值:1说明被阻塞,0说明不阻塞.-1表示错误
*/

********************************************************
//内核区
    
//把自己设置的信号集设置到内核区(只能设置阻塞集)
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*
	1.	行为设置[SIG_BLOCK | SIG_UNBLOCK(非阻塞) | SIG_SETMASK(自定义信号集替换内核信号集)]
			- SIG_BLOCK:将用户设置的信号集的阻塞信号添加到内核中,其它数据不变(两个信号集 或运算)
			- SIG_UNBLOCK:将用户设置的信号集,对内核中的数据进行解除阻塞,其它数据不变(内核 与运算 取反的自己信号集)
			- SIG_SETMAKE:将内核中的信号集替换为用户设置的信号集(只能对阻塞集)
	
	2.	已经初始化好的用户自定义信号集.
	3.	修改后原来的信号集,可以为NULL
	返回值:成功0,失败-1;设置错误号(EFAULT[指向错误地址],EINVAL[信号非法])
*/

//获取内核中的未决信号集
int sigpending(sigset_t *set);
/*
	参数
	1.	传出参数,内核中未决信号集信息. 	
*/

多个相同信号产生时会阻塞,直到上一个处理函数结束(临时阻塞信号集).来多少次都只能记录1,其它都丢弃了

19.SIGCHLD信号

1.SIGCHLD信号产生的条件
  • 子进程终止时
  • 子进程接收到SIGSTOP信号停止时
  • 子进程处在停止态,接受到SIGCONT后唤醒时

以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号

2.作用

处理僵尸进程

20.共享内存(速度最快)

1.定义
  • 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种IPC 机无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。

  • 与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快。

2.步骤
  • **调用shmget ()创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。**这个调用将返回后续调用中需要用到的共享内存标识符。
  • **使用shmat ()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。**此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引这块共享内存,程序需要使用由shmat()调用返回的 addr值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  • **调用shmdt ()来分离共享内存段。**在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
  • **调用shmctl ()来删除共享内存段。**只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
相关函数
//创建一个新共享内存段或取得一个既有共享内存段的标识符,新创建的内存段中的数据都会被初始化为0
int shmget(key_t key, size_t size, int shmflg);
/*
	参数
	1.	key_t类型,是一个整数.就是该内存的标识符.16进制表示
	2.	共享内存大小
	3.	属性
		- 访问权限
		- 附加属性: 创建(IPC_CREAT | 0664)/判断(IPC_EXCL需要搭配IPC_CREAT)共享内存是否存在
	返回值:成功返回内存标识符,失败-1置errno;
*/

//让内存与当前进程关联
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
	参数
	1.	共享内存的标识符
	2.	申请的共享内存的起始地址,一般为NULL,内核指定
	3.	对共享内存的操作
			读权限:SHM_RDONLY	必须要有
			读写:	0			没指定默认为读写权限
	返回值:成功返回共享内存的起始地址,失败返回一个(void *)-1;
*/

//解除当前进程和共享内存的关联
int shmdt(const void *shmaddr);
/*
	参数
	1.	共享内存的首地址(shmat获得)
	返回值:成功0,失败-1
*/

//对共享内存进行操作,一般用于删除共享内存,只有删除才会消失.创建共享内存的进程被销毁对共享内存没用任何影响
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
	参数
	1.	共享内存的标识符
	2.	要做的操作
		- IPC_STAT : 获取当前共享进程状态;
        - IPC_SET  : 设置共享内存的状态;
        - IPC_RMID : 标记共享内存被销毁(没有标记才能删除,标记 = 有进程使用它);
    3.	需要设置或者获取的共享内存属性信息    
    	- IPC_STAT : buf存储销毁的共享内存信息
    	- IPC_SET  : buf需要初始化数据,设置到内核中
    	- IPC_RMID : 没用,直接NULL;
*/

//根据指定的路径名和int值生成一个共享内存的key;
key_t ftok(const char *pathname, int proj_id);
/*
	参数
	1.	指定一个存在路径
	2.	int类型的值,系统调用只会使用其中的一个字节(8bit.0~255),一般指定字符'a';
*/
4.补充:

1.共享内存维护了一个结构体struct shmid_ds,这个结构体中有一个成员shm_nattach ,它记录了关联的进程个数.

2.ipcs查出来如果key为0,说明该内存被标记删除

3.一个共享内存可以多次调用shmctl删除,因为shmctl只是标记删除,不是直接.直到关联的进程数为0是才删除.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AanA6Vp7-1645380357334)(第二种 多进程开发.assets/image-20220220233316781.png)]

5.与内存映射区别
  1. 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)

  2. 共享内存效果更高

  3. 内存

    • 所有的进程操作的是共享同一块内存.
    • 内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
  4. 数据安全

    1. 进程突然退出
      1. 共享内存还存在
      2. 内存映射区消失
    2. 运行进程的电脑死机,宕机了
      1. 数据存在在共享内存中,没有了
      2. 内存映射区的数据,由于磁盘文件中的数据还在,所以内存映射区的数据还存在.
  5. 生命周期

    • 内存映射区:进程退出,内存映射区销毁
    • 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0时删除)或者关机

21.守护进程

1.进程组(pgid)
  • 进程组和会话在进程之间形成了一种两级层次关系︰**进程组是一组相关进程的集合.会话是一组相关进程组的集合。**进程组和会话是为支持shell 作业控制而定义的抽象概念,用户通过shell能够交互式地在前台或后台运行命令。
  • 进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的 ID,新进程会继承其父进程所属的进程组ID.
  • 进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员
2.会话(sid)
  • **会话是一组进程组的集合。**会话首进程是创建该新会话的进程,其进程ID会成为会话ID。新进程会继承其父进程的会话ID。
  • 一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。在任一时刻,**会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。****当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
  • 当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。其它新产生的会话没有控制终端
  • 注意:创建会话的进程不能是一个组长!
3.守护进程
  • 守护进程(Daemon Process) ,也就是通常说的Daemon进程(精灵进程),是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周
    期性地执行某种任务或等待处理某些发生的事件。一般采用以d 结尾的名字。
  • 守护进程具备下列特征:
    • 生命周期很长,守护进程会在系统启动的时候被创建并―直运行直至系统被关闭。
    • 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如SIGINT、SIGQUIT) [因为被杀死不会接受输入]
  • Linux的大多数服务器就是用守护进程实现的。比如.Internet服务器,web服务器,httpd等。
!!!4.步骤!!!
  1. 执行一个 fork (),之后父进程退出,子进程继续执行。
    • 父进程产生的子进程一定不会是组长,所有可以创建一个新会话!
  2. 子进程调用setsid()开启一个新会话。
    • 开启新会话后,因为不是首会话.不具备控制终端,这样就不会接受输入命令
    • 新会话的SID和内部新建的组PGID均为当前子进程PID,不会冲突(父进程就会进程组冲突)
  3. 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。
  4. 修改进程的当前工作目录,通常会改为根目录(/) 。
    • 根目录安全,不会被卸载.需要权限
  5. 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
    • 脱离终端,不是没有终端.所有如果不关闭可能会在原来的终端输出数据;
  6. 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/null(所有输出都会丢弃),并使用dup2()使所有这些描述符指向这个设备。
  7. 核心业务逻辑

视频链接:https://www.nowcoder.com/study/live/504(免费的,我看着感觉老师讲的很好!)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

公仔面i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值