目录
- 一、进程的简介
- 1、什么是进程,进程的概念
- 2、进程状态
- 3、什么是进程号
- 4、进程间的通信方法(IPC)
- 二、 fork()创建子进程
- 三、父、 子进程间的文件共享
- 1、实验1
- 2、实验2
- 四、使用execl函数执行新程序
- 五、关于终端上对进程的一些指令操作
- 六、孤儿进程
- 七、僵尸进程
- 1、监视子进程
- 八、守护进程
- 1、何为守护进程
- 2、编写守护进程程序
- 九、进程间的通信方法实现
- 1、管道通信
- ①无名管道通信
- ②有名管道通信
- 2、信号通信
- 信号的目的是用来通信的
- 信号由谁处理、怎么处理
- 信号是异步的
- ①信号的分类
- Ⅰ、可靠信号与不可靠信号:
- Ⅱ、实时信号与非实时信号:
- ②信号的发送函数及使用测试实验
- Ⅰ、raise()函数
- Ⅱ、kill()函数
- Ⅱ、alarm()函数
- ③信号的接收函数及使用测试实验
- Ⅰ、pause()函数
- ④信号的处理函数及使用测试实验
- Ⅰ、signal()函数
- 3、共享内存、消息队列、信号量更新中...
一、进程的简介
1、什么是进程,进程的概念
进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。
2、进程状态
Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、 可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
⚫ 就绪态(Ready) : 指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程;
⚫ 运行态: 指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
⚫ 僵尸态: 僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”;
⚫ 可中断睡眠状态: 可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒;
⚫ 不可中断睡眠状态: 不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态) ,表示进程处于一种等待状态,等待某种条件成立之后便会进入到就绪态;所以,处于等待态的进程是无法参与进程系统调度的。
⚫ 暂停态: 暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。一个新创建的进程会处于就绪态,只要得到 CPU 就能被执行。以下列出了进程各个状态之间的转换关系,如下所示:
3、什么是进程号
Linux 系统下的每一个进程都有一个进程号(process ID,简称 PID),进程号是一个正数,用于唯一标
识系统中的某一个进程。
4、进程间的通信方法(IPC)
①管道通信:分别为有名管道和无名管道。
②信号。
③共享内存。
④消息队列。
⑤信号量。
⑥套接字(socket)。
二、 fork()创建子进程
三、父、 子进程间的文件共享
调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(),
这也意味着父、子进程对应的文件描述符均指向相同的文件表,如下图所示:
由此可知,子进程拷贝了父进程的文件描述符表,使得父、子进程中对应的文件描述符指向了相同的文件表, 也意味着父、子进程中对应的文件描述符指向了磁盘中相同的文件,因而这些文件在父、子进程间实现了共享,譬如,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置偏移量。
1、实验1
父进程打开文件之后,然后 fork()创建子进程,此时子进程继承了父进程打开的文件描述符(父进程文件描述符的副本) ,然后父、子进程同时对文件进行写入操作。
上图测试结果可知,此种情况下,父、子进程分别对同一个文件进行写入操作,结果是接续写,不管是父进程,还是子进程,在每次写入时都是从文件的末尾写入, 很像使用了 O_APPEND 标志的效果。 其原因也非常简单, 图 9.6.1 中便给出了答案,子进程继承了父进程的文件描述符,两个文件描述符都指向了一个相同的文件表,意味着它们的文件偏移量是同一个、绑定在了一起,相互影响,子进程改变了文件的位置偏移量就会作用到父进程,同理,父进程改变了文件的位置偏移量就会作用到子进程。
2、实验2
父进程在调用 fork()之后,此时父进程和子进程都去打开同一个文件,然后再对文件进行写入操作。
上图测试结果可知,这种文件共享方式实现的是一种两个进程分别各自对文件进行写入操作,因为父、子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况。
四、使用execl函数执行新程序
当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码,那么这个时候子进程可以通过 exec 函数来实现运行另一个新的程序。而execl函数是exec族函数里面的其中一个员,但它是比较常使用的那个。
1、先创建新的程序hello.c文件。
2、创建一个execl.c文件,功能:创建子进程,并且将子进程继承的父进程的程序替换成新的程序,并运行。
上图测试现象可知,execl函数是可以替换子进行的程序,然后执行新的程序的。
五、关于终端上对进程的一些指令操作
指令:ps -aux
其功能是查看系统进程的详细信息。
指令: | grep
其功能是管道筛选信息。
指令:kill -9 [pid]
其功能是杀死,结束对应进程。
六、孤儿进程
父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程;因为子进程在结束后需要父进程将子进程的一些资源释放掉,而父进程先于子进程结束的话,那么系统就会让init进程接管父进程职责,释放结束后的子进程资源。 在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程, 换言之,某一子进程的父进程结束后,该子进程调用 getppid()将返回 1, init 进程变成了孤儿进程的“养父”。
孤儿进程程序实现与测试:
由上图测试结果可知,在红色框框中,父进程还未结束;在黄色框框中,由于子进程sleep阻塞了3秒,父进程先于子进程结束了,然后子进程的父进程(pid=10604)变成了父进程(pid=2122),在右边框框可知,pid:2122是/sbin/upstart其实就是init进程。
七、僵尸进程
进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用wait()(或其变体 waitpid()、 waitid()等)函数回收子进程资源,归还给系统。
如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。 子进程结束后其父进程并没有来得及立马给它“收尸”, 子进程处于“曝尸荒野”的状态,在这么一个状态下,我们就将子进程成为僵尸进程;至于名字由来,肯定是对电影情节的一种效仿!
当父进程调用 wait()(或其变体,下文不再强调)为子进程“收尸”后,僵尸进程就会被内核彻底删除。另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(), 故而从系统中移除僵尸进程。
如果父进程创建了某一子进程,子进程已经结束,而父进程还在正常运行,但父进程并未调用 wait()回收子进程,此时子进程变成一个僵尸进程。 首先来说,这样的程序设计是有问题的,如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!所以,在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用 wait()将其回收,避免僵尸进程。
僵尸进程程序实现与测试:
通过命令可以查看到子进程 10997 依然存在,可以看到它的状态栏显示的是“Z”(zombie,僵尸),表示它是一个僵尸进程。 僵尸进程无法被信号杀死,大家可以试试,要么等待其父进程终止、要么杀死其父进程,让 init 进程来处理,当我们杀死其父进程之后,僵尸进程也会被随之清理。
1、监视子进程
在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视。而这可以通过系统调用 wait()以及其它变体来监视子进程的状态改变。
wait()函数:
对于许多需要创建子进程的进程来说,有时设计需要监视子进程的终止时间以及终止时的一些状态信息,在某些设计需求下这是很有必要的。 系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息,其函数原型如下所示:
使用wait函数监视子进程程序实现与测试:
实验现象可知,父进程也是经过2秒的等待后才进行对子进程收尸处理的,说明wait()函数能起到监视和减少子进程变成僵尸进程发生。
八、守护进程
1、何为守护进程
守护进程(Daemon) 也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生, 主要表现为以下两个特点:
⚫ 长期运行。 守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终止,否则直到系统关机都会保持运行。 与守护进程相比,普通进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响,它们将会一直运行着、直到系统关机。
⚫ 与控制终端脱离。 在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都会依附于这个终端,这是上一小节给大家介绍的控制终端,也就是会话的控制终端。当控制终端被关闭的时候, 该会话就会退出, 由控制终端运行的所有进程都会被终止, 这使得普通进程都是和运行该进程的终端相绑定的; 但守护进程能突破这种限制,它脱离终端并且在后台运行, 脱离终端的目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息所打断。
守护进程是一种很有用的进程。 Linux 中大多数服务器就是用守护进程实现的,譬如, Internet 服务器inetd、 Web 服务器 httpd 等。同时,守护进程完成许多系统任务,譬如作业规划进程 crond 等。
守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。守护进程与终端无任何关联,用户的登录与注销与守护进程无关、不受其影响,守护进程自成进程组、自成会话,即pid=gid=sid。通过命令"ps -ajx"查看系统所有的进程,如下所示:
TTY 一栏是问号?表示该进程没有控制终端,也就是守护进程,其中 COMMAND 一栏使用中括号[]括起来的表示内核线程,这些线程是在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用 k 开头的名字,表示 Kernel。
2、编写守护进程程序
编写守护进程一般包含如下几个步骤:
1) 创建子进程、终止父进程
父进程调用 fork()创建子进程,然后父进程使用 exit()退出,这样做实现了下面几点。第一,如果该守护进程是作为一条简单地 shell 命令启动,那么父进程终止会让 shell 认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程ID,但它有自己独立的进程ID,这保证了子进程不是一个进程组的组长进程,这是下面将要调用 setsid 函数的先决条件!
2) 子进程调用 setsid 创建会话
这步是关键,由于之前子进程并不是进程组的组长进程,所以调用 setsid()会使得子进程创建一个新的会话,子进程成为新会话的首领进程,同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。 所以这里调用 setsid 有三个作用:让子进程摆脱原会话的控制、让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。在调用 fork 函数时,子进程继承了父进程的会话、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。 setsid 函数能够使子进程完全独立出来,从而脱离所有其他进程的控制。
3) 将工作目录更改为根目录
子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录,当然也可以指定其它目录来作为守护进程的工作目录。
4) 重设文件权限掩码 umask
文件权限掩码 umask 用于对新建文件的权限位进行屏蔽。由于使用 fork 函数新建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此, 把文件权限掩码设置为 0, 确保子进程有最大操作权限、这样可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是 umask,通常的使用方法为 umask(0)。
5) 关闭不再需要的文件描述符
子进程继承了父进程的所有文件描述符, 这些被打开的文件可能永远不会被守护进程(此时守护进程指的就是子进程,父进程退出、子进程成为守护进程) 读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载,所以必须关闭这些文件, 这使得守护进程不再持有从其父进程继承过来的任何文件描述符。
6) 将文件描述符号为 0、 1、 2 定位到/dev/null
将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,这使得守护进程的输出无处显示、也无处从交互式用户那里接收输入。
7) 其它:忽略 SIGCHLD 信号
处理 SIGCHLD 信号不是必须的, 但对于某些进程,特别是并发服务器进程往往是特别重要的,服务器进程在接收到客户端请求时会创建子进程去处理该请求,如果子进程结束之后,父进程没有去 wait 回收子进程,则子进程将成为僵尸进程;如果父进程 wait 等待子进程退出,将又会增加父进程的负担、也就是增加服务器的负担,影响服务器进程的并发性能,在 Linux 下,可以将 SIGCHLD 信号的处理方式设置为SIG_IGN,也就是忽略该信号,可让内核将僵尸进程转交给 init 进程去处理,这样既不会产生僵尸进程、又省去了服务器进程回收子进程所占用的时间。
从上图可知, daemon进程成为了一个守护进程,与控制台脱离,当关闭当前控制终端时,daemon进程并不会受到影响,依然会正常继续运行;
守护进程可以通过终端命令行启动,但通常它们是由系统初始化脚本进行启动,譬如/etc/rc*或/etc/init.d/*等。
补充知识点:
九、进程间的通信方法实现
1、管道通信
①无名管道通信
pipe无名管道通信只能作用在有亲缘关系的进程之间通信,详情如下图所示:
pipe是一种单向数据传输的管道,其参数pipefd[2]是个整形数组,而pipefd[0]和pipefd[1]分别是创建的读管道的文件描述符和写管道的文件描述符。
使用pipe函数实现父进程与子进程间的通信及测试:
从上图可知,当子进程读管道数据时,如果管道没有数据就会一直阻塞。只有管道有数据可读,子进程才会通,如下图所示:
在父进程中向管道写入数据,然后子进程向管道读取数据。
②有名管道通信
mkfifo有名管道通信可以作用在无亲缘关系的进程之间通信,详情如下图所示:
mkfifo其实就是创建了一个实实在在的fifo文件,然后进程间的通信就是通过对创建的fifo文件进行操作,其fifo文件和块设备文件一样,都是不占用字节的,如下图所示:
使用mkfifo函数实现两个无亲缘关系的进程间的通信及测试:
读fifo数据的进程程序:
写fifo数据的进程程序:
现象:
2、信号通信
信号的基本概念:信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程, 其实是在软件层次上对中断机制的一种模拟。 大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。
信号的目的是用来通信的
一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。 信号可以由“谁”发出呢? 以下列举的很多情况均可以产生信号:
⚫ 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为 0、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。
⚫ 用于在终端下输入了能够产生信号的特殊字符。 譬如在终端上按下 CTRL + C 组合按键可以产生中断信号(SIGINT),通过这个方法可以终止在前台运行的进程;按下 CTRL + Z 组合按键可以产生暂停信号(SIGCONT),通过这个方法可以暂停当前前台运行的进程。
⚫ 进程调用 kill()系统调用可将任意信号发送给另一个进程或进程组。 当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是 root 超级用户。
⚫ 用户可以通过 kill 命令将信号发送给其它进程。 kill 命令想必大家都会使用,通常我们会通过 kill命令来“杀死”(终止)一个进程,譬如在终端下执行"kill -9 xxx"来杀死 PID 为 xxx 的进程。 kill命令其内部的实现原理便是通过 kill()系统调用来完成的。
⚫ 发生了软件事件,即当检测到某种软件条件已经发生。 这里指的不是硬件产生的条件(如除数为 0、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的定时器已经超时、进程执行的 CPU 时间超限、进程的某个子进程退出等等情况)。
进程同样也可以向自身发送信号,然而发送给进程的诸多信号中,大多数都是来自于内核。
以上便是可以产生信号的多种不同的条件,总的来看,信号的目的都是用于通信的,当发生某种情况下,通过信号将情况“告知”相应的进程,从而达到同步、通信的目的。
信号由谁处理、怎么处理
信号通常是发送给对应的进程,当信号到达后, 该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:
⚫ 忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是 SIGKILL 和 SIGSTOP,这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。
⚫ 捕获信号。 当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux 系统提供了 signal()系统调用可用于注册信号的处理函数。
⚫ 执行系统默认操作。 进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。
信号是异步的
信号是异步事件的经典实例,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。
信号本质上是 int 类型数字编号,这些信号在<signum.h>头文件中定义, 每个信号都是以 SIGxxx 开头,这里就自己去查了。
①信号的分类
Ⅰ、可靠信号与不可靠信号:
不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。
在 Linux 系统下使用"kill -l"命令可查看到所有信号,如下所示:
括号" ) "前面的数字对应该信号的编号,编号 1~31 所对应的是不可靠信号,编号 34~64 对应的是
可靠信号,从图中可知,可靠信号并没有一个具体对应的名字,而是使用了 SIGRTMIN+N 或 SIGRTMAXN 的方式来表示。
Ⅱ、实时信号与非实时信号:
实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的, 非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。 实时信号保证了发送的多个信号都能被接收, 实时信号是 POSIX 标准的一部分,可用于应用进程。
②信号的发送函数及使用测试实验
Ⅰ、raise()函数
可以在终端中敲man 3 raise可知raise函数的功能,如下图所示:
由上图可知,raise(sig)等价于kill(getpid(), sig),也就是说其功能就是自己给自己发送信号(实质是先将信号和pid号发给内核,内核再发送该信号给对应的pid进程或直接处理)。
这里我做一个简单自己杀死自己的实验,如下图所示:
Ⅱ、kill()函数
这里不过多介绍该函数了,可以在man手册查询。
这里我做一个简单 “进程kill” 杀死手动指定的 “进程a” 的实验,如下图所示:
Ⅱ、alarm()函数
这里不过多介绍该函数了,可以在man手册查询。
这里我做一个简单闹钟定时程序,当时间到了发出闹钟时间就终止程序的实验,如下图所示:
③信号的接收函数及使用测试实验
Ⅰ、pause()函数
pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,详细信息可以可以在man手册查询。
这里我做一个简单闹钟定时使程序退出pause状态,这次闹钟程序不是用默认的信号处理函数,当时间到了发出闹钟时间就触发pause函数,让程序继续往下走,如下图所示:
④信号的处理函数及使用测试实验
Ⅰ、signal()函数
当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作。
忽略信号:
系统默认:
捕获信号: