Nameless Site

But one day, you will stand before its decrepit gate,without really knowing why.

0%

Linux进程的软中断和管道通信

Linux进程管理命令——进程查看

  • ps命令:报告进程标识、用户、CPU时间消耗及其他属性
    • 命令单独使用可以看到前台执行的进程;后台进程可以使用带参 数的ps命令(如ps -ax)
    • 提供进程的一次性查看,结果不连续
    • 结果数据很精确,但数据量庞大
  • top命令:显示CPU占用率为前几位的进程
    • 动态显示,输出结果连续
    • 消耗较多的系统资源
  • pstree命令:列出当前的进程,以及它们的树状结构
    • 将当前的执行程序以树状结构显示,弥补ps命令的不足
    • 支持指定特定程序(PID)或使用者(USER)作为显示的起始

Linux进程管理命令—进程终止

  • 终止一个进程或终止一个正在运行的程序
    • kill命令:根据PID向进程发送信号,缺省操作是停止进程
    • 如果进程启动了子进程,只终止父进程,子进程运行中将仍 消耗资源成为“僵尸”进程,可用kill -9强制终止退出
    • pkill命令:终止同一进程组内的所有进程。允许指定要终止的进程名称,而非PID
    • Killall命令:与pkill应用方法类似,直接杀死运行中的程 序
    • 数据库服务器的父进程不能用这些命令杀死(容易产生更多 的文件碎片导致数据库崩溃)

Linux进程控制函数——进程创建

fork()

pid=fork();
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。

它不需要参数并返回一个整数值。下面是fork()返回的不同值。

  • 负值:创建子进程失败。
  • 零:返回到新创建的子进程。
  • 正值:返回父母或来电者。该值包含新创建的子进程的进程ID

头文件:

1
2
#include<unistd.h>/*#包含<unistd.h>*/
#include<sys/types.h>/*#包含<sys/types.h>*/

函数原型:
pid_t fork( void);
(pid_t 是一个宏定义,其实质是int 被定义在#include中)
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1

函数说明
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。

为什么fork会返回两次?
由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0;
(3)如果出现错误,fork返回一个负值。
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
引用一位网友的话来解释fork函数返回的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0.
调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回,箭头表示各自的执行处。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。

exec()

函数族exec() :启动另外的进程取代当前的进程

  • include

  • extern char **environ;
  • int execl(const char *path, const char *arg, …);
  • int execlp(const char *file, const char *arg, …);
  • int execle(const char *path, const char *arg, const char *envp[]);
  • int execv(const char *path, const char *argv[]);
  • int execve(const char *path, const char *argv[], const char *envp[];
  • int execvp(const char *file, const char *argv[]);

其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

exec族函数的作用

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似”三十六计”中的”金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
现在我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和用户做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化,我们已经知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种”写时拷贝(copy-on-write)”技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。
对于新程序的命令行参数和环境表有长度大小的限制,对于linux来讲这个限制是4096个字节。执行了exec函数的进程不改变以下进程特征:

  • 1.进程ID和父进程ID
  • 2.实际用户ID和实际组ID
  • 3.进程组ID和附加组ID
  • 4.控制终端
  • 5.会话ID
  • 6.时钟预留着时间
  • 7.当前工作目录和根目录
  • 8.文件创建屏蔽字和文件锁
  • 9.信号屏蔽字和未处理信号集
  • 10.资源限制

返回值
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。

注意
大家在平时的编程中,如果用到了exec函数族,一定记得要加错误判断语句。因为与其他系统调用比起来,exec很容易受伤,被执行文件的位置,权限等很多因素都能导致该调用的失败。最常见的错误是:

  • 1.找不到文件或路径,此时errno被设置为ENOENT;
  • 2.数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;
  • 3.没有对要执行文件的运行权限,此时errno被设置为EACCES。
  • l表示以参数列表的形式调用
  • v表示以参数数组的方式调用
  • e表示可传递环境变量
  • p表示PATH中搜索执行的文件,如果给出的不是绝对路径就会去PATH搜索相应名字的文件,如PATH没有设置, 则会默认在/bin,/usr/bin下搜索。
  • 另:调用时参数必须以NULL结束。原进程打开的文件描述符是不会在exec中关闭的,除非用fcntl设置它们的“执行时关闭标志(close on exec)”而原进程打开的目录流都将在新进程中关闭。

Linux进程属性操作

  • 设置进程属性
    • nice():改变进程执行的优先级
    • setpgid():将指定进程的组进程设为指定的组识别码
    • setpgrp():将目前进程的组进程识别码设为目前进程的进程 识别码,等价于setpgid(0,0)
    • setpriority():设置进程、进程组和用户的执行优先权
  • 获取进程属性
    • getpid():获取目前进程的进程标识
    • getpgid():获得参数pid指定进程所属的组识别码
    • getpgrp():获得目前进程所属的组识别号,等价于
    • getpgid(0)
    • getpriotity():获得进程、进程组和用户的执行优先权

进程退出

  • 正常退出:在main()函数中执行return、调用exit()函数 或_exit()函数
  • 异常退出:调用abort()函数、进程收到信号而终止
  • 区别
    • exit是一个函数,有参数,把控制权交给系统
    • return是函数执行完后的返回,将控制权交给调用函数
    • exit是正常终止进程,abort是异常终止
    • exit中参数为0代表进程正常终止,为其他值表示程序执行过程 中有错误发生
    • exit()在头文件stdlib.h中声明,先执行清除操作,再将控制权 返回给内核
    • _exit()在头文件unistd.h中声明,执行后立即返回给内核

等待进程终止

wait(); waitpid();

  • ① wait() 语法格式: pid=wait(stat_addr);
    wait()函数使父进程暂停执行,直到它的一个子进程结束为止,该函数 的返回值是终止运行的子进程的PID。参数status所指向的变量存放子 进程的退出码,即从子进程的main函数返回的值或子进程中exit()函数 的参数。如果status不是一个空指针,状态信息将被写入它指向的变量。
  • ② waitpid() 语法格式:waitpid(pid_t pid,int * status,int options)
    用来等待子进程的结束,但它用于等待某个特定进程结束。
    参数pid指明要等待的子进程的PID,参数status的含义与wait()函数中的 status相同。

    如果在调用 waitpid()时子进程已经结束,则 waitpid()会立即返回子进程结束状态值。 子进程的结束状态值会由参数 status 返回,而子进程的进程识别码也会一起返回。如果不在意结束状态值,则参数 status 可以设成 NULL。参数 pid 为欲等待的子进程识别码.
    其他数值意义如下:

    • pid<-1 等待进程组识别码为 pid 绝对值的任何子进程。
    • pid=-1 等待任何子进程,相当于 wait()。
    • pid=0 等待进程组识别码与目前进程相同的任何子进程。
    • pid>0 等待任何子进程识别码为 pid 的子进程。
    • 参数options提供了一些额外的选项来控制waitpid,参数 option 可以为 0 或可以用”|”运算符把它们连接起来使用,比如:ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
      WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
      WUNTRACED 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。WIFSTOPPED(status)宏确定返回值是否对应与一个暂停子进程。

      子进程的结束状态返回后存于 status,底下有几个宏可判别结束情况:
      WIFEXITED(status)如果若为正常结束子进程返回的状态,则为真;对于这种情况可执行WEXITSTATUS(status),取子进程传给exit或_eixt的低8位。
      WEXITSTATUS(status)取得子进程 exit()返回的结束代码,一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
      WIFSIGNALED(status)若为异常结束子进程返回的状态,则为真;对于这种情况可执行WTERMSIG(status),取使子进程结束的信号编号。
      WTERMSIG(status) 取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED 来判断后才使用此宏。
      WIFSTOPPED(status) 若为当前暂停子进程返回的状态,则为真;对于这种情况可执行WSTOPSIG(status),取使子进程暂停的信号编号。
      WSTOPSIG(status) 取得引发子进程暂停的信号代码,一般会先用 WIFSTOPPED 来判断后才使用此宏。
      如果执行成功则返回子进程识别码(PID) ,如果有错误发生则返回
      返回值-1。失败原因存于 errno 中。

进程的软中断通信

signal()

表头文件#include

功能:设置某一信号的对应动作

函数原型:void (*signal(int signum,void(* handler)(int)))(int);
或者:typedef void (*sig_t)( int );
sig_t signal(int signum,sig_t handler);

参数说明
第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler描述了与信号关联的动作,它可以取以下三种值:

  • (1)一个无返回值的函数地址
    此函数必须在signal()被调用前申明,handler中为这个函数的名字。当接收到一个类型为signum的信号时,就执行handler 所指定的函数。这个函数应有如下形式的定义:
    void func(int sig);
  • (2)SIG_IGN
    这个符号表示忽略该信号,执行了相应的signal()调用后,进程会忽略类型为sig的信号。
  • (3)SIG_DFL
    这个符号表示恢复系统对信号的默认处理。

函数说明
signal()会依参数signum 指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。

返回值:返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。

下面的情况可以产生Signal:

  • 按下CTRL+C产生SIGINT
  • 硬件中断,如除0,非法内存访问(SIGSEV)等等
  • Kill函数可以对进程发送Signal
  • Kill命令。实际上是对Kill函数的一个包装
  • 软件中断。如当Alarm Clock超时(SIGURG),当Reader中止之后又向管道写数据(SIGPIPE),等等

Signals:

Signal Description
SIGABRT 由调用abort函数产生,进程非正常退出
SIGALRM 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
SIGBUS 某种特定的硬件异常,通常由内存访问引起
SIGCANCEL 由Solaris Thread Library内部使用,通常不会使用
SIGCHLD 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略
SIGCONT 当被stop的进程恢复运行的时候,自动发送
SIGEMT 和实现相关的硬件异常
SIGFPE 数学相关的异常,如被0除,浮点溢出,等等
SIGFREEZE Solaris专用,Hiberate或者Suspended时候发送
SIGHUP 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送
SIGILL 非法指令异常
SIGINFO BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程
SIGINT 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程
SIGIO 异步IO事件
SIGIOT 实现相关的硬件异常,一般对应SIGABRT
SIGKILL 无法处理和忽略。中止某个进程
SIGLWP 由Solaris Thread Libray内部使用
SIGPIPE 在reader中止之后写Pipe的时候发送
SIGPOLL 当某个事件发送给Pollable Device的时候发送
SIGPROF Setitimer指定的Profiling Interval Timer所产生
SIGPWR 和系统相关。和UPS相关。
SIGQUIT 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程
SIGSEGV 非法内存访问
SIGSTKFLT Linux专用,数学协处理器的栈异常
SIGSTOP 中止进程。无法处理和忽略。
SIGSYS 非法系统调用
SIGTERM 请求中止进程,kill命令缺省发送
SIGTHAW Solaris专用,从Suspend恢复时候发送
SIGTRAP 实现相关的硬件异常。一般是调试异常
SIGTSTP Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程
SIGTTIN 当Background Group的进程尝试读取Terminal的时候发送
SIGTTOU 当Background Group的进程尝试写Terminal的时候发送
SIGURG 当out-of-band data接收的时候可能发送
SIGUSR1 用户自定义signal 1
SIGUSR2 用户自定义signal 2
SIGVTALRM setitimer函数设置的Virtual Interval Timer超时的时候
SIGWAITING Solaris Thread Library内部实现专用
SIGWINCH 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程
SIGXCPU 当CPU时间限制超时的时候
SIGXFSZ 进程超过文件大小限制
SIGXRES Solaris专用,进程超过资源限制的时候发送

注意

  • 不要使用低级的或者STDIO.H的IO函数
  • 不要使用对操作
  • 不要进行系统调用
  • 不是浮点信号的时候不要用longjmp
  • signal函数是由ISO C定义的。因为ISO C不涉及多进程,进程组以及终端I/O等,所以他对信号的定义非常含糊,以至于对UNIX系统而言几乎毫无用处。
  • 备注:因为signal的语义与现实有关,所以最好使用sigaction函数替代本函数

当某个信号出现时,系统有三种处理方式:

  • 忽略信号:大多数信号使用,但SIGKIL和SIGSTOP不能被忽略
  • 捕捉信号:通知内核在某种信号发生时,调用一个用户函数
  • 执行系统默认动作:异常终止(abort)、退出(exit)、忽略(ignore)、停 止(stop)或继续(continue)

功能

  • 发送信号:发送进程把信号送到指定进程信号域的某一位上,如目标进程正在一个可被中断的优先级上睡眠,核心便将其唤醒
  • 预置对信号的处理方式:进程处于核心态时,即使受到软中断也不予理睬;只有当它返回到用户态后,才处理软中断信号
  • 收受信号的进程按事先规定完成对相应事件的处理

进程的软中断通信——函数的使用

  • 向一个进程或一组进程发送一个信号:int kill(pid, sig)
    pid>0时,核心将信号发送给进程pid
    pid<0时,核心将信号发送给与发送进程同组的所有进程
    pid=-1时,核心将信号发送给所有用户标识符真正等于发送进程的有 效用户标识号的进程

  • 预置信号接收后的处理方式:signal(sig, function)
    function=1时,屏蔽该类信号
    function=0时,收到sig信号后终止自己
    function为非0、非1类整数时,执行用户设置的软中断处理程序

Linux进程间通信—管道和有名管道

  • 管道用于具有亲缘关系进程间的通信
    • 管道是半双工的,数据只能单向流动(双方通信需建立两个管道)
    • 管道只能用于父子进程或兄弟进程之间
    • 管道对于管道两端的进程而言就是一个文件,并单独构成一种文件 系统,存在于内存中
    • 写管道的内容添加在管道缓冲区的末尾,读管道则从缓冲区头部读 出
  • 有名管道在普通管道具备功能基础上,通过给管道命名的 方法变成管道文件,允许无亲缘关系进程间通过访问管道 文件进行通信

无名管道的使用

  • int pipefd[2]; int pipe(pipefd); /*创建无名管道*/
    pipefd[0]只能用于读; pipe[1]只能用于写

pipe函数定义中的fd参数是一个大小为2的一个数组类型的指针。该函数成功时返回0,并将一对打开的文件描述符值填入fd参数指向的数组。失败时返回 -1并设置errno。

通过pipe函数创建的这两个文件描述符 fd[0] 和 fd[1] 分别构成管道的两端,往 fd[1] 写入的数据可以从 fd[0] 读出。并且 fd[1] 一端只能进行写操作,fd[0] 一端只能进行读操作,不能反过来使用。要实现双向数据传输,可以使用两个管道。

默认情况下,这一对文件描述符都是阻塞的。此时,如果我们用read系统调用来读取一个空的管道,则read将被阻塞,知道管道内有数据可读;如果我们用write系统调用往一个满的管道中写数据,则write也将被阻塞,直到管道有足够的空闲空间可用(read读取数据后管道中将清除读走的数据)。当然,用户可自行将 fd[0] 和 fd[1] 设置为非阻塞的。

如果管道的写端文件描述符 fd[1] 的引用计数减少至0,即没有任何进程需要往管道中写入数据,则对该管道的读端文件描述符 fd[0] 的read操作将返回0(管道内不存在数据的情况),即读到了文件结束标记(EOF,End Of File);反之,如果管道的读端文件描述符 fd[0] 的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符 fd[1] 的write操作将失败,并引发SIGPIPE信号(往读端被关闭的管道或socket连接中写数据)。

管道内部传输的数据是字节流,这和TCP字节流的概念相同。但它们又存在细微的差别。应用层程序能往一个TCP连接中写入多少字节的数据,取决于对方接受窗口的大小和本端的拥塞窗口的大小。而管道的话本身拥有一个容量限制,它规定如果管道的写端应用程序不将管道中数据读走的话,该管道最多还能被写入多少字节的数据。管道容量的大小默认是65536字节。我们也可以使用fcntl函数来修改管道容量。

  1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。

  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。

  3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

  • 将数据写入管道:write()
    • 函数原型:int write(int handle, void *buf, int nbyte)
    • 管道长度受到限制,管道满时写入操作将被阻塞,直到管道中的 数据被读取
    • fcntl()可将管道设置为非阻塞模式
  • 从管道读取数据:read()
    • 函数原型:ssize_t read (int fd, void *buf, size_t count);
    • 返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0。
    • 参数:参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。
      read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。
    • 当数据被读取后,数据将自动被管道清除
    • 不能由一个进程向多个进程同时传递同一个数据
    • fcntl()可将管道读模式设置为非阻塞模式
  • 关闭管道:close()
    • 函数原型:int close(int fd);
    • 返回值:成功返回0,出错返回-1并设置errno
    • 参数fd是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
    • 关闭读端口时,在管道上进行写操作的进程将收到SIGPIPE信号
    • 关闭写端口时,进行读操作的read()函数将返回0

管道通信的使用—命名管道的创建与读写

  • 创建命名管道:
    1
    2
    int mknod(const char *path, mode_t mod, dev_t dev)
    int mkfifo(const char *path, mode_t mode)
  • 命名管道必须先调用open()将其打开
    • 同时用读写方式(O_RDWR)打开时,一定不会导致阻塞
    • 以只读方式(O_RDONLY)打开时,调用open()函数的进程将会被 阻塞直到有写方打开管道
    • 以写方式(O_WRONLY)打开时,阻塞直到有读方打开管道

实例

实现进程简单控制和利用管道通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <unistd.h>  //exex、fork头文件
#include <signal.h> //signal头文件
#include <sys/types.h> //wait头文件
#include <sys/wait.h> //wait头文件
#include <stdlib.h> //exit头文件
#include <pthread.h> //mutex头文件
#include <stdio.h>

int pipefd[2]; /*创建无名管道,pipefd[0]只能用于读; pipe[1]只能用于写*/
int child_1, child_2; //用于存储创建的子进程pid
pthread_mutex_t mutex; //进程互斥锁,避免抢占打印输出

void kill_child1() { //父进程杀死子进程1
printf("Child process 1 is killed by parent!\n");
close(pipefd[1]); //关闭管道写端
exit(0); //子进程1退出
}

void kill_child2() { //父进程杀死子进程2
printf("Child process 2 is killed by parent!\n");
close(pipefd[0]); //关闭管道读端
exit(0); //子进程2退出
}

void kill_childprocess() { //父进程捕捉到中断信号,用系统调用Kill()向两个子进 程发出信号
if (kill(child_1, SIGUSR1) < 0)
printf("Kill child1 process error!\n");
if (kill(child_2, SIGUSR1) < 0)
printf("Kill child2 process error!\n");
}

int main(int argc, char const* argv[]) {

int rtn;
pthread_mutex_init(&mutex, NULL); //初始化mutex

//创建管道
if (pipe(pipefd) == -1) {
printf("Create a nameless pipe error!\n");
}

//创建第一个子进程
switch (child_1 = fork())
{
case - 1:
printf("Create child process failed!\n");
break;
case 0:
close(pipefd[0]); //子进程1关闭读端
signal(SIGINT, SIG_IGN); //SIGINT:由Interrupt Key产生,通常是CTRL+C或者DELETE。
//发送给所有ForeGround Group的进程
signal(SIGUSR1, kill_child1); // 设置进程处理信号

int i = 1;
while (1) { //死循环里每秒通过管道发送信号给子进程2
sleep(1);
write(pipefd[1], (void*)&i, sizeof(int));
pthread_mutex_lock(&mutex);
printf("I have send you %d times!\n", i);
pthread_mutex_unlock(&mutex);
i++;
}
break;
default:
break;
}

//创建第二个子进程
switch (child_2 = fork())
{
case -1:
printf("Create child process failed!\n");
break;
case 0:
close(pipefd[1]); //子进程关闭写端
signal(SIGINT, SIG_IGN); //SIGINT:由Interrupt Key产生,通常是CTRL+C或者DELETE。
//发送给所有ForeGround Group的进程
signal(SIGUSR1, kill_child2); // 设置进程处理信号

int i = 1;
while (1) { //死循环里每秒通过管道发送信号给子进程2
read(pipefd[0], (void*)&i, sizeof(int));
pthread_mutex_lock(&mutex);
printf("I have received your message %d times!\n", i);
pthread_mutex_unlock(&mutex);
}
break;
default:
break;
}

signal(SIGINT,kill_childprocess); //当捕捉到中断信号后
//父进程用系统调用Kill()向两个子进 程发出信号

//等到两个子进程结束
waitpid(child_1, &rtn, 0);
waitpid(child_2, &rtn, 0);

// 关闭管道
close(pipefd[0]);
close(pipefd[1]);

printf("Parent Process is killed!\n");
return 0;
}