19.Linux 进程

我们要了解Liunx内核提供的进程、管道、信号等内核对象,了解这些内核对象的应用将有助于我们更好地了解Linux内核究竟为应用层提供了什么特性,也有且于后续编写更底层的驱动程序。

1.简单了解进程

一般来说 ubuntu 中有很多 shell 终端,而这个终端就是一个进程,或许读者很可能打开多个端,那么这些所有被打开的终端就一个个进程,这些进程是不一样的,独立运行在系统中,打开三个 shell 终端,这些终端各自有输入输出,互不干扰。
每个运行中的 shell 都是一个单独的进程,假如读者从一个 shell 里面调用一个程序的时候,对应
的程序在一个新进程中运行,运行结束后 shell 继续工作,高级的程序员经常在一个应用程序中
同时启用多个协作的进程以使程序可以并行更多任务、使程序更健壮,或者可以直接利用已有的
其它程序,这样子整个系统中运行的进程就可以相互配合,而不是独立运行,这就涉及到进程间
的通信。
2.查看进程
即使读者刚打开 Linux 电脑,没有运行任何程序,电脑中也会有进程存在,因为系统中必须要有
进程在处理一些必要的程序,以保证系统能正常运行。其实在 Linux 中是通过检查表记录与进程
相关的信息的,进程表就是一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在
一个表中,其中包括进程的 PID 、进程的状态、命令字符串和其他一些 ps 命令输出的各类信息。
操作系统通过进程的 ID 对它们进行管理,这些 PID 是进程表的索引,就目前的 Linux 系统而言,
系统支持可以同时运行的进程数可能只与用于建立进程表项的内存容量有关,而没有具体的数量的限制,也就是说系统有足够的内存的话,那么理论上就可以运行无数个进程。
3.1进程ID
Linux 系统中的每个进程都会被分配一个唯一的数字编号,我们称之为进程 ID ProcessID ,通
常也被简称为 PID )。进程 ID 是一个 16 位的正整数,默认取值范围是从 2 32768 (可以修改), 由 Linux 在启动新进程的时候自动依次分配,当进程被启动时,系统将按顺序选择下一个未被使 用的数字作为它的 PID ,当 PID 的数值达到最大时,系统将重新选择下一个未使用的数值,新的 PID 重新从 2 开始,这是因为 PID 数字为 1 的值一般是为特殊进程 init 保留,即系统在运行时就 存在的第一个进程,init 进程负责管理其他进程。
3.2父进程ID
任何进程(除 init 进程)都是由另一个进程启动,该进程称为被启动进程的父进程,被启动的进程称为子进程,父进程号无法在用户层修改。父进程的进程号(PID )即为子进程的父进程(PPID ).用户可以通过调用 getppid() 函数来获得当前进程的父进程号。
为了更直观看到这些进程,我们立即使用 ps 命令去查看系统中的进程情况, ps 命令可以显示我
们正在运行的进程、其他用户正在运行的进程或者目前在系统上运行的所有进程。
在Linux下执行以下命令:
ps -aux
19.Linux 进程_第1张图片
输出结果如上图所示,可以很明显看到,编号为 1 的进程是 init 进程。它位于 /sbin/init
录中。

3.3父进程与子进程

进程启动时,启动进程是新进程的父进程,新进程是启动进程的子进程

每个进程都有一个父进程(除了系统中如“僵尸进程”这种特殊进程外),因此,读者可以把 Linux
中的进程结构想象成一个树状结构,其中 init 进程就是树的“根”;或者可以把 init 进程看作为
操作系统的进程管理器,它是其他所有进程的祖先进程。我们将要看到的其他系统进程要么是由
init 进程启动的,要么是由被 init 进程启动的其他进程启动的。
总的来说 init 进程下有很多子进程,这些子进程又可能存在子进程,就像家族一样。系统中所有
的父进程 ID 被称为 PPID ,不同进程的父进程是不同的,这个值只是当前进程的父进程的 ID ,系
统中的父进程与子进程是相对而言的,就好比 爷爷 <-> 爸爸 <-> 儿子 之间的关系,爸爸相对于
爷爷而言是儿子,相对于儿子而言则是爸爸。
为了更加直观看出系统中父进程与子进程,此处我们使用 pstree命令将进程以树状关系列出来,在Linux上执行以下命令:
19.Linux 进程_第2张图片

4.程序与进程

程序的概念

程序是一个普通文件,是为了完成特定任务而准备好的指令序列与数据的集合,这些指令和数据中以“可执行映像”的格式保存在磁盘中。正如我们所写的一些代码,经过编译器编译后,就会生成对应的可执行文件,那么这个就是程序,或者称之可执行程序。

进程的概念

进程

进程(process)则是程序执行的具体实例,比如一个可执行文件,在执行的时候,它就是一个进
程,直到该程序执行完毕。那么在程序执行的过程中,它享有系统的资源,至少包括进程的运行
环境、 CPU 、外设、内存、进程 ID 等资源与信息,同样的一个程序,可以实例化为多个进程,在
Linux 系统下使用 ps 命令可以查看到当前正在执行的进程,当这个可执行程序运行完毕后,进程
也会随之被销毁(可能不是立即销毁,但是总会被销毁)。

程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行,这种执行的
程序称之为进程,也就是说进程是系统进行资源分配和调度的一个独立单位,每个进程都有自己
单独的地址空间。

举个例子,我们可以看到 /bin 目录下有很多可执行文件,如下图所示,我们在系统中打开一个终
端就是一个进程,这个进程由 bash 可执行文件(程序)实例化而来,而一个 Linux 系统可以打开
多个终端,并且这些终端是独立运行在系统中的。

程序变成进程

Linux 系统中,程序只是个静态的文件,而进程是一个动态的实体,进程的状态(后续讲解进
程状态)会在运行过程中改变,那么问题来了,程序到底是如何变成一个进程的呢?
其实正如我们运行一个程序(可执行文件),通常在 Shell 中输入命令运行就可以了,在这运行的
过程中包含了程序到进程转换的过程,整个转换过程主要包含以下 3 个步骤:
1. 查找命令对应程序文件的位置。
2. 使用 fork() 函数为启动一个新进程。
3. 在新进程中调用 exec 族函数装载程序文件,并执行程序文件中的 main() 函数。

5.总结

总的来说,程序与进程有以下的关系:
1. 程序只是一系列指令序列与数据的集合,它本身没有任何运行的含义,它只是一个静态的
实体。而进程则不同,它是程序在某个数据集上的执行过程,它是一个动态运行的实体,有
自己的生命周期,它因启动而产生,因调度而运行,因等待资源或事件而被处于等待状态,
因完成任务而被销毁。
2. 进程和程序并不是一一对应的,一个程序执行在不同的数据集上运行就会成为不同的进程,
可以用进程控制块来唯一地标识系统中的每个进程。而这一点正是程序无法做到的,由于
程序没有和数据产生直接的联系,既使是执行不同的数据的程序,他们的指令的集合依然
是一样的,所以无法唯一地标识出这些运行于不同数据集上的程序。一般来说,一个进程
肯定有一个与之对应的程序,而且有且只有一个。而一个程序有可能没有与之对应的进程
(因为这个程序没有被运行),也有可能有多个进程与之对应(这个程序可能运行在多个不
同的数据集上)。
3. 进程具有并发性而程序没有。
4. 进程是竞争计算机资源的基本单位,而程序不是。

6.进程状态

在学习进程状态之前,先带领大家看看系统中常见的进程状态,可以通过 ps 命令将系统中运行
的进程信息打印出来,我们只需要关注 STAT 那一列的信息即可,进程的状态非常多种,具体如
下图:

在linux上输入以下命令:

ps -ux
19.Linux 进程_第3张图片

 从上图中可以看到进程的状态有比较多种,有些是 S,有些是 Ss,还有些是 SlRlR+ 等状态,具体是什么含义呢?

19.Linux 进程_第4张图片

7.进程状态转换

从前文的介绍我们也知道,进程是动态的活动的实例,这其实指的是进程会有很多种运行状态,
一会儿睡眠、一会儿暂停、一会儿又继续执行。虽然 Linux 操作系统是一个多用户多任务的操作
系统,但对于单核的 CPU 系统来说,在某一时刻,只能有一个进程处于运行状态(此处的运行
状态指的是占用 CPU ),其他进程都处于其他状态,等待系统资源,各任务根据调度算法在这些
状态之间不停地切换。但由于 CPU 处理速率较快,使用户感觉每个进程都是同时运行
下图展示了 Linux 进程从被启动到退出的全部状态,以及这些状态发生转换时的条件。
1. 一般来说,一个进程的开始都是从其父进程调用 fork() 开始的,所以在系统一上电运行的
时候,init 进程就开始工作,在系统运行过程中,会不断启动新的进程,这些进程要么是由
init 进程启动的,要么是由被 init 进程启动的其他进程所启动的。
2. 一个进程被启动后,都是处于可运行状态(但是此时进程并未占用 CPU 运行)。处于该状
态的进程可以是正在进程等待队列中排队,也可以占用 CPU 正在运行,我们习惯上称前者
为“就绪态”,称后者为“运行态”(占用 CPU 运行)。
3. 当系统产生进程调度的时候,处于就绪态的进程可以占用 CPU 的使用权,此时进程就是处
于运行态。但每个进程运行时间都是有限的,比如 10 毫秒,这段时间被称为“时间片”。当
进程的时间片已经耗光了的情况下,如果进程还没有结束运行,那么会被系统重新放入等
待队列中等待,此时进程又转变为就绪状态,等待下一次进程的调度。另外,正处于“运
行态”的进程即使时间片没有耗光,也可能被别的更高优先级的进程“抢占”,被迫重新回
到等到队列中等待。
4. 处于“运行态”的进程可能会等待某些事件、信号或者资源而进入“可中断睡眠态”,比如
进程要读取一个管道文件数据而管道为空,或者进程要获得一个锁资源而当前锁不可获取,
甚至是进程自己调用 sleep() 来强制将自己进入睡眠,这些情况下进程的状态都会变成“可
中断睡眠态”。顾名思义,“可中断睡眠态”就是可以被中断的,能响应信号,在特定条件
发生后,进程状态就会转变为“就绪态”,比如其他进程想管道文件写入数据后,或者锁资
源可以被获取,或者是睡眠时间到达等情况。
5. 当然,处于“运行态”的进程还可能会进入“不可中断睡眠态”,在这种状态下的进程不能
响应信号,但是这种状态非常短暂,我们几乎无法通过 ps 命令将其显示出来,一般处于这
种状态的进程都是在等待输入或输出(I/O)完成,在等待完成后自动进入“就绪态”。

19.Linux 进程_第5张图片

6. 当进程收到 SIGSTOP 或者 SIGTSTP 中的其中一个信号时,进程状态会被置为“暂停态”,
该状态下的进程不再参与调度,但系统资源不会被释放,直到收到 SIGCONT 信号后被重新
置为就绪态。当进程被追踪时(典型情况是使用调试器调试应用程序的情况),收到任何信
号状态都会被置为 TASK_TRACED 状态,该状态跟暂停态是一样的,一直要等到 SIGCONT
信号后进程才会重新参与系统进程调度。
7. 进程在完成任务后会退出,那么此时进程状态就变为退出状态,这是正常的退出,比如在
main 函数内 return 或者调用 exit() 函数或者线程调用 pthread_exit() 都是属于正常退出。为
什么要强调正常退出呢?因为进程也会有异常退出,比如进程收到 kill 信号就会被杀死,其
实不管怎么死,最后内核都会调用 do_exit() 函数来使得进程的状态变成“僵尸态(僵尸进
程)”,这里的“僵尸”指的是进程的 PCBProcess Control Block,进程控制块)。为什么一
个进程的死掉之后还要把尸体(PCB)留下?因为进程在退出的时候,系统会将其退出信息
都保存在进程控制块中,比如如果他正常退出,那进程的退出值是什么?如果是被信号杀
死?那么是哪个信号将其杀死?这些“死亡信息”都被一一封存在该进程的 PCB 当中,好
让别人可以清楚地知道:我是怎么死的。那谁会关心他是怎么死的呢?那就是它的父进程,
它的父进程之所以要启动它,很大的原因是要让这个进程去干某一件事情,现在这个孩子
已死,那事情办得如何,因此需要把这些信息保存在进程控制块中,等着父进程去查看这
些信息。
8. 当父进程去处理僵尸进程的时候,会将这个僵尸进程的状态设置为 EXIT_DEAD,即死亡
态(退出态),这样子系统才能去回收僵尸进程的内存空间,否则系统将存在越来越多的僵
尸进程,最后导致系统内存不足而崩溃。那么还有两个问题,假如父进程由于太忙而没能
及时去处理僵尸进程的时候,要如何处理?又假如在子进程变成“僵尸态”之前,它的父
进程已经先它而去了(退出),那么这个子进程变成僵死态由谁处理呢?第一种情况可能不
同的程序员有不同的处理,父进程有别的事情要干,不能随时去处理僵尸进程。在这样的
情形下,可以考虑使用信号异步通知机制,让一个孩子在变成僵尸的时候,给其父进程发
一个信号,父进程接收到这个信号之后,再对其进行处理,在此之前父进程该干嘛就干嘛。
而如果如果一个进程的父进程先退出,那么这个子进程将变成“孤儿进程”(没有父进程),
那么这个进程将会被他的祖先进程收养(adopt),它的祖先进程是 init(该进程是系统第一
个运行的进程,他的 PCB 是从内核的启动镜像文件中直接加载的,系统中的所有其他进程
都是 init 进程的后代)。那么当子进程退出的时候,init 进程将回收这些资源。
8.启动新进程
Linux 中启动一个进程有多种方法,比如可以使用 system() 函数,也可以使用 fork() 函数去启
动(在其他的一些 Linux 书籍也称作创建进程,本书将全部称之为启动进程)一个新的进程,第
一种方法相对简单,但是在使用之前应慎重考虑,因为它效率低下,而且具有不容忽视的安全风
第二种方法相对复杂了很多,但是提供了更好的弹性、效率和安全性。

 8.1 system()进程实验

这个 system() 函数是C 标准库中提供的,它主要是提供了一种调用其它程序的简单方法。读者可
利用 system() 函数调用一些应用程序,它产生的结果与从 shell 中执行这个程序基本相似。事
实上, system() 启动了一个运行着/bin/sh 的子进程,然后将命令交由它执行,其中/bin/sh 是一个 shell 的一种。

下面对system函数做一个简单的介绍: 

头文件 

 #include 

定义函数 

 int system(const char * string); 

函数说明 

 system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命>令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。

返回值 =-1:出现错误,

返回值 =0:调用成功但是没有出现子进程

返回值>0:成功退出的子进程的id

如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值>。如果system()调用成功则最后会返回执行shell命令后的返回值,但是此返回值也有可能为 system()调用/bin/sh失败所返回的127,因此最好能再检查errno 来确认执行成功。 

因为 system() 函数使用 shell 调用命令,它受到系统 shell 自身的功能特性和安全缺陷的限制,因此,并不推荐使用这种方法去启动一个进程
举个例子:
#include 
#include 
#include 
#include 

int main(void)
{
        pid_t result;

        printf("This is a system demo!\n\n");
/*调用system函数*/
        result=system("ls -l");

        printf("Done!\n\n");

        return result;

}

19.Linux 进程_第6张图片

从程序运行的结果可以看到,只有当 system() 函数运行完毕之后,才会输出 Done ,这是因为程序 从上往下执行,而无法直接返回结果。虽然 system() 函数很有用,但它也有局限性,因为程序必须等待由 system() 函数启动的进程结束之后才能继续,因此我们不能立刻执行其他任务。
当然,你也可以让“ ls -l ”命令在后台运行,只需在命令结束位置加上” & 即可,具体命令如下:
ls -l &
如果在 system() 函数中使用这个命令,它也是可以在后台中运行的,那么 system() 函数的调用将
shell 命令结束后立刻返回。由于它是一个在后台运行程序的请求,所以 ls 程序一启动 shell
返回了,修改 system.c 源码 :
#include 
#include 
#include 
#include 

int main(void)
{
	pid_t result;

	printf("This is a system demo!\n\n");

	result=system("ls -l &");

	printf("Done!\n\n");

	return result;

}
19.Linux 进程_第7张图片

 可以看出来,在 ls 命令还未来得及打印出它的所有输出结果之前,system() 函数就程序就打印出 字符串 Done 然后退出了。system() 程序退出后,ls 命令继续完成它的输出。这类的处理行为往往会给用户带来很大的困惑,也不一定如用户所预料的结果一致,因此如果读者想要让进程按 照自己的意愿执行,就需要能够对它们的行为做更细致的控制,接下来讲解其他方式启动新的进 程。

9.fork()进程实验
在前面的文章我们也了解到, init 进程可以启动一个子进程,它通过 fork() 函数从原程序中创建
一个完全分离的子进程,当然,这只是 init 进程启动子进程的第一步,后续还有其他操作的。不
管怎么说, fork() 函数的基础功能就是启动一个子进程,其示意图具体见 fork 示意图
19.Linux 进程_第8张图片

父进程中的 fork() 调用后返回的是新的子进程的 PID 。新进程将继续执行,就像原进程一样,
不同之处在于,子进程中的 fork() 函数调用后返回的是 0 ,父子进程可以通过返回的值来判断究
竟谁是父进程,谁是子进程。
fork() 函数用于从一个已存在的进程中启动一个新进程,新进程称为子进程,而原进程称为父进
程。使用 fork() 函数的本质是将父进程的内容复制一份,正如细胞分裂一样,得到的是几乎两个
完全一样的细胞,因此这个启动的子进程基本上是父进程的一个复制品,但子进程与父进程有不
一样的地方,它们的联系与区别简单列举如下
子进程与父进程一致的内容:
进程的地址空间。
进程上下文、代码段。
进程堆空间、栈空间,内存信息。
进程的环境变量。
标准 IO 的缓冲区。
打开的文件描述符。
信号响应函数。
当前工作路径。
子进程独有的内容:
进程号 PIDPID 是身份证号码,是进程的唯一标识符。
记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
挂起的信号。这些信号是已经响应但尚未处理的信号,也就是”悬挂”的信号,子进程也
不会继承这些信号。
因为子进程几乎是父进程的完全复制,所以父子两个进程会运行同一个程序但是这种复制有一
个很大的问题,那就是资源与时间都会消耗很大,当发出 fork() 系统调用时,内核原样复制父进
程的整个地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,因为它需要做一些
事情:
为子进程的页表分配页面。
为子进程的页分配页面。
初始化子进程的页表。
把父进程的页复制到子进程相应的页中
创建一个地址空间的这种方法涉及许多内存访问,消耗许多 CPU 周期,并且完全破坏了高速缓
存中的内容,因此直接复制物理内存对系统的开销会产生很大的影响,更重要的是在大多数情况
下,这样直接拷贝通常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,
这样就完全丢弃了所继承的地址空间。因此在 Linux 中引入一种写时复制技术 Copy On Write
简称 COW ),我们知道, Linux 系统中的进程都是使用虚拟内存地址,虚拟地址与真实物理地址
之间是有一个对应关系的,每个进程都有自己的虚拟地址空间,而操作虚拟地址明显比直接操作
物理内存更加简便快捷,那么显而易见的,写时复制是一种可以推迟甚至避免复制数据的技术。
内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间(页面)。
那么写时复制的思想就是在于:父进程和子进程共享页面而不是复制页面。而共享页面就不能被
修改,无论父进程和子进程何时试图向一个共享的页面写入内容时,都会产生一个错误,这时内
核就把这个页复制到一个新的页面中并标记为可写。原来的页面仍然是写保护的,当还有进程试
图写入时,内核检查写进程是否是这个页面的唯一属主,如果是则把这个页面标记为对这个进程
是可写的。
总的来说,写时复制只会用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地
址空间,资源的复制是在需要写入的时候才会进行,在此之前,父进程与子进程都是以只读方式
共享页面,这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。而在绝大多数的时
候共享的页面根本不会被写入,例如,在调用 fork() 函数后立即执行 exec(),地址空间就无需被
复制了,这样一来 fork() 的实际开销就是复制父进程的页表以及给子进程创建一个进程描述符。
理论相关的知识了解至此即可,下面就看看 fork() 函数的使用,它的函数原型如下:
pid_t  fork(void);
fork() 启动新的进程后,子进程与父进程开始并发执行,谁先执行由内核调度算法来决定。 fork() 函数如果成功启动了进程,会对父子进程各返回一次
其中对父进程返回子进程的 PID
对子进程返回 0
如果 fork() 函数启动子进程失败,它将返回-1
失败通常是因为父进程所拥有的子进程数目超过了规定的限制(CHILD_MAX ),此时 errno 将设为 EAGAIN
如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno 变量将被设为 ENOMEM。
本小节提供的例程在 system_programing/fork 目录下, fork.c 中的示例就是使用 fork() 函数启动一
个新进程,并且在进程中打印相关的信息,如在父进程中打印出” In father process!! ”等信息,例
程源码如下:
#include 
#include 
#include 
#include 

int main(void)
{
        pid_t result;

        printf("This is a fork demo!!\n\n");

        result = fork();

        if(result == -1)
        {
                printf("fork error!!\n");
        }

        else if(result == 0)
        {
                printf("The returned value is %d,In child process!! My PID is %d\n\n",result,getpid());
        }
        else
        {
                printf("The returned value is %d,In father process!! My PID is %d\n\n",result,getpid());
        }
        return result;
}
~         

我们来分析一下这段代码:

首先在第 12 行的时候调用了 fork 函数,调用 fork 函数后系统就会启动一个子进程,并且
子进程与父进程执行的内容是一样的(代码段),可以通过返回值 result 判断 fork() 函数的
执行结果。
如果 result 的值为-1,那代表着 fork() 函数执行出错,出错的原因在前文也提到,在此具体
不细说。
如果返回的值为 0 ,则表示此时执行的代码是子进程,那么就打印返回的结果、“ In child
process!! ”与子进程的 PID ,进程的 PID 通过 getpid() 函数获取得到。
如果返回的值大于 0,则表示此时执行的代码是父进程,同样也打印出返回的结果、”In
father process!!”与父进程的 PID
19.Linux 进程_第9张图片
细心的同学就会发现,在这个实验现象中,父进程的返回值就是子进程的 PID ,而子进程的返回
值则是 0 。而且子进程并不会再次执行 fork() 函数之前的内容,而 fork() 函数之后的内容在父进
程和子进程都会执行一遍.
10.exec系列函数进程实验
事实上,使用 fork() 函数启动一个子进程是并没有太大作用的,因为子进程跟父进程都是一样的,
子进程能干的活父进程也一样能干,因此世界各地的开发者就想方设法让子进程做不一样的事
情,于是诞生了 exec 系列函数。
exec系列函数主要是用于替换进程的执行程序,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换。另外,这里的可执行文件既可以是二进制文件,也可以是 Linux 下任何可执行脚本文件。
简单来说就是覆盖进程,举个例子,A 进程通过exec 系列函数启动一个进程 B 此时进程 B 会替换进程 A 进程 A 的内存空间、数据段、代码段等内容都将被进程 B 占用然后进程 A 将不复存在.
10.1exec系列函数的实验分析
下面直接通过execl()实验进行详解:
int execl( const char *path,const char *arg,...)
execl() 函数用于执行参数 path 字符串所代表的文件路径(必须指定路径)
接下来是一系列可变参数,它们代表执行该文件时传递过去的 argv[0]argv[1] argv[n]
最后一个参数必须用空指针 NULL 作为结束的标志。
execl函数实例:
#include 
#include 

int main(void)
{
        int err;

        printf("this is a execl function test demo!\n\n");

        err = execl("/bin/ls","ls","-la",NULL);

        if(err < 0){
                printf("execl fail!\n\n");
        }

        printf("Done!\n\n");

}
代码中通过 execl() 函数的参数列表调用了 ls 命令程序,然后将第二个以后的参数当做该文件的
argv[0] argv[1] argv[n] ,最后一个参数必须用空指针 NULL 作为结束的标志。它其实
就是与我们在终端上运行” ls -la ”产生的结果是一样的。

程序先打印出它的第一条消息”this is a execl function test demo!接着调用 exec 系列函数(实验中使用 execl() 函数),这个函数在/bin/ls 目录中搜索程序 ls,然后它将会替exec_demo 本身的进程,程序运行结果与在终端中使用以下所示的 shell 命令一样,如下图。

19.Linux 进程_第10张图片

提示 : exec 系列函数是直接将当前进程给替换掉的,当调用exec系列函数后,当前进程将不会
再继续执行,所以示例程序中的“Done! ”将不被输出,因为当前进程已经被替换了,一般情况
下,exec 系列函数是不会返回的,除非发生了错误
出现错误时,exec 系列函数将返回-1,并且会设置错误变量 errno

因此我们可以通过调用 fork() 复制启动一个子进程,并且在子进程中调用 exec 系列函数替换子
进程,这样把 fork() exec 系列函数结合在一起使用就是创建一个新进程所需要的一切了。

exec 族的其它函数
exec 族实际包含有 6 个不同的 exec 函数,它们功能一样,主要是传参的形式不同,函数原型分
别如下:
int execl(const char *path, const char *arg, ...)
int execlp(const char *file, const char *arg, ...)
不用在写文件路径了
#include 
#include 

int main(void)
{
        pid_t result;
        result = fork();
        if(result > 0)
        {
                execlp("ls","ls","-l",NULL);
                printf("error!!\r\n");
                return -1;
        }
        return 0;
}
int execle(const char *path, const char *arg, ..., char *const envp[])
int execv(const char *path, char *const argv[])
传进来的只能是一个字符串的数组
#include 
#include 

int main(void)
{
        pid_t result;
        char *arg[]={"ls","-l",NULL};
        result = fork();
        if(result > 0)
        {
                execv("/bin/ls",arg);
                printf("error!!\r\n");
                return -1;
        }
        return 0;
}

int execvp(const char *file, char *const argv[])
int execve(const char *path, char *const argv[], char *const envp[])
#include 
#include 

int main(void)
{
        pid_t result;
        char *arg[]={"env",NULL};
        char *env[]={"PATH=/tmp","name=lzf",NULL};
        result = fork();
        if(result > 0)
        {
                execve("/usr/bin/env",arg,env);
                printf("error!!\r\n");
                return -1;
        }
        return 0;
}

 ①.l后缀和v后缀两者必须选其一来使用

int execl(const char *path, const char *arg, ...)
int execv(const char *path, char *const argv[])

这些函数可以分为两大类
execlexeclp execle 传递给子程序的参数个数是可变的,如“ ls -la ”示例中,“-la ”为子序“ ls ”的参数。
execvexecvp execve 通过数组去装载子程序的参数。
无论那种形式,参数都以一个空指针 NULL 结束。
总结来说,可以通过它们的后缀来区分他们的作用:
名称包含 l (list)字母的函数(execlexeclp execle)接收参数列表“list”作为调用程序的参数。
名称包含 p (path)字母的函数(execvp execlp)可接受一个程序名作为参数它会在当前的执行
路径和环境变量“PATH”中搜索并执行这个程序(即可使用相对路径)
名字不包含 p 母的函数在调用时必须指定程序的完整路径(即要求绝对路径)。
名称包含 v 字母的函数(execv、execvp 和 execve)的子程序参数通过一个数组“vector”装
载。以数组的形式传参
名称包含 e 字母的函数(execve execle)比其它函数多接收一个指明环境变量列表的参
并且可以通过参数 envp 传递字符串数组作为新程序的环境变量  这个 envp 参数的格式
应为一个以 NULL 指针作为结束标记的字符串数组  ,每个字符串应该表示为“environment
= virables”的形式。
关于这些函数的用法,可直接查看示例代码的 exec.c 文件进行实验,此处不再展开。
11.终止进程(进程的退出)
Linux 系统中,进程终止(或者称为进程退出,为了统一,下文均使用“终止”一词)的常见
方式有 5 种,可以分为正常终止异常终止
正常终止
main 函数返回。
调用 exit() 函数终止。
调用 _exit() 函数终止。
exit和_exit退出函数
头文件:
#include
#include
原型:
void _exit(int status);
void exit(int status);
#include 
#include 
#include 

int main()
{
        pid_t result;
        result = fork();

        if(result == -1)
        {
                printf("error\n\n");
        }
        else if(result == 0)
        {
                printf("son");
                _exit(0);
        }
        else
        {
                printf("father");
                exit(0);
        }
}

输出结果:father

返回值:
不返回
异常终止
调用 abort() 函数异常终止。
由系统信号终止。
Linux 系统中, exit() 函数定义在 stdlib.h 中,而 _exit() 定义在 unistd.h 中,exit() _exit() 函数都是用来终止进程的当程序执行到 exit() _exit() 函数时,进程会无条件地停止剩下的所有操作,清除包括 PCB 在内的各种数据结构,并终止当前进程的运行。不过这两个函数还是有区别的,具体下图所示。
19.Linux 进程_第11张图片

 从图中可以看出,_exit() 函数的作用最为简单:直接通过系统调用使进程终止运行。当然,在终 止进程的时候会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构

而 exit() 函 数则在这些基础上做了一些包装,在执行退出之前加了若干道工序:比如 exit() 函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,这就是“清除 I/O 缓冲”。

由于在 Linux 的标准函数库中,有一种被称作“缓冲 I/Obuffered I/O)”操作,其特征就是对应
每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在
下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存
中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容
一次性写入文件。
这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。比如有些数据,程序认为 
已经被写入文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用
_exit() 函数直接将进程关闭,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,就一定
要使用 exit() 函数。
不管是那种退出方式,系统最终都会执行内核中的同一代码,这段代码用来关闭进程所用已打开
的文件描述符,释放它所占用的内存和其他资源。
下面一起看看 _exit() 与 exit() 函数的使用方法:
#include
#include
函数原型:
void _exit(int status);
void exit(int status);
这两个函数都会传入一个参数 status 这个参数表示的是进程终止时的状态码.
0 表示正常终止
其他非 0 值表示异常终止,一般都可以使用-1 或者 1 表示 能父进程希望知道一个子进程何时结束,或者想要知道子进程结束的状
态,甚至是等待着子进程结束,那么我们可以通过在父进程中调用 wait() 或者 waitpid() 函数让父
进程等待子进程的结束。
从前面的文章我们也了解到,当一个进程调用了 exit() 之后,该进程并不会立刻完全消失,而是
变成了一个僵尸进程 僵尸进程是一种非常特殊的进程,它已经放弃了几乎所有的内存空间,没
有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等
信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间 。那么无论如何,父进程都要
回收这个僵尸进程,因此调用 wait() 或者 waitpid() 函数其实就是将这些僵尸进程回收,释放僵尸
进程占有的内存空间,并且了解一下进程终止的状态信息。
wait函数
wait() 函数原型如下:
pid_t wait(int *wstatus);
wait()函数在被调用的时候,系统将暂停父进程的执行,直到有信号来到或子进程结束,如果在
调用wait()函数时子进程已经结束,则会立即返回子进程结束状态值。子进程的结束状态信息
会由参数 wstatus 返回,与此同时该函数会返子进程的 PID,它通常是已经结束运行的子进程的
PID。状态信息允许父进程了解子进程的退出状态,如果不在意子进程的结束状态信息,则参数
wstatus可以设成NULL
#include 
#include 
#include 
#include 
#include 

int main()
{
        pid_t result;
        int status;
        result = fork();

        if(result == -1)
                printf("error!!\r\n");
        if(result == 0)
        {
                printf("son!!\r\n");
                exit(77);
        }
        else
        {
                wait(&status);
                if(WIFEXITED(status) == 1)
                        printf("exit value:%d\r\n",WEXITSTATUS(status));

                return 0;
        }
}

wait() 函数有几点需要注意的地方:
• wait() 要与 fork() 配套出现
如果在使用fork()之前调用 wait()wait() 的返回值则为-1
常情况下 wait() 的返回值为子进程的 PID
参数 wstatus 用来保存被收集进程退出时的一些状态,它是一个指向 int 类型的指针,但如
果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数
情况下,我们都会这样做),我们就可以设定这个参数为 NULL
当然,Linux系统提供了关于等待子进程退出状态的一些宏定义,我们可以使用这些宏定义来直
接判断子进程退出的状态:
WIFEXITED(status) :如果子进程正常结束,返回一个非零值
• WEXITSTATUS(status):如果 WIFEXITED 非零,返回子进程退出码
• WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值
• WTERMSIG(status) :如果 WIFSIGNALED非零,返回信号代码
• WIFSTOPPED(status):如果子进程被暂停,返回一个非零值
WSTOPSIG(status):如果WIFSTOPPED非零,返回一个信号代码
实验分析
wait() 函数使用实例如下:
#include 
#include 
#include 
#include 
#include 

int main()
{

        pid_t pid, chil_pid;
        int status;

        pid = fork();

        if(pid < 0){
                printf("Error fork\n");
        }

        else if (pid == 0){

                printf("I am a child process!my pid is %d!\n\n",getpid());

                sleep(3);

                printf("I am about to quit the process!\n\n");

                exit(0);
        }       
        
        else{
        
                child_pid = wait(&status);
        
                if(child_pid == pid){
                        printf("Get exit child process id:%d\n",child_pid);
                        printf("Get child exit status:%d\n\n",status);
                }else {
                        printf("Some error occured.\n\n");
                }
                exit(0);
        }
}       
我们来分析一下这段代码:
(1) :首先调用 fork() 函数启动一个子进程。
(2) :如果 fork() 函数返回的值 pid 0 ,则表示此时运行的是子进程,那么就让子进程输出
一段信息,并且休眠 3 秒。
(3) :休眠结束后调用 exit() 函数退出,退出状态为 0 ,表示子进程正常退出。
(4) :如果 fork() 函数返回的值 pid 不为 0 ,则表示此时运行的是父进程,那么在父进程中调
wait(&status) 函数等待子进程的退出,子进程的退出状态将保存在 status 变量中。
(5) :若发现子进程退出(通过 wait() 函数返回的子进程 pid 判断),则打印出相应信息,如
子进程的 pid status
19.Linux 进程_第12张图片

waitpid()函数
waitpid()函数的作用和wait()函数一样,但它并不一定要等待第一个终止的子进程,它还有其他选项,比如指定等待某个 pid 的子进程、提供一个非阻塞版本的 wait() 功能等。实际上 wait() 函
数只是 waitpid() 函数的一个特例,在 Linux 内部实现 wait 函数时直接调用的就是 waitpid 函数。
函数原型
pid_t waitpid(pid_t pid, int *wstatus,int options);
waitpid() 函数的参数有 3 ,下面就简单介绍这些参数相关的选项:
• pid:参数 pid 为要等待的子进程 ID,其具体含义如下:
pid < -1:等待进程组号为 pid 绝对值的任何子进程。
pid = -1:等待任何子进程,此时的 waitpid() 函数就等同于 wait() 函数。
pid =0:等待进程组号与目前进程相同的任何子进程,即等待任何与调用 waitpid() 数的进程在同一个进程组的进程。
pid > 0:等待指定进程号为 pid 的子进程。
wstatus :与 wait() 函数一样。
options :参数 options 提供了一些另外的选项来控制 waitpid() 函数的行为。如果不想使用这
些选项,则可以把这个参数设为 0
WNOHANG:如果 pid 指定的子进程没有终止运行,则 waitpid() 函数立即返回 0,而
不是阻塞在这个函数上等待;如果子进程已经终止运行,则立即返回该子进程的进程
号与状态信息。
WUNTRACED:如果子进程进入了暂停状态(可能子进程正处于被追踪等情况),则
马上返回。
WCONTINUED:如果子进程恢复通过 SIGCONT 信号运行,也会立即返回(这个不常
用,了解一下即可)。
很显然,当 waitpid() 函数的参数为 (子进程 pid, status,0) 时,waitpid() 函数就完全退化成了 wait() 函数。 wait.c 实例文件中提供了 waitpid 函数的示例,示例的实验现象与 wait() 函数的是类似的。
13.进程的生老病死
应用程序规定的运行时间叫作时间片

进程状态:

- TASK_RUNNING:就绪/运行状态
- TASK_INTERRUPTIBLE:可中断睡眠状态
- TASK_UNINTERRUPTIBLE:不可中断睡眠状态
- TASK_TRACED:调试态
- TASK_STOPPED:暂停状态
- EXIT_ZOMBIE:僵死状态
- EXIT_DEAD:死亡态

 

14.对进程的管理

进程组、会话、终端

进程组

作用:对同等类型的进程进行管理

进程组的诞生

①在shell里面执行一个应用程序,对于大部分进程来说,自己就是进程组的首进程。进程组只有一个进程

②如果进程调用了fork函数,那么父子进程同属一个进程组,父进程为首进程

③在shell中通过管道执行连接起来的应用程序,两个程序同属于一个进程组,第一个程序为进程组的首进程

进程组id:PGID,由首进程的pid来决定的

会话:SID

作用:管理进程组

会话的诞生

调用setsid函数,新建一个会话,应用程序作为会话的第一个进程,称为会话的第一个进程,称为会话首进程。

用户在终端正确登录之后,启动shell时Linux系统会创建一个新的会话,shell进程作为会话首进程

会话id:会话首进程id,SID

前台进程组

shell进程启动时,默认是前台进程组的首进程,前台进程组的首进程会占用会话所关联的终端来运行,shell启动其他应用程序时,其他应用程序成为首进程。

后台进程组

后台进程中的程序是不会占用终端

在shell进程里启动程序时,加上&符号可以指定程序运行在后台进程组里面

Ctrl+z让前台进程组程序变成后台进程组程序

如何让后台进程组程序变成前台进程组程序

jobs:查看有哪些后台进程组

fg+jobid可以把后台进程组切换为前台进程组

19.Linux 进程_第13张图片

终端 

①物理终端

-串口终端

-LCD终端

②伪终端

-ssh远程联接产生的终端

-桌面系统启动的终端

③虚拟终端

Linux内核自带的

Ctrl+alt+f0~f6可以打开7个虚拟终端

14.守护进程

会话管理前后台进程组

会话关联着一个终端

当终端被关闭了之后,会话中的所有进程都会被关掉

守护进程 

不受终端影响,就算终端退出,也可以继续在后台运行

如何来写一个守护进程

1.创建一个子进程,父进程直接退出

方法:通过fork()函数

2.创建一个新的会话,摆脱终端的影响

方法:通过setsid()函数

3.改变守护进程的当前工作目录,改为“/”

方法:通过chrdir()函数

4.重设文件权限的掩码

新建文件的权限受文件掩码影响

 0002是当前文件权限掩码

002(000 000 010):只写

新建文件的默认执行权限:666(110 110 110)

真正的文件执行权限:666&~umask

重设文件权限的掩码

方法:通过umask()函数

5.关闭不需要的文件描述符

0,1,2:标准输入、输出、出错

方法:通过close()函数关闭文件描述符 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXFILE 3

int main()
{
	pid_t pid;
	int fd,len,i,num;
	char *buf="the dameon is running\n";

	len=strlen(buf)+1;

	pid =fork();	
	if(pid<0){
	printf("fork fail\n");
	}		

	if(pid>0)
		exit(0);
	setsid();

	chdir("/");

	umask(0);

	for(i=0;i

sudo vi /var/log/dameon.log 

普通进程伪装成守护进程

nohup

ps命令详解

aux

axjf

①a:显示一个终端所有的进程

②u:显示进程的归属用户及内存使用情况

③x:显示没有关联控制终端的进程

④j:显示进程归属的进程组id、会话id,父进程id

⑤f:以ASCII形式显示出进程的层次关系

ps aux

①USER:进程的归属用户

②PID:进程的身份证号码

③%CPU:表示进程占用了CPU计算能力的百分比

④%MEM:表示进程占用了系统内存的百分比

⑤VSZ:进程使用的物理内存大小

⑥TTY:表示进程关联的终端

⑦STAT:表示进程当前状态

⑧START:表示进程启动时间

⑨TIME:记录进程的运行时间

⑩COMMAND:表示进程执行的具体程序

ps axjf

①PPID:表示进程为父进程id

②PID:进程的身份证号码

③PGID:进程所在进程组的id

④SID:进程所在会话的id

⑤TTY:表示进程关联的终端

⑥TPGID:表示进程是否是一个守护进程,值为1,表示进程为守护进程

⑦STAT:表示进程当前状态

⑧UID:启动进程的用户id

⑨TIME:记录进程的运行时间

⑩COMMAND:表示进程的层次关系

使用场景

关注进程本身:ps aux

关注进程间的关系:ps axjf

15.僵尸进程和托孤进程

进程的正常退出步骤:

①子进程调用exit()函数退出

②父进程调用wait()函数为子进程处理其他事情

僵尸进程

子进程退出后,父进程没有调用wait()函数处理身后事,子进程变为僵尸进程

#include 
#include 
#include 
#include 

int main()
{
        int pid;

        if((pid = fork())<0 ){

                perror("fail to fork");
                return -1;
        }
        else if(pid == 0){

                printf("child exit now.\n");
                exit(0);
        }
        else{
                while(1);
        }

        return 0;

}

z:表示僵尸态 

托孤进程

父进程比子进程先退出,子进程变为孤儿进程,Linux系统会把子进程托孤给1号进程(init进程)

15.进程间通信(ipc)

进程间通信

①数据传输

②资源共享

③事件通知

④进程控制

Linux系统下的ipc

①早期Unix系统ipc

-管道

-信号

-fifo(数据传输)

②system-v ipc(贝尔实验室)

-system-v消息队列

-system-v信号量

-system-v共享内存

③socket ipc(BSD)

④posix ipc(IEEE)

-posix消息队列

-posix信号量

-posix共享内存

16.无名管道

pipe函数

头文件:

#include

函数原型:

int pipe(int pipefd[2]);

返回值:

成功:0

失败: -1

特点

①特殊文件(没有名字),无法使用open,但是可以使用close。

②只能通过子进程继承文件描述符的形式来使用

③write和read操作可能会阻塞进程

④所有文件描述符被关闭之后,无名管道被销毁

使用步骤

①父进程调用pipe函数,pipe函数就会创建无名管道

②fork子进程

③close无用端口

④write/read读写端口

⑤close读写端口

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_DATA_LEN 256

int main()
{
        pid_t pid;
        int pipe_fd[2];
        int status;
        char buf[MAX_DATA_LEN];
        const char data[] = "Pipe Test Program";
        int real_read,real_write;

        memset((void*)buf, 0,sizeof(buf));
        
        /* 创建管道 */
        if(pipe(pipe_fd) < 0)
        {
                printf("pipe create error\n");
                exit(1);
        }
        /* 创建一子进程 */
        if((pid = fork()) == 0)
        {
        /* 子进程关闭写描述 */
                close(pipe_fd[1]);

        /* 子进程读取管道内容 */
                if((real_real = read(pipe_fd[0],buf,MAX_DATA_LEN))>0)
                {
                        printf("%d bytes read from the pipes is '%s'\n",real_read,buf);
                }
        /* 关闭子进程描述符 */
                close(pipe_fd[0]);

                exit(0);
        }

        else if (pid > 0)
        {
            
         /* 父进程关闭读描述 */       
                close(pipe_fd[0]);

                if((real_write = write(pipe_fd[1],data,strlen(data)))!= -1)
                {
                        printf("Parent write %d bytes : '%s'\n",real_write,data);
                }
         /* 关闭父进程写描述符 */
                close(pipe_fd[1]);

        /* 收集子进程退出信息 */
                wait(&status);

                exit(0);
        }
}

17.有名管道

无名管道只能在父子进程之间进行数据传输

mkfifo函数

头文件: 

#include

#include

函数原型:

int mkfifo(const char *filename,mode_t mode);

返回值:

成功:0

失败:-1

特点

①有文件名,可以使用open函数打开

②任意进程间数据传输

③write和read操作可能会阻塞进程

④write具有“原子性”

使用步骤

①第一个进程mkfifo有名管道

②open有名管道,write/read数据

③close有名管道

④第二个进程open有名管道,read/write数据

⑤close有名管道

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MYFIFO "/tmp/myfifo"
/* 4096 定义在于limits.h中 */
#define MAX_BUFFER_SIZE PIPE_BUF
/* 参数为即将写入的字符串 */
int main(int argc,char *argv[])
{
        char buff[MAX_BUFFER_SIZE];
        int fd;
        int nread;

/* 判断有名管道是否存在,若尚未创建,则以相应的权限创建 */
        if(access(MYFIFO,F_OK) == -1){
                if((mkfifo(MYFIFO,0666) < 0)&& (errno != EEXIST)){
                        printf("Cannot create fifo file\n");
                        exit(1);
                }
        }
        /* 以只读堵塞方式打开有名管道 */
        fd=open(MYFIFO,O_RDONLY);

        if(fd == -1){
                printf("Open file error\n");
                exit(1);
        }

        /* 循环读取有名管道数据 */
        while(1){
                memset(buff,0,sizeof(buff));
                if((nread = read(fd,buff,MAX_BUFFER_SIZE))>0){
                        printf("Read '%s' from FIFO\n",buff);
                }
        }       

        close(fd);
        exit(0);
}

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
/* 有名管道文件名 */
#define MYFIFO "/tmp/myfile"

/*4096 定义在于limits.h中 */
#define MAX_BUFFER_SIZE PIPE_BUF

/* 参数为即将写入的字符串 */
int main(int argc,char *argv[])
{
        int fd;
        char buff[MAX_BUFFER_SIZE];
        int nwrite;

        if(argc <= 1){
                printf("Usage: ./fifo_write string\n");
                exit(1);
        }
        /* 填充命令行第一个参数到buff */
        sscanf(argv[1],"%s",buff);
        
        /* 以只写阻塞方式打开FIFO管道 */
        fd = open(MYFIFO,O_WRONLY);

        if( fd == -1){
                printf("Open fife file error\n");
                exit(1);
        }
        
        if((nwrite = write(fd,buff,MAX_BUFFER_SIZE))>0){
                printf("Write '%s' to FIFO\n",buff);
        }       
        
        close(fd);
        exit(0);
}       

18.信号简介

信号的基本概念

 软件模拟中断,进程接受信号后做出相应响应

19.Linux 进程_第14张图片

Linux下有64种信号的类型 

怎么产生信号?

硬件

-执行非法指令

-访问非法内存

-驱动程序

-...

软件

控制台:

①Ctrl+c:中断信号

②Ctrl+l:退出信号

③Ctrl+z:停止信号

19.Linux 进程_第15张图片

 kill命令

sudo kill -9 3301

 程序调用kill()函数

信号的处理方式:

-忽略:进程当信号从来没有发生过

-捕获:进程会调用相应的处理函数,进行相应的处理

-默认:使用系统默认处理方式来处理信号

19.常用信号分析

 信号名       信号编号            产生原因               默认处理方式         

 SIGHUP         1                    关闭终端                     终止                
 SIGINT           2                      ctrl+c                        终止                
 SIGQUIT        3                      ctrl+\                     终止+转储           
 SIGABRT       6                     abort()                   终止+转储           
 SIGPE            8                   算术错误                     终止                
 SIGKILL         9                   kill -9 pid            终止,不可捕获/忽略 
 SIGUSR1      10                   自定义                       忽略                
 SIGSEGV      11                   段错误                  终止+转储           
 SIGUSR2      12                    定义                          忽略                
 SIGALRM      14                 alarm()                        终止                
 SIGTERM      15                 kill pid                         终止                
 SIGCHLD      17            (子)状态变化                    忽略                
 SIGTOP        19                   ctrl+z                暂停,不可捕获/忽略

20.signal_kill_raise函数

signal函数(在程序里面设置对信号的处理方式)

头文件:

#include

函数原型:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum,sighandler_t handler);

参数:

-signum:要设置的信号

-handler:

①SIG_IGN:忽略

②SIG_DFL:默认

③void (*sighandler_t)(int):自定义

返回值:

成功:上一次设置的handler

失败:SIG_ERR

#include 
#include 
#include 
#include 
#include 
#include 

/* 信号处理函数 */
void signal_handler(int sig)
{
        printf("\nthis signal number is %d\n",sig);

        if(sig == SIGINT){
                printf("I have get SIGINT!\n\n");
                printf("The signal has been restored to the default processing mode!\n\n");
              /* 恢复信号为默认情况 */
                signal(SIGINT,SIG_DFL);
        }

}

int main(void)
{
        printf("\nthis is an alarm test function\n\n");

        /* 设置信号处理的回调函数 */
        signal(SIGINT,signal_handler);

        while(1){
                printf("waiting for the SIGINT signal , please enter \"ctrl+c\"...\n");
                sleep(1);
        }

        exit(0);
}

19.Linux 进程_第16张图片

kill函数

头文件:

#include

#include

原型函数:

int kill(pid_t pid,int sig);

参数:

-pid:进程id

-sig:要发送的信号

返回值:

成功:0

失败:-1

raise函数(和kill函数很类似)

头文件:

#include

原型函数:

int raise(int sig);

参数:

sig:发送信号

返回值:

成功:0

失败:非0

#include 
#include 
#include 
#include 
#include 
#include 

int main(void)
{
        pid_t pid;

        int ret;

        if((pid = fork()) < 0){
                printf("Fork error\n");
                exit(1);
        }

        if(pid == 0){

                printf("Child is waiting for SIGSTOP signal!\n\n");

                raise(SIGSTOP);

                printf("Child won't run here forever!\n");

                exit(0);
        }

        else{

                sleep(3);

                if((ret = kill(pid,SIGKILL)) == 0){
                        printf("Parent kill %d!\n\n",pid);
                }

                wait(NULL);

                printf("parent exit!\n");

                exit(0);

        }
}

21.信号集处理函数

屏蔽信号集

屏蔽某些信号

-手动

-自动

未处理信号集

信号如果被屏蔽,则记录在未处理信号集中

-非实时信号(1~31),不排队,只留一个

-实时信号(34~64),排队,保留全部

信号集相关API

①int sigemptyset(sigset_t *set);

-将信号集合初始化为0

②int sigfillset(sigset_t *set);

-将信号集合某一位设置为1

你可能感兴趣的:(linux)