文章目录
一.让程序在后台运行
在之前的章节中,如果要想程序,在命令提示行下输入程序名后按回车键,程序被运行,然后等待程序运行完成。在程序运行的过程中,也可以用 Ctrl+c 中止它。
在实际中,我们需要让程序在后台运行,没有界面,没有用户输入数据,例如 socket服务端程序。
如果想让程序在后台运行,有两种方法。
1.加 “&” 符号让程序在后台运行
如果想让程序在后台运行,执行程序的时候,命令的最后面加“&” 符号。如:
./C++封装服务端 5005 &
(1)输入了这个命令行,按回车,就会出现,这个进程的编号。如:
(2)查看一下这个程序是不是真的运行,
从图片中看到这个对应进程的编号,这个程序真的是在后台运行了。
(3)从图片可以得到 4475 的父进程是 4452,我们来看一下4452这个进程是什么。
我们会看到这个进程后面有 “-bash” 的标识,这个是什么。一般来说,安装linux时你如果没有改shell,默认的都是bash,所以当你开启一个终端时就会生成一个叫bash的进程,在打开一个终端,又会生成一个bash的进程,关掉终端一个终端就少一个bash进程。
Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它接收用户命令,然后调用相应的应用程序。
介绍Bash之前首先介绍Shell,shell是一个程序,可以称之为壳程序,用于用户与操作系统进行交互。用来区别与核,相当于是一个命令解析器,Shell有很多种,这里列出其中几种 :
Bourne SHell(sh)
Bourne Again SHell(bash)
C SHell(csh)
KornSHell(ksh)
zsh
各个shell的功能都差不太多,在某些语法的下达下面有些区别,Linux预设就是bash。
简单点说,直接把shell和bash先理解为一个东西好了,就是Linux中的那个终端窗口(Terminal),也就是那个小黑框,下面的例子都是在Linux的终端窗口中运行的。
2.如何中断程序
2.1 Ctrl+c 无法中断
在后台运行的程序,用 “Ctrl+c” 无法中断,并且就算终端退出了,程序仍在后台运行。
(1)我直接用 ==“exit”==退出这个终端,然后再次连接,查看系统的进程,4475这个进程还在运行。
(2)我们会看到 4475 这个进程的父进程是 1 。还记得编号为1的进程是什么进程吗?这是系统进程
这个就说明了: 如果终端退出了,后台运行的程序将由系统托管。
那么我该如何中断在后台运行的程序呢?
2.2 使用 killall 杀死进程
(1)killall + 程序名
(2)killall + 进程编号
使用了这个命令之后,再次查看进程,发现 4475 这个进程已经没有了。
3.采用 fork()让程序在后台运行
另外一种方法就是 采用 fork() 函数。主程序执行 fork() ,生成一个子进程,然后父进程退出,留下子进程继续运行,子进程将由系统托管。 要记得是父进程先退出,如果是子进程先退出(白发人送黑发人),会产生僵尸进程。
(1)我在 socket通信的服务端的主程序加上fork() 函数,并且让父进程退出。
(2)执行程序
(3)查看进程,如下图,会看到 4619 这个进程的父进程是 1,说明了它是由系统托管的。也说明了 fork() 产生的子进程能在后台运行。
二.signal 信号
signal 信号是Linux编程中非常重要的部分,接下来将详细介绍信号的基本概念、实现和使用,和与信号的几个系统调用(库函数)。
signal 信号是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断,从它的命名可以看出,它的实质和使用很像中断。
1.信号的基本概念
软中断信号(signal,又简称为信号)用来通知进程发生了什么事件 。进程之间可以通过调用 kill 库函数 发送软中断信号。Linux内核也可能给进程发送信号,通知进程发生了某个事件(例如内存越界)
信号只是用来通知某个进程发生了什么事件,无法给进程传递任何数据,进程对数据的处理方法有三种
1.1处理信号的方法
1.1.1 忽略某个信号
第一种方法是:忽略某个信号,对该信号不做任何处理,就像没有发生过一样。
1.1.2 设置函数处理
第二种方法:设置中断的处理函数,收到信号后,由该函数来处理。
1.1.3采用系统默认处理
第三种方法是:对该信号的处理采用系统的默认操作,大部分的信号的默认操作时终止进程。
2.信号的类型
发出信号的原因有很多,这里按发出信号的原因简单分类,以简单了解各种信号。
处理动作一项中的字母含义如下:
(1)A 缺省的动作是终止进程。
(2)B 缺省的动作是忽略此信号,将该信号丢弃,不做处理。
(3)C 缺省的动作是终止进程并进行内核映像转储(core dump),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员 提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。
(4)D 缺省的动作是停止进程,进入停止状况以后还能重新进行下去。
(5)E 信号不能被捕获。
(6)F 信号不能被忽略。
3.signal 库函数
signal 库函数可以设置程序对信号的处理方式。 在Linux 系统中定义了一些信号,当我们的操作触发了这些信号,signal 库函数就会捕捉到这个信号,做出对应的处理。就比如说,在程序运行的时候,我想要停止这个程序的,按下 Ctrl+c 。这个时候 signal 库函数就会捕捉到这个信号,就会终止这个程序。这个处理方法是可以自己定义的。
1.函数的声明及参数
sighandler_t signal(int signum,sighandler_t,handler);
(1)参数 signum: 表示信号的编号,也可以是信号名
(2)参数 handler :表示信号的处理方式,有三种情况:
1)SIG_IGN(signal ignore):忽略参数 signum 所指的信号
2)一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数
3)SIG_DFL(signal default):恢复参数 signum 所指的信号的处理方法为默认值。
不用关心 signal 的返回值。
4.信号有什么用
服务程序运行在后台,如果想让中止它,强行杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有释放资源,会影响系统的稳定,用Ctrl+c中止与杀程序是相同的效果。
如果能向后台程序发送一个信号,后台程序收到这个信号后,调用一个函数,在函数中编写释放资源的代码,程序就可以有计划的退出,安全而体面。
信号还可以用于网络服务程序抓包等,这是较复杂的应用场景,暂时不介绍。
5.信号的应用
在实际开发中,在main函数开始的位置,程序员会先屏蔽掉全部的信号。这么做的目的是不希望程序被干扰。然后,再设置程序员关心的信号的处理函数。
for (int ii=0;ii<100;ii++) signal(ii,SIG_IGN);
这段代码就是用来屏蔽全部的信号的,因为信号的编号的范围在(0-99)之内,所以会被全部屏蔽。信号的编号在信号的类型那里查看。
5.1 测试屏蔽所有信号
那现在就来测试一下,当屏蔽了所有的信号,我执行 Ctrl+c 和执行killall 会怎么样。
(1)现在 socket 通信的服务端的 main 函数开始的位置,放置屏蔽所有信号的代码段。
(2)执行这个程序,然后执行 Ctrl+c 和 killall 命令,程序是没有终止的。
如果不相信,我们来查看一下系统中的进程,当我执行了 Ctrl+c 和 killall 命令 这个进程还是存在的,进程编号为 5026
(3)这时候问题就来了,那这个进程要如何终止呢?其实系统还留了最后一手,就算你屏蔽了所有的信号,但是有一个是无法屏蔽的——killall -9 +进程编号(进程名称) 这个命令会 强制杀死进程。
那么我们就来测试一下,刚刚那个 5026 的进程还没有终止,现在我执行 killall -9 C++服务端 这个命令。
执行了这个命令,系统弹出空白的命令行,说明是生效的。我们再来查看一次系统的进程
5026 这个进程没有在运行了。说明这个命令生效了。
5.2 测试编写命令处理函数
这次我们选择捕获 SIGINT(Ctrl + c)和 SIGTERM(killall)。
(1)我们先屏蔽掉所有的信号处理,也就是在 main 函数的开始加上,屏蔽代码段。
(2)接下来,我们就编写这两个信号的处理函数,处理函数必须要有一个参数,用来传信号对应的值。
void EXIT(int sig)
{
printf("收到了信号%d,程序退出。\n",sig);
//在这里添加释放资源的代码。
TCPServer.~CTcpServer();
exit(0); //程序退出
}
(3)把这个处理函数,放在捕获函数 signal() 里面,用来处理捕获的信号(在命令行执行的命令)。
Ctrl+c 对应 SIGINET,对应数值 2 :
killall 对应 SIGTERM,对应数值15
(4)执行这个程序,并执行 Ctrl+c 命令,结果如下
重新启动程序,再执行 killall 这个命令,结果如下。有时候 ,killall 和执行程序的窗口是不能同时执行的,要另开一个窗口执行 killall
(5)编写的处理函数也可以在函数的其他地方调用。比如说 之前有错误或者不执行下面的代码,我们经常用 return(-1) ; 那么现在就可以用 这个编写好的处理函数代替了。
5.3 测试捕获 killall -9 命令
(1)将捕获信号的处理函数加进去
(2)执行程序,并执行这个命令,结果如下
执行这个命令时,程序突然杀死,但是并没有“接收到信号xx,程序退出”这样的提示信息,说明这个命令,不是 signal(SIGKILL,EXIT) 捕获。说明此信号不能被忽略,也无法捕获。
5.4 总结
程序员关心的信号有三个:SIGINT、SIGTERM和SIGKILL。
程序在运行的进程中,如果按Ctrl+c,将向程序发出SIGINT信号,信号编号是2。
采用“kill 进程编号”或“killall 程序名”向程序发出的是SIGTERM信号,编号是15。
采用“kill -9 进程编号”向程序发出的是SIGKILL信号,编号是9,此信号不能被忽略,也无法捕获,程序将突然死亡。
所以,程序员只要设置SIGINT和SIGTERM两个信号的处理函数就可以了,这两个信号可以使用同一个处理函数,函数的代码是释放资源。
三.发送信号
Linux操作系统提供了kill命令向程序发送信号,C语言也提供了kill库函数,用于在程序中向其它进程或者线程发送信号。
3.1 函数声明及参数
函数声明:
int kill(pid_t pid, int sig);
kill函数将参数sig指定的信号给参数pid 指定的进程。
参数pid 有几种情况:
1)pid>0 将信号传给进程号为pid 的进程。
2)pid=0 将信号传给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意,发送信号者进程也会收到自己发出的信号。
3)pid=-1 将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信息。
sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在运行。
返回值说明: 成功执行时,返回0;失败返回-1,errno被设为以下的某个值。
EINVAL:指定的信号码无效(参数 sig 不合法)。
EPERM:权限不够无法传送信号给指定进程。
ESRCH:参数 pid 所指定的进程或进程组不存在。
3.2 测试 kill() 函数,pid>0
(1)先启动 C++封装服务端程序,然后查看这个进程的进程编号,作为 kill() 函数的第一个参数。进程编号为 5267
(2)用另外一个程序向 5267 进程发信号。信号为2
(3)执行这个小程序,C++封装服务端程序会收到信号2,然后退出程序。
3.3 测试 kill() 函数,pid=0
这次用多进程的服务端程序测试‘
(1)先在多线程的服务端程序,修改代码。在主程序修改捕获信号函数以及处理函数。
1)
2)这次的测试,是先向父进程发信号,然后父进程给子进程发信号。当父进程收到了信号(杀死进程)时,然后去调用处理函数处理。在父进程退出之前,给子进程发了信号(杀死进程的信号)。
(2)执行多进程服务端,然后查看进程编号。进程编号为 5814
(3)执行多个客户端,产生多个子进程。这时候子进程就有了两个。
(4)执行另外一个小程序,向服务端程序(父进程)发信号(终止信号)。
(5)查看各个进程的状态。一开始是父进程先收到信号2,然后打印接收情况。在退出之前给两个子进程发信号(15),接着就是子进程收到信号,打印接收情况。这里为什么有三个收到信号15的呢?pid=0 将信号传给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意,发送信号者进程也会收到自己发出的信号。同时验证了这句话。