C语言笔记(23)Linux进程间通信介绍

进程间通信介绍
在这里插入图片描述
早期UNIX进程间通信方式(从Unix继承过来的进程间通信机制)
无名管道(pipe)
有名管道(fifo)
信号(signal)

System V IPC (IPC进程间通信的缩写)
又引入了三种通信机制 (Linux同样继承了过来)
共享内存(share memory)
消息队列(message queue)
信号灯集(semaphore set)

套接字(socket)既可以用于本地通信,更多的用于不同主机之间通过网络来通信,关于套接字的用法后面具体介绍
主要学习前面六种 。这六种共同点是用于本地通信 ,一台计算机内部不同的进程之间的通信

无名管道
在这里插入图片描述

我们在前面介绍线程的时候 , 因为线程是共享同 一个地址空间所以线程间要交换数据非常的方便 ,最简单方法就是定义一个全局的缓冲区就可以, 而进程不一样, 每个进程有自己独立地址空间是私有的,有自己的代码,有自己的数据,所以我们说进程间要通信要交换数据的话, 就必须通过专门的通信机制来实现, 其中无名管道是最简单的一种机制, 、

无名管道特点 :
在这里插入图片描述
1.无名管道使用的时候,有一个限制,它的使用范围,无名管道只能用于具有亲缘关系的进程之间的通信, 什么是有亲缘关系 ? 父子进程, 祖孙进程, 兄弟进程 ,他们之间都是具有亲缘关系,在这些进程之间如果要通信的话,那么我们就可以使用无名管道
2.无名管道它是一种单工的通信模式,单工指的是数据,单方向的,一个进程在使用无名进程管道的时候,要么是只能读这个无名管道,要么只能去写,不允许一个进程同时读写一个无名管道。 这是不允许的。
3. 无名管道创建的时候返回的是文件描述符 ,分别用于读写管道 。多个进程通过无名管道通信的时候我们通常要约定好哪些进程读无名管道,哪些进程写无名管道。并且无名管道有固定的读端和写端 ,什么叫读端和写端?无名管道创建好之后,那么它会返回两个文件描述符, 这两个文件描述符分别用于读管道和写管道 。所以我们把这个对应的文件描述符称之为无名管道的读端和写端 。既然,无名管道创建的时候返回的是文件描述符 ,分别用于读写管道 。那么肯定是通过文件IO来操作。

无名管道创建
在这里插入图片描述
用来创建 一个无名管道。
参数 : 整形数组, 之前说过, 无名管道创建好之后会返回两个文件描述符,那么这两个文件描述符怎么样能够传递到我们的程序中呢 ? 实际上就是通过我们的这个参数, 是一个整形的数组,这个数组中,我们在定义数组的时候 会包含两个元素,那么这个整形数组就是用来保存和无名管道对应的文件描述符,所以我们在使用的时候,首先我们先定义一个整形数组,数组要包含两个元素然后把数组名作为参数传递进来, 这样就可以了
返回值 : 整形 ,成功时返回0 ,失败时返回EOF
管道创建好之后, 就可以在有亲缘关系的进程间通信,
为什么无名管道只能用于亲缘关系的进程间通信 ?
无名管道,为什么是无名呢 ?就是管道创建好以后,虽然它返回两个文件描述符,但是无名管道不像我们的普通文件它有路径,它在文件系统中是不可见的,那么是普通文件的话我不同的进程都可以通过路径打开同一个文件,而无名管道它在文件系统中是不可见的,它仅仅在内存中存在,并且无名管道是某一个进程创建的,那么其他进程要想打开无名管道的话,那么唯一 的方法,只能通过继承的方式打开其他进程创建的无名管道。所以说有亲缘关系的进程才可以使用, 比如说 ,我父进程创建了一个无名管道,那么当它创建一个子进程的时候,那么子进程会把父进程打开的文件,包括无名管道给继承过去,相当于我在子进程中也打开了同一个无名管道, 那么这样父子进程就可以通过无名管道进行通信,那么同样祖孙进程兄弟进程都可以通过类似的方式打开同一个无名管道进行通信 。

在这里插入图片描述
虽然返回的两个文件描述符,但是实际上一个进程只能用到其中一个文件描述符
那怎么实现一个双向的进程间通信呢 ? 那么对于无名管道来说,要实现一个双向的进程间通信,需要两个无名管道,因为它最早实现的时候,它就是一个单工的一个单向的数据传输。
在这里插入图片描述
对于无名管道来说,它跟普通文件还不一样 ,虽然我们前面说了这个无名管道,它创建的时候,它会返回两个文件描述符,实际上无名管道跟普通文件是不一样的,普通文件我们从中读出内容之后 ,那么文件中的内容还存在,不影响,但是对于管道不一样 ,管道中的内容一旦读走之后, 那这个内容就已经不存在了,这一点是跟普通文件不一样的。
并且像这种无名管道的特殊文件,它是不支持定位操作的,

在各种不同的情况下,如何正确的去读写无名管道, 实际上不仅仅是无名管道,我们在后面有名管道的读写的特性跟无名管道也是一样的。

我们从无名管道读取内容的话,分以下几种情况:
在这里插入图片描述
1.写端存在 :
当无名管道创建的时候,会返回两个文件描述符,一个是用于读管道叫读端, 一个是用于写管道叫写端。假设我们当前进程创建了一个无名管道,返回两个文件描述符,那么我们说当前这个管道有一个读端和一个写端, 如果当前进程创建了一个子进程,那么子进程会把父进程打开的文件都继承,这个时候相当于子进程中也有一个文件描述符用于读管道,有一个文件描述符写管道,那么我们说当前这样无名管道有两个读端和写端,那么什么叫写端存在 ?
表示至少有一个进程可以通过文件描述符写管道,那么这种情况我们叫写端存在。
当写端存在的时候,我们读无名管道,会有哪些不同的情况 ?
(1)有数据 管道中有数据, 有数据的话一个进程去读管道的话,有数据的话也分成两种情况
一种是管道中的数据大小,比我进程读管道时指定大小要多, 比方说我管道中有100个字节,而我们的进程指定要读50个字节。
另一种情况是,管道中的数据虽然有,但是比进程希望读取的数据要少,比如说我管道中当前有50个字节,而我进程希望去读100个甚至更多的字节, 那么无论是哪一种情况,只要是当前管道中有数据,那么读这个管道的进程它调用read函数执行就能够成功,并且返回实际读取的字节数 。
实际读取字节数:如果进程指定的读取的大小, 如果小于管道中实际的数据的话,那么它指定多少就能读多少,那么如果我们进程指定读的字节数是超过了管道中实际的数据的大小,那么管道中有多少数据,它都能够读取到并返回,所以说它返回的是实际读取的字节数。
(2)无数据 , 当前管道中没有数据的话,那么我们进程去读管道,会阻塞,叫读阻塞,表明我们是因为读管道因为没有数据而阻塞。因为读操作引起的阻塞。直到管道中有数据。直到其他进程往管道中写入数据之后,读管道的这个进程那么就能够从管道中读取回数据,并且返回。
2.写端不存在
这个时候我们去读管道
(1)有数据 管道中有数据, 只要管道中有数据,一个进程就能够读管道,并且返回的是实际读取的字节数。这一点跟有没有写端没有关系。
(2)无数据 这个时候我们的进程去读管道, read并不会阻塞而是立刻返回0,这一点类似我们读普通文件的时候,比如说我们读到文件末尾,就会返回0. 所以我们也经常通过这个read的返回值来判断什么时候数据已经没有了。
比方说我们两个进程通过管道通信,一个进程通过管道发数据,另外一个进程通过管道收这个数据, 那么我们接收这个数据的进程,我们怎么知道什么时候对方数据发送完了呢 ?那么很简单对方如果把数据发送完了,对方会主动关闭管道的写端,那么我接收数据的进程一直读,直到把管道中的数据都读完之后, 并且检测到写端已经不存在了,那么我读的进程read会立刻返回0,那么我们就知道数据已经发送完了。

写无名管道
在这里插入图片描述
1.读端存在
至少有 一个进程可以通过文件描述符读取管道中的内容,那么我们叫读端存在。 读端存在的时候写管道会这么处理?
管道在创建好以后再内存中 去创建,管道也有默认的大小,如果我进程一直往管道中写入的话很显然,管道的空间会一直占用。直到管道它的空间都已经被写满。
(1) 有空间 管道中有足够的空间 ,如何写无名管道。
假设无名管道创建好以后有默认大小是1024个字节,我们往这个无名管道写入数据的时候,假设我们当前进程要写入256个字节,假设当前管道是空的,那么我们从管道最开始写入256个字节,写成功之后,writ操作就会返回,实际写入的字节数。
在这里插入图片描述
(2)无空间 无空间分两种情况
1.空间不足 我上面的例子,比如第二次我要求进程可能要写入1024个字节,而我们管道中剩余的空间没有1024个字节那么这种情况我们叫空间不足,甚至说可能经过多次写之后,我们管道中一点空间都没有了,那么这两种情况都属于空间不足。如果是空间不足的话, 那么我们写管道, 系统会对空间不足的情况 不保证原子操作(原子操作是指一次性的不会被分开的)就是说我应用层可能是通过一次写操作,但是我在写管道的时候比如说原先剩余空间还有1000字节,那我们先写入这么多数据,那么这时候我们的管道,已经写完了,所以已经没有办法再继续写,那么我剩余的数据,只有等到管道再次有空间的时候然后我们再继续写入 。这个叫不保证原子操作。用通俗的话讲就是有多少空间我先写多少,剩余的数据我等有空间的时候再继续写,这个时候实际上写操作是没有完成的。所以进程会阻塞。阻塞到什么时候呢 ?
一直阻塞到写操作完成为止, 也就是说直到其他进程把管道的数据读走了,那么这时候管道中有空间了这时候写进程继续把剩余的数据写到我们的管道中并返回。所以空间不足的时候,实际上进程去写一个管道是会阻塞的。
通过这个特性我们可以很方便的来计算无名管道的大小。、
无名管道实际上创建好之后是有一个默认大小的,那么我们就可以根据我们刚才总结的写管道的这个特性,我们可以很容易的计算出一个管道有多大。我们的基本思路是 ,因为我们也不确定管道的默认大小是多少,所以我们用循环往管道中写入数据,直到写满的时候我们的进程就会阻塞,那么在循环的过程中,只要我写操作返回了,那么就认为数据已经写入了,所以在每次循环中我来统计每循环一次我认为就写入了这么多个数据,做个累加并打印。那么这样就能够很方便的计算出无名管道的大小。
在这里插入图片描述
大小一般通常是以 1024 k为单位
在这里插入图片描述
缺省,即系统默认状态,意思与“默认”相同。
结论,在Linux中创建的无名管道,缺省大小是 64k 字节
2. 读端不存在
无论有没有空间,只要有进程去写一个没有读端的管道 . 这个进程就会异常结束 . (被信号结束)系统叫管道断裂 ! 我们系统不允许任何进程去写一个没有读端的管道 . 因为进程没有读端,意味着你写入数据不会被其他进程读走. 所以这样写也是没有意义的. 所以我们的系统并不允许 ,
(1) 有空间 ,
(2) 无空间 ,
我们如何来验证一下 , 当一个进程去写一个没有读端的管道时 它会被信号结束呢 ?
当子进程结束的时候父进程能够回收 子进程的退出状态, 所以我们在子进程中去写管道然后父进程回收 ,然后我们来验证一下子进程是不是被信号结束 .

在这里插入图片描述
父进程回收完之后, 最低位字节的值如果是0表示子进程正常结束, 如果最低位字节(第七位)0-6非零的话表示是被信号结束的.并且这个值就是信号的类型 .

无论是无名管道 还是 有名管道 它的读写特性都是一样的.
无名管道虽然能够进行进程间通信,但是缺点也很明显,
1.只能用于有亲缘关系的进程之间,
2.并且无名管道是单工的通信方式,只能实现单方面的数据传输, 如果要实现双向的必须要创建两个无名管道。很显然无名管道在使用的时候,显然不够灵活,也不够方便 ,
为了突破无名管道的限制,系统中又实现了有名管道这样一种进程间通信机制, 对于有名管道来说,有名指的是有名管道创建好以后有一个实际的文件和这个有名管道是对应起来的。也就是说当我们创建好一个有名管道之后,那么在我们的文件系统里会看到一个实际的文件 ,那么这个文件有文件,有名称,那么这样一来我们的任意两个进程就可以通过指定同一个有名管道,同一个路径同一个文件名的有名管道。 进而进行通信
文件类型有7类, p代表的是有名管道文件
有名管道 :
在这里插入图片描述
1.对应管道文件 ,任意两个进程都可以通过open打开同一个有名管道。然后在他们之间实现一个通信的过程。
2.有名管道在打开的时候用open打开的时候可以指定不同的读写方式。而不同的读写方式就决定了我们这个进程它是一个读端还是一个写端 。当我们进程打开有名管道的时候,如果用的是只读方式,那么open返回的文件描述符实际上上就代表的是管道的一个读端。那么以只写方式打开的话返回的文件描述符代表的是一个写端 。如果是以读写方式打开一个有名管道的话,返回的文件描述符即代表读端也代表写端。
3.打开管道之后返回的是一个文件描述符,那么之后就可以通过文件IO ,对有名管道进行读写操作
有名管道虽然强调创建的时候有一个对应文件被创建,在我们文件系统中可见,但我们往管道中写入的所有的内容,实际上依然是存放在内存中的,这点跟无名管道是一样的, 并且当管道的读端和写端都关闭的时候,无论是无名管道还是有名管道,当所以的读端和写端都关闭的时候, 那么管道存放在内存中的内容都会被自动释放。

有名管道创建
在这里插入图片描述
返回值: 整形, 成功0 失败返回-1
第一个参数 : path 要创建的 有名管道的文件的路径 ,可以是相对路径,也可以是绝对路径,那么如果没有带路径的话表示是在当前目录下创建。
第二个参数:mode 指定创建的有名管道文件的权限,这个权限跟我们的open函数的第三个函数是一样的。一般我们是通过一个八进制数,三个八进制数,来指定这个管道文件的读写权限。 管道文件跟普通文件还不一样。普通文件有可读可写可执行,那么对于管道文件来说的话,主要就是指定读写的权限,
一旦我们创建好有名管道文件之后,那么我们在程序中通过open根据路径打开一个有名管道,之后通过read write来对管道进行读写。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们的代码里面是运行成功之后会打印,但是运行程序的时候并没有打印这句话,这是什么原因呢 ?说明我们当前这个程序在open的时候阻塞了,那么实际上这也是有名管道使用上的一个特点,有名管道,当进程打开 一个有名管道之后,如果当前只有读端或只有写端的时候open会阻塞,因为你只有读端或者写端的话,实际上对于管道来说,它实际上没有任何的意义,只有当读端和写端都存在了,实际上打开一个管道才有意义。所以先运行read_fifo的时候相当于只有读端,open的时候会阻塞,只有当打开另外一个程序write_fifo运行起来,因为write_fifo是以只写打开方式打开管道的所以它相当于是管道的一个写端,当他 运行起来的时候相当于既有读端又有写端,那么这两个程序open都能够成功
在这里插入图片描述

有名管道在打开的时候有可能会被阻塞,什么时候会被阻塞 ?
当只有读端或只有写端的时候,有名管道打开会阻塞
只有当读端和写端都存在了,open才会返回,程序才会继续往下只写。

信号机制 :
信号是Linux下的进程间通信机制的一种 . 信号的工作原理 实际上和中断的处理是类似的, 我们也说信号它是在软件层次上对中断机制的一种模拟 .
在这里插入图片描述

什么是中断 ?
通常来说的中断 是 硬中断 硬件中断, 是由我们的外部设备的一种工作方式, 这里的中断指的是中断CPU的执行,那么对于外部设备来说, 当它需要被处理的时候,它会产生一个中断信号 .当我们的CPU收到中断信号之后 , 在执行完当前指令 就会去及时的响应中断 .去处理中断 .那么中断处理完之后CPU再返回继续执行 后面的指令. 那么这是中断机制的处理过程.

信号的处理,跟这个中断的处理是类似的.那么首先它是软件层次上的一种 模拟 , 信号并不是由硬件产生的. 它是我们的系统通过内核 产生的一种通信机制 .当一个程序在执行的时候, 如果它收到了信号, 那么程序也会暂停执行, 先去响应信号 .那么响应完信号之后, 那么我们的程序再返回, 继续往下执行.所以说一个程序对信号的处理过程跟我们的系统处理中断是类似的 .并且它是一个异步的通信方式. 那么怎么理解异步的通信方式呢? , 这里指的是一个进程在任何条件下都可以随时的接收信号. 不需要做什么特殊的处理 .

在Linux下面有很多不同的信号类型 ,内核会通过不同的信号通知我们的进程,每个信号代表不同的事件发生 .
信号机制 在unix很早就实现了这种机制, Linux不仅仅是继承了早期的unix的一些信号, 并且进行了一些扩展, 那么针对早期的unix的信号的不足 , Linux进行了扩展, 后面有引入了一些实时信号 .使得信号更加可靠 .那么我们可以通过一个命令来查看当前在我们的系统中都有哪些信号类型 .
kill -l
这个命令就会显示当前系统中所以支持的信号类型, 这里面的前面的31一种就是早期的unix中就已经引入的一些信号, 那么这些信号Linux已经继承了过来 . 早期信号的话因为它不支持信号的排队,所以有可能造成信号的丢失, 所以有的资料上面也把前31种信号称为不可靠信号 .,那么为了克服这些不可靠信号的缺点Linux里面又进行了扩展,那么又引入了一些可以进行排队的不会丢失的一些信号. 用来满足一些不同的需求.
在这里插入图片描述
一个进程, 它的信号有不同的响应方式, 这是可以在程序中进行设置的,一般来说进程对信号的响应方式主要包括有三类,
1. 缺省的方式 我们的系统对每种信号都有默认的处理方式,
2. 忽略信号 忽略某个信号指的是当进程收到这个信号之后不做任何的处理,就跟没有收到信号一样
3. 捕捉信号(注册信号) 指的是我们进程中对某一个或某些信号我们设定一个信号处理函数,当进程收到这个信号以后就会自动的去执行我们事先指定好的信号处理函数 . 这样的响应方式我们叫捕捉信号 .

在这里插入图片描述
SIGHUP信号 : 交互进程:当终端关闭的时候 , 交互进程会自动结束. 终端关闭的时候,向依附于这个终端的所有进程发送SIGHUP信号 , 会把进程结束,所以守护进程和终端无关, 当终端关闭的时候守护进程并不会受终端的影响 .

SIGINT信号 : Ctrl + c 就是产生了一个这个信号, 会把当前终端的所有前台进程终止 .
SIGILL信号 : 这个信号代表当前进程遇到了一条非法指令,那么我们知道进程中它执行的指令是来自于程序,而程序是存放在磁盘上的 , 是我们事先编译好的可执行的一个文件,那么程序是存在磁盘上的,磁盘有可能长时间有可能会出现坏道, 那么这时候存放在磁盘上的这个程序就有可能出现错误,那么我们在执行这个错误的程序的时候就有可能遇到一条非法指令, 那么遇到了非法指令的话,我们的 系统就会向当前系统发送 SIGILL信号, 这个信号也是默认当前的进程终止.
SIGSEV信号 : 当我们程序中非法去访问一个内存的时候, 我们内核就会向当前的进程发送这个信号.例如在程序中错误的使用了野指针 ,空指针,或者说数组越界了,都有可能引起这个信号, 这个信号会把当前的进程结束, 并且会显示段错误 . 所以我们平常所说的段错误,是因为非法访问内存导致内核给当前进程发送这个信号, 这个信号也是默认让这个进程去终止.
SIGPIPE信号 : 总结管道的读写特性的时候,说过,当一个进程去写一个没有读端的管道时,就会收到内核发送的SIGPIPE信号, 代表管道断裂 , 默认也是让当前进程结束
在这里插入图片描述
SIGKILL信号和SIGSTOP信号 : 这两个信号比较特殊,跟其他信号不太一样,其他信号有默认方式,可以设置成默认,也可以设置成忽略,也可以捕捉,但这个SIGKILL信号和SIGSTOP信号我们可以认为它的级别很高只能默认去执行默认的操作,而不能在我们的程序中去忽略或者说去捕捉这两个信号,这两个信号只能使用默认的方式 ,SIGKILL这个信号就是让进程结束的.当一个进程收到这个信号以后, 无论这个进程处于什么状态,它都会立刻结束 ,SIGSTOP信号, 主要是让信号处于停止态 .如果一个进程正在运行的时候,收到了这个信号, 就进入停止态.

SIGTSTP 信号 :跟SIGSTOP信号类似, 都是让进程处于停止态 .,这个信号我们一般都怎么产生呢?通过一个组合键Ctrl + z 这个信号就会发给当前终端的前台进程,让当前终端的前台进程在后台挂起, 就是这个信号使进程的状态发生了改变 .但是这个信号的话也可以忽略也可以捕捉,

SIGCONT 信号 : 跟SIGSTOP和SIGTSTP信号正好相反, 它是让进程重新回到运行态,重新开始运行.实际上我们用gdb调一个程序的时候, 那么我们通过断点设置, 可以让进程进入停止态, 如果我们希望让调试程序重新运行的话,我们gdb有一个键c , c这个命令 ,c这个命令实际上对应的就是SIGCONT这个信号.

SIGALRM信号 : 叫定时器信号 .在程序里可以设置一个闹钟,也叫定时器, 当闹钟时间到的时候,那么我们的内核会给进程发这样一个信号 , 通知进程时钟 ,闹钟,时间已经到了, 这个信号的话默认是让进程终止,所以一般来说我们使用这个信号的时候,通常是捕捉 , 会给定它一个信号处理函数, 比如用它来实现超值检测等等 .

SIGUSR1 和 SIGUSR2 这两个信号是我们系统中用来保留给用户程序使用的 ,那这两个信号实际上是并没有代表具体的系统事件 . 是留给用户程序, 如果程序中要使用到信号通信的话,我们首先可以使用这两个信号. , 这两个信号也是默认让进程终止 .

信号相关命令 :
在这里插入图片描述
主要就是发送信号的命令 , kill / killall
kill [-signal] pid
kill 英文单词的愿意是杀死的意思,
实际上这个命令是用来向一个进程 或 向一个进程组发信号,只不过大部分信号的默认操作是让进程终止, 所以这个命令就叫 kill 但实际上严格的说的话它是向一个进程去发一个信号 , kill这个命令在使用的时候可以跟上一些选项和参数 ,
kill是向进程发信号 ,kill可以指定发送什么样的信号 , ,默认情况下发送的是 15 SIGTERM 这个信号就是用来结束一个进程的 .
在这里插入图片描述
什么时候kill后面跟上信号的类型 ? 不让说 -9 , -11等等根据需要可以指定发送哪一个信号.
kill -9 pid
后面紧跟着参数 , 参数指的是把这个信号发送给谁 pid 描述的是发送的对象
pid的话可以是多个值, 一种情况是, 我指定发送给某一个进程 , 这个pid就是这个进程的进程号, 那么你需要先通过ps命令 先查看到 进程的进程号, 然后把它作为参数来发送. 这是一种情况,
那么除了给某个进程发送信号之外呢 , 实际上还可以给一个进程组发信号 , 也可以给其他所有的进程发信号 ., 一般来说的话, 我们是通常给某一个进程发信号 .
例如 :
kill -9 6437
6437是对方的进程号 , 我们向进程发送类型为9的信号, 除了这样发送之外呢.我还可以
kill -9 -8126
如果是负的数字 , -8126它表示什么含义呢 ?
它代表的是向进程组号为 8126的进程组中所有的进程发信号, 8126是进程组的 组的id,
kill -9 -1
如果是-1的话实际上是表示这个信号是发送给除了系统中init进程和我当前进程之外给所有的进程发信号这样用的话实际上是很危险的. 因为用-9的话,这个-9 SIGKILL相当于 init进程之外包括自己之外其他 进程都会被结束. 那么整个系统可能就无法再运行了.
所以一般来说前两种用法比较多,向某一个指定的进程发信号或者说向某一个指定的进程组, 其中-8123这个绝对值就是进程组的组ID

killall 它可以通过程序的名称,就是进程运行的程序名,或者是根据用户去发送一个信号 .
prog指定进程名 或者说是通过程序
比方说我当前有多个进程都在运行某一个程序,那么我可以通过killall后面跟上程序的名称把所有运行这个程序的进程给它发信号
另外一种 :
user 通过 -u user 这个用户名, 把当前所有这个用户创建的这个进程 , 向它发信号 .

killall 也可以指定发送的信号类型, 如果不指定, 默认发送的是 15 类型的信号
killall a.out
这时候就会向我们系统中所有运行这个程序的进程都发送15这个信号
killall -u username
-u 后面跟上用户名, 这样的话就会向所有username创建的进程发送15这个信号
那么我向其他所有进程发信号的话,会不会把我系统中所有的进程都无意中给结束呢 ?
实际上不用太担心这个问题,因为kill和killall在执行的时候,系统会检查权限,除非你是系统管理员,否则的话普通用户只能向自己创建的进程发信号 .而不允许向其他的用户创建的进程发信号 所以就不会出现把别人用户创建的进程结束的情况 , 但是你是超级用户的话, 你在使用这些命令的时候一定要特别注意, 不然的话有可能造成一些 进程中无意中被你结束

如何去发送一个信号 ?(函数)
当我们再程序中需要发送一个信号的时候, 我们可以使用 函数kill 或者 raise
在这里插入图片描述
kill函数 向一个进程 或 一个进程组发送一个指定的信号, 那么我们在使用这些函数的时候首先我们需要先引入头文件 <signal.h> 这里面定义了用到了一些描述信号类型的宏 ,
功能是发送信号 :
返回值 : 成功返回 0 失败返回-1 并且设置错误信息
第一个参数 : 用来指定发送给谁 , 一种是给定对方的进程号, 接收信号的进程的进程号, 另外一种是指定成0 ,0代表这个信号会发送给当前进程的同组进程 , 如果是-1的话表示这个信号会发送给除了我们系统的init进程和我当前进程之外的所有进程, 也可以通过进程组号 , 发给指定的进程组, kill这个函数的参数跟kill命令的参数含义是一样的,只不过一个是命令另外一个是函数 .
第二个参数 : 指定要发送的信号的类型 .习惯是通过宏来指定, 不太建议直接指定数字, 因为数字的话很难去记忆 ,不像宏非常直观 ,代表可读性更好 .

那么kill这个函数,能不能向我当前进程发信号 ?
没有问题, 第一个参数指定成当前进程的进程号, 比如说我通过getpid能够获取当前进程的进程号,我就可以向自己发送一个信号, 那么同样也可以向父进程发送信号, 没有问题

raise函数 作用是向当前进程 ,自己给自己发信号, 所以它只有一个参数 ,
参数 : 要发送的信号的类型 (kill完全可以替代这个函数)
在这里插入图片描述
alarm 函数创建一个闹钟 . 一个定时器,
返回值 : 整型, 返回的是上一个定时器的剩余时间, 失败返回EOF -1
在Linux下面 一个程序中只能有一个定时器 ,当我们用alarm设定一个新的定时器的时候, 那么原先的定时器就会失效 , 同时返回原先的定时器的剩余时间 .
参数 : seconds 指定定时器的时间 . 什么时候触发 表示从当前时间开始经过多少秒,所以单位是秒, 以秒为单位,若干秒之后定时器的时间到, 内核就会向当前进程发送一个定时器的信号 . 这个秒数的话还有一个特殊用法, 就是如果我这个地方指定的是一个0的话, 表示的是取消当前的定时器.
一个进程中只能有一个定时器, 当我们设定一个新的定时器的时候, 原先的定时器就失效了,

pause 函数 它让进程进入一个等待态 ,阻塞睡眠 , 那么什么时候返回呢? 直到有信号来,当进程收到一个信号以后, 进程会首先去响应信号, 如果进程没有结束,那么进程就会从pause返回, 继续往下执行,
返回值 : 通常一直是 -1 , errno通常一直是4 SIGALRM 表示被信号中断
在这里插入图片描述
如果你想等某一个信号来的话,那么你就不能让当前的进程结束, 要让他睡眠 ,或死循环都可以.
3秒过后,闹钟时间到, 这个时候我们的内核 就会向我们的进程发送一个定时器信号 .那么收到这个定时器信号以后 ,我们的进程首先去响应这个定时器信号 . 定时器信号默认的操作是让进程终止 ,也就是说进程需要定时器信号的时候, 它实际上就已经结束了, pause后面的打印语句实际上是没有被执行的.,所以我们这个程序执行的时候 ,打印出来的是另外一个信息Alarm clock 这个信息实际上就是定时器缺省的处理方式, 定时器信号缺省的处理方式里面会打印这句话,然后把当前进程结束 ., 因为进程结束了, 所以没有办法再返回继续往下执行

网络超时检测 : 在网络通信的时候,比方说我使用套接字接收数据, 如果一直没有数据的话那么我们这个程序就一直会在阻塞, 等待数据 , 一种是一直阻塞, 直到有数据来, 还有 一种情况就是,我可能指定一个超时, 那么我们可以等待数据但是我会设定一个最长的等待时间, 这时候我们就可以利用定时器来实现, 我们在接收数据之前 , 我们首先创建一个定时器, 比如说我定义一个六秒的最长等待时间, 创建一个6秒的闹钟, 开始接收我们的数据, 如果6秒内数据来了,我们把定时器清空, 处理数据,然后循环回来. ,在下一次接收数据之前 , 再次设定闹钟, 那么如果六秒钟时间之内没有数据来,那么六秒钟的时候定时器信号来了, 那么这个时候我们程序就可以去捕捉这个定时器信号 ,比如打印 一下错误信息, 或者让进程跳出循环等等,都可以.

设置信号的响应方式 :
一个进程对信号的响应方式主要有三种 , 一种是按照缺省的方式, 一种是忽略, 一种是可以捕捉一个信号, 我们这里就可以利用signal这个函数, 来设置当前进程对某一个信号的响应方式,
在这里插入图片描述
signal函数不是发信号的, 仅仅是把某个信号和一个信号处理函数关联起来, 那么在执行的时候,并没有任何信号的产生, 我们的进程也不会阻塞 .仅仅是把某一个信号和一个信号处理函数关联起来, 这样当进程收到这个信号的时候就会自动去执行 我们指定的这个信号处理函数 .

第一个参数 : int signo 整型 ,含义就是要设置的信号类型 ., 这里可以是任何一种合法的信号 .除了我们前面讲的SIGKILL信号和SIGSTOP这两个信号级别很高,是不能够被设置的,
第二个参数 : void (*handler)(int) 是一个函数指针, 存放的是某一个函数的入口地址, 所以我们在调用signal的时候, 就需要先定义好一个信号处理函数, 然后把这个函数名 , 函数名代表函数入口地址,作为实参传递进来, 并且这个信号处理函数的接口是固定的 ,那么这个信号处理函数它有一个整型的参数 ,没有返回值 , handler 通常有三种方式来指定, 其中两个是宏 ,SIG_DFL代表缺省方式,这个宏表示把这个信号恢复成默认的处理方式,(实际上程序运行的时候,每个信号都是缺省的方式) , SIG_IGN代表的是忽略指定的信号. ,除此之外我们 还可以捕捉这个信号 ,所谓捕捉信号 我们自己指定一个信号处理函数,当然这个函数的接口是固定的, 我们把这个函数通过signal跟某一个信号关联起来 .这种方式 我们叫捕捉这个信号.

signal 函数的返回值 :
实际上也是一个函数指针 , 实际上跟我们signal的第二个参数是完全相同的类型. 它的返回值也是一个函数指针 . 成功的时候返回的是原先的信号处理函数,也是一个函数指针,失败时返回一个特定的值 SIG_ERR

在这里插入图片描述
在这里插入图片描述
main函数里面调用了两次 signal分别设置两个信号 一个是SIGINT一个信号是SIGQUIT ,我们可以看到我们分别把INT信号跟QUIT信号都通过同一个信号处理函数 ,handler关联起来 ,从这里我们就能够看出,实际上我们对于不同的信号,我们可以指定相同的信号处理函数,这是允许的 , 当然了你也可以给每个信号指定一个不同的信号处理函数 . 如果指定了同一个信号处理函数的话, 意味着无论我当前进程收到INT还是收到QUIT都会去执行handler这个函数 , 那么我们在这个信号处理函数里,怎么去区分 到底当前进程收到的是SIGINT还是SIGQUIT信号呢?
信号函数的接口是固定的 有一个整型的参数 , 没有返回值, 整型参数的含义就很重要了,这个整型的参数就是当前信号收到的类型, 这个参数是内核给当前进程传的参数, 所以我们在信号处理函数中,如果是多个信号 共用同一个信号处理函数, 我们就可以根据信号处理函数中这个形参的值判断当前进程收到的是哪一个信号.可以用switch , 我们可以看到signal只是把一个信号和一个信号处理函数关联起来. signal这个函数本身并不会发送信号, 并且signal执行的时候,进程并不会阻塞 , 进程继续往下执行 while(1) pause();循环进入一个睡眠,这时候如果进程收到了SIGINT信号就会去执行handler. 直到当前进程被其他信号结束 ,才会退出

signal设置只需要设置一次就会一直有效 ,并不需要在我们的程序中重复的去设置某一个信号 .

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值