Linux进程管理、共享内存和信号:从基础到实战的全面指南
目录在左侧!!!
本文系统地介绍了Linux进程管理的核心概念和关键技术。主要内容包括: 进程基础:进程与程序的区别、并发与并行的概念、进程控制块(PCB)的结构和进程状态转换。 进程控制:详细讲解fork/vfork创建进程、父子进程关系、exec函数族替换进程映像,以及孤儿/僵尸进程的处理方法。 进程间通信: 管道通信(无名/有名管道) 信号机制(信号发送/处理) 消息队列 共享内存 信号量(PV操作实现同步)守护进程; 系统调用:涵盖进程创建、终止、回收等关键系统调用的使用方法和实现原理。并通过大量代码演示了这些理论
1.进程的概念
进程就是启动的程序,和程序不同,程序是一个二进制的文件,并且占用磁盘空间。进程的所有数据都在内存中,还需要占用更多的系统资源(cpu、物理内存)
2.并行和并发
并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行
PS:并发不是真正意义上的”同时进行“,只是cpu把一个时间段划分成几个时间片段,然后在这个时间区间之间来回切换,由于cpu处理的速度非常块,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行
并行:当系统有一个以上cpu时,当一个cpu执行一个进程时,另一个cpu可以执行另一个进程,两个进程互补抢占cpu资源,可以同时进行,这中叫并行
PS:决定并行的因素不是cpu的数量,而且是cpu的核心数量,比如一个cpu多个核也可以并行
为了区别并行和并发,做了一个表格来更好解释:
| 并发 | 指的是多个事情,在同一时间段内同时发生了 | 并发的多个任务之间是互相抢占资源的 |
| 并行 | 指的是多个事情,在同一时间点上同时发生了 | 并行的多个任务之间是不互相抢占资源的 |
PS:只有在多cpu或者一个cpu多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的
3.PCB(进程控制块)
全名:Process Control Block
功能:每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息
linux内核的进程控制块是task_struct结构体,结构体的定义可以在/user/src/linux-headers-3.16.0-30/include/linux/sched.h头文件中查看。内部成员有很多,我们重点掌握以下部分即可。
| 进程id。系统中每个进程有唯一的id,在c语言中用pit_t类型表示,其实就是一个非负整数。 |
| 进程的状态,有就绪,运行,挂起,停止等状态。 |
| 进程切换时需要保存和恢复一些cpu寄存器 |
| 描述虚拟地址空间的信息 |
| 描述控制终端的信息 |
| 当前工作目录(current working directory) |
| umask掩码 |
| 文件描述符,包含很多指向file结构体的指针 |
| 和信号相关的信息 |
| 用户id和组id,可以用stat查看 |
| 会话(Session)和进程组 |
| 进程可以适用的资源上线(Resource Limit),umilit -a |
4.进程状态
进程基本的状态有五种,分别为初始态,就绪态,运行态,挂起态和终止态。
其中初始态为进程准备状态,常常与就绪态结合来看
状态运行情况如下图:

5.进程的控制
5.1 fork函数
首先,一个进程,包括代码,数据和分配给进程的资源。那么分配给进程的资源我们可以用fork函数来理解。这里讲解一下fork函数。
(1)fork()函数:不只是简单地“使用”父进程已分配的资源,它创建了一个几乎完全独立的副本,但采用了一种聪明的方式去延迟真正的资源复制。
简单理解:通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事。但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
(2)一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
函数原型:
pid_t fork(void)
返回值:
- =0:当前进程为子进程
- >0,当前进程为父进程
- 1,出错
包含的头文件:
- #include<sys/types.h>
- #include<unistd.h>
下图中的左图,代表fork函数之后父进程和子进程的情况,右图为父进程和子进程的区别。

拓展vfork函数:
vfork函数也能创建进程,但与fork函数有区别。
- 区别1: vfork可以直接使用父进程存储空间,不拷贝
- 区别2:可以保证子进程先运行,当子进程调用exit退出后,父进程才执行
vfork函数代码实战:
先建立一个代码实战的c程序。取名为vfork_test.c,下图为内部程序:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid > 0)
{
while(1)
{
printf("cnt = %d\n",cnt);
printf("parent pid is %d\n",getpid());
sleep(1);
}
}
else if(pid == 0)
{
while(1)
{
cnt++;
printf("cnt = %d\n",cnt );
printf("child pid is %d\n",getpid());
sleep(1);
if(cnt == 3)
{
exit(0);
}
}
}
return 0;
}
结果图如下,这就是vfork函数的作用,先运行子进程,当子进程调用exit退出后,父进程才执行:

5.2 父进程和子进程的基本常识
(1)子进程开始的时间就是父进程结束的时间
(2)父进程要执行文本段;文本段位于fork前的代码;子进程只进行fork之后的代码
(3)子进程也有父进程的文本段,但是不执行
PS:父子进程的执行顺序是不一定的:虽然在shell进程,和每次打印的结果来看,每次都是父进程先打印,但这也只能说明我们的父进程,子进程和shell进程相互同cpu的静态过程当中,可能只是我们的shell进程先把父进程的东西先打印,但不一定是父进程先执行。
(4)获得fork返回值可以用getpid/getppid函数
- getpid:得到当前进程的pid
- getppid:得到当前进程的父进程的PID
5.3 父子进程间的数据共享
(1)fork之后两个地址空间区数据完全相同
(2)各个进程的地址空间中的数据是完全独立的
(3)对于同一个变量,读时共享
(4)写的时候分别在物理地址上拷贝一份变量进行的单独读写
拓展:
问:父子进程之间可不可以通过全局变量通信?
答:不能,两个进程内存不可以共享
fork代码实战:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
int i = 0;
for(i = 0;i < 4;i++)
{
printf("----i = %d----\n",i);
}
pid = fork();
if(pid > 0)
{
printf("this is father progress %d\n",getpid());
}
else if(pid == 0)
{
printf("this is child progress %d,father progress is %d\n",getpid(),getppid());
}
for(i = 0;i < 4;i++)
{
printf("i = %d\n",i);//会发现父进程和子进程都会运行这一块代码
}
return 0;
}
然后运行一下结果:

- 红色框部分代表最后一段for循环,这部分代码父进程和子进程都会运行
- 蓝色框部分代表fork之前,这部分代码不会受到父进程和子进程的影响
- 绿色框部分代表父进程和子进程的pid值
6.ps和kill命令
6.1 ps命令
作用:查看进程信息
部分参数:
- a:显示现行终端机下的所有程序,包括其他用户的程序
- u:以用户为主的格式来显示程序状况
- x:显示所有程序,不以终端机来区分
一般常用两种:
第一种:
ps aux
linux演示结果为:

第二种:
ps ajx
linux演示结果为:

其中PPID代表父进程ID,PID,PGID代表组ID,SID代表回话ID。
6.2 kill命令
作用:向指定的进程发送信号。
kill可将指定的信息送至程序。预设的信息为SIGTERM(15),可将制定程序终止。若仍无法终止该程序,可使用SIGKILL(9)信息尝试强制删除程序。程序或工作的编号可利用ps指令或job指令查看。
参数:
- -a:当处理当前进程时,不限制命令名和进程号的对应关系
- -l<信息编号>:若不加<信息编号>选项,则-l参数会列出全部的信息名称
- -p:制定kill命令只打印相关进程的进程号,而不发送任何信号
- -s<信息名称或编号>:制定要送出的信息
- -u:指定用户
代码实战
先vi一个hello.c的程序,作为我们的背景代码:
#include <stdio.h>
int main()
{
while(1)
{
printf("hello world!\n");
sleep(1);
}
return 0;
}
然后我们生成一个hello的运行程序,也就是通过gcc hello.c -o hello来实现。

然后开始运行我们的hello.c,即./hello

这时候红色框中的kill是不会出现的,他会一直出现hello world!这一行输出。
我们如果想要这个killed出现,那么我们再打开一个终端,输入ps aux| grep hello
这时候会弹出一个./hello运行程序的pid,然后我们再输入kill -9 (当前pid),即可kill我们之前运行的hello运行程序。

7.exec函数族
作用:让父子进程来执行不想干的操作,操作一般为以下三种:
- 能够替换进程地址空间的代码.text段(文本段)
- 执行另外的程序,不需要创建额外的地址空间
- 当前程序中调用另外一个应用程序
exec函数族的函数原型拥有统一的返回值:
- 函数运行成功,则不返回
- 如果执行失败,打印错误信息,退出子进程
一般函数原型有三种,下面对这三种函数原型进行讲解,在进行讲解之前,可以先补充一个知识点,就是函数原型带有字母的一般规律,看下面这个表。
| l(list) | 命令行参数列表 |
| p(path) | 搜索file时适用path变量 |
| v(vector) | 使用命令行参数数组 |
| e(environment) |
使用环境变量数组,不适用进程原有的环境变量,设置新加载程序运行的环境变量 |
接下来再开始讲解三种函数原型:
7.1 指定执行目录下的程序
作用:一般执行自己写的程序
函数原型:
int execl(const char *path,const char *arg,... ,/* (char *)NULL */);
参数:
- path:要执行程序的路径(最好是绝对路径)
- 变参arg:要执行的程序需要的参数
- 第一位arg:占位-----------最好和我们的程序一样
- 后边的arg:命令的参数
- 参数写完之后:null
execl代码实战1:
依旧先建立一个代码实战的c程序。取名为execl_text.c,下图为内部程序
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int i = 200;
int main()
{
pid_t pid;
int i = 0;
pid = fork();
if(pid > 0)
{
i += 400;
printf("i = %d\n",i);
printf("this is father progress %d\n",getpid());
}
else if(pid == 0)
{
execl("/bin/ls","ls""-l",NULL);//execl函数
i+= 200;
printf("i = %d\n",i);
printf("this is child progress %d,father progress is %d\n",getpid(),getppid());
}
return 0;
}
然后我们运行一下该程序:

可以看到我们运行出来的图,我们两个红色框分别代表父进程的printf输出和子进程的printf输出。
但我们子进程的printf输出是不是并没有正确显示,那是因为execl函数会替换进程中的.text内容,也就是文本段的内容,其后的程序不会再执行。
中间一段文件名,也就是我们execl函数的执行情况。
execl代码实战2:
依旧先建立一个代码实战的c程序。取名为execl_text.c,这次我们对我们之前写的hello.c程序进行execl函数的实战,下图为内部程序
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int i = 200;
int main()
{
pid_t pid;
int i = 0;
pid = fork();
if(pid > 0)
{
i += 400;
printf("i = %d\n",i);
printf("this is father progress %d\n",getpid());
}
else if(pid == 0)
{
execl("/home/harry/csdn/hello","hello",NULL);//因为我们将之前的hello文件放在了我们的csdn目录下,所以要改一下路径,可以通过pwd指定来看地址
i+= 200;
printf("i = %d\n",i);
printf("this is child progress %d,father progress is %d\n",getpid(),getppid());
}
return 0;
}
最后我们再调用一个终端,用之前学到的kill来杀死他,看下图,但这次kill之后,我们运行程序的结尾并没有出现killed这一显示,要记住!!!子程序依旧只执行execl函数。

7.2 执行PATH环境变量能够搜索到的程序
作用:执行系统自带的程序:/bin/xx,比如 ps aux
函数原型:
int execlp(const char *file,const char *arg,... ,/* (char *)NULL */);
参数:
- file:执行的命令名字
- 变参arg:要执行的程序需要的参数
- 第一位arg:占位-----------最好和我们的程序一样
- 后边的arg:命令的参数
- 参数写完之后:null
execlp代码实战1:
依旧先建立一个代码实战的c程序。取名为execlp_text.c,这次我们执行系统自带的ps aux作为例子,看代码图
#include <stdio.h>c
#include <sys/types.h>
#include <unistd.h>
int i = 200;
int main()
{
pid_t pid;
int i = 0;
pid = fork();
if(pid > 0)
{
i += 400;
printf("i = %d\n",i);
printf("this is father progress %d\n",getpid());
}
else if(pid == 0)
{
execlp("ps","ps","aux",NULL);
i+= 200;
printf("i = %d\n",i);
printf("this is child progress %d,father progress is %d\n",getpid(),getppid());
}
return 0;
}
结果图如下,和execl一样,execlp函数不再执行其后的程序。

7.3 执行指定路径,指定环境变量下的程序
函数原型:
int execle(const char *path,const char *arg,...,/*(char *)NULL,char * const envp[] */);
int execv(const char *path, char *const argv[]);
参数:
- path:/bin/ps
- char *argv[]:{“ps”,“aux”,NULL}
- execv(/ps/aux“”, args)
8.孤儿进程和僵尸进程
8.1 孤儿进程
概念:一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。
孤儿进程将被init进程(即进程号为1)所收养,并由init进程对它们完成状态收集工作
拓展:
问:孤儿进程为什么会被init进程收养?
答:为了释放子进程的占用的系统资源
1.进程结束之后,能够释放用户区空间
2.释放不了PCB,必须由父进程释放
孤儿进程代码实战:
依旧先建立一个代码实战的c程序。取名为child_text.c,程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid > 0)
{
printf("parent process pid %d\n",getpid());
}
else if(pid == 0)
{
sleep(2);
printf("child process pid %d,ppid %d\n",getpid(),getppid());
}
return 0;
}
先打印父进程为3281,然后通过sleep(2)来让父进程先死,得到结果,ppid为2234,然后我们ps aux来看一下父进程情况,可以看出父进程确认先死,这就是孤儿进程

8.2 僵尸进程
概念:一个比较特殊的状态,当进程退出父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵尸进程。
僵尸进程会在以终止状态保持在进程表中,并且会一直等待父进程读取退出状态代码。(翻译过来就是 子进程退出的时候,父进程还在工作,没有时间回收子进程的资源)
拓展:
问:因为无法直接kill僵尸进程,那么僵尸进程怎么解决?
答:因为僵尸进程是一个已经死掉了的进程,因此可以直接kill父进程
僵尸进程代码实战:
依旧先建立一个代码实战的c程序。取名为z_text.c,程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid > 0)
{
while(1)
{
sleep(1);
printf("parent process pid %d\n",getpid());
}
}
else if(pid == 0)
{
printf("child process pid %d,ppid %d\n",getpid(),getppid());
}
return 0;
}
运行一下该程序,得到结果图,并且再打开一个终端,输入ps aux看一下当前进程状态:

可以看到我们的父进程是一直在打印的,当前的进程显示为Z+状态,该状态代表僵尸状态。
然后我们输入一下kill -9 3321,会发现无法kill僵尸进程,但我们kill父进程就可以解决僵尸进程
9.进程回收
一般进程回收,我们用wait阻塞函数和waitpid函数。
9.1 wait阻塞函数
函数作用有以下:
- 阻塞并等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)
PS:调用一次只能收回一个子进程
函数原型:
pid_t wait(int *wstatus)
返回值:
- -1:回收失败,已经没有子进程了
- >0:回收子进程对应的pid
参数说明:
status:判断子进程如何退出状态
1. WIFEXITED(status):为非0,进程正常结束
WEXITSTATUS(status):如上宏为真,使用此宏,获取进程退出状态的参数(例如最终return为10,那么退出状态的参数为10)
2.WIFSIGNALED(status):为非0,进程异常退出
WTERMSIG(status):如上宏为真,使用此宏,取得使进程种植的那个信号的编号(例如 用kill -9 -pid ,那么输出的编号是9)
wait函数代码实战1:
依旧先建立一个代码实战的c程序。取名为text.c(之后都用这个文件来操作),程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int i = 0;
for(i = 0;i < 4;i ++)
{
printf("----i = %d ----\n",i);
}
pid = fork();
if(pid > 0)
{
pid_t wpid;
wpid = wait(NULL);
printf("wpid = %d\n",wpid);
printf("this is father process %d\n",getpid());
}
else if(pid == 0)
{
printf("this is child process %d,father pid is %d\n",getpid(),getppid());
}
for(i = 0;i < 4;i++)
{
printf("i = %d\n",i);
}
return 0;
}
看一下结果图,我们能发现wpid出现的时间在父进程执行完之后,才输出的wpid的结果,这就是wait阻塞函数的等待子进程结束

也可以在父进程的两个printf之间加入以下代码,来判断子进程状态是否为正常退出状态
if(WIFEXITED(status))
{
printf("exit value is %d\n",WEXITSTATUS(status));
}
如果此时的退出值为函数的返回值,则代表正常退出。
wait函数代码实战2:
在text.c文件中操作,代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int i = 0;
for(i = 0;i < 4;i ++)
{
printf("----i = %d ----\n",i);
}
pid = fork();
if(pid > 0)
{
int status;
pid_t wpid;
wpid = wait(&status);
printf("wpid = %d\n",wpid);
//nomal exit
if(WIFEXITED(status))
{
printf("exit value is %d\n",WEXITSTATUS(status));
}
//abnormal exit
if(WIFSIGNALED(status))
{
printf("exit by signal is %d\n",WTERMSIG(status));
}
printf("this is father process %d\n",getpid());
}
else if(pid == 0)
{
while(1)
{
sleep(1);
printf("this is child process %d,father pid is %d\n",getpid(),getppid());
}
}
for(i = 0;i < 4;i++)
{
printf("i = %d\n",i);
}
return 0;
}
再打开一个终端,然后用kill杀死子进程,看一下最后的结果图:

可以看到结果,直到子程序被kill了,才会输出父进程中的程序,这就是wait函数的作用。
9.2 waitpid函数
作用:同wait函数,但多一个功能,可以选择阻塞或者不阻塞,由第三个参数决定
函数原型:
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
- >0:返回清理掉的子进程ID
- -1:回收失败,无子进程
- 如果为非阻塞 则返回值 =0:参数3为WNOHANG,且子进程正在运行
参数说明:
pid:指定回收某个子进程
- pid == -1 回收所有子进程
- pid>0回收某个pid相等的子进程
- pid==0 回收当前进程组的任一子进程
- pid<0 子进程的PID取反(加减号)
status: 子进程的退出状态,用法同wait函数
options: 设置为WNOHANG,函数非阻塞;设置为0,函数阻塞
函数注意事项:
- 在子进程回收的情况下,都会经过短暂的僵尸状态
- 为了避免打印多次,处理方法为:在子进程回收下,加入一个判断,判断返回值是否为0,如果为0,则continue,这样就避免打印。(也就是加入一个小的if循环)
10.进程退出
进程退出一般分为两种,一种为正常退出,一种为异常退出。
10.1 正常退出
正常退出方法有五种,分别为:
- main函数调用return
- 进程调用exit(),标准c库
- 进程调用_exit()或者 _Exit(),属于系统调用
- 进程最后一个线程返回
- 最后一个线程调用pthread_exit
10.2 异常退出
以下三种情况会发生异常退出:
1.调用abort函数
2.当进程收到某些信号时,比如ctrl+C
3.最后一个线程对取消(cancellation)请求做出相应
不管进程如何终止,最后都会执行内核中的同一段代码,这段代码和相应进程关闭所有打开描述符,释放它所使用的存储器等。
对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit,exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数,在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得终止状态。
11.进程间的通信(IPC)
IPC:全名Inter Process Communication
进程间通信常用的几种方法:
1.管道通信:有名管道,无名管道
2.信号---系统开销小
3.信息队列---内核的链表
4.信号量---计数器
5.共享内存
6.内存映射
7.套接字---稳定
二、无名管道
1.管道的概念
看下图,一目了然。

1.1 本质
是一个内核缓冲区;管道也是一个伪文件,不占用磁盘空间
1.2 特点
首先由两部分组成,分别为读端和写端,对应两个文件的描述。数据或进程1从写段流入,读端流出到数据或进程2;(可以对应上面的图来看)
其次是操作管道的进程被销毁之后,管道自动释放。
最后管道默认是阻塞的
2.管道的原理
内部实现方式:队列,并且是一个环形队列。
特点:先进先出
缓冲区大小:默认4k,大小根据实际情况进行适当调整
3.管道的局限性
队列:数据只可以读取一次,不可以重复读取
半双工:数据传输方向是单向的
举例:
- 单工:可以看成遥控器,只能从一端发送给另一端,但另一端口不能发送回给这一端
- 半双工:可以看成对讲机,两端都能互相发送,但a端发送消息给b端口的时候,b端口不能同时发消息回来,需要等待a端消息发送完毕,才能再从b端口发送消息回给a端
- 全双工:可以看成手机,两端都能互相发送,并且同时发送
匿名管道:适用于有血缘关系的进程
4.创建匿名管道
函数原型:
int pipe(int pipefd[2])
返回值:
- 成功:返回0
- 失败:即创建失败,返回-1
参数说明:
fd -传出参数(常用!!重点):
fd[0]-读端;
fd[1]-写端
5.父子进程使用管道通信
实现 ps aux|grep “bash”
数据重定向:dup2
分析一下上面这一段:左边是父进程,中间是管道|,右边是子进程。
流程是:通过父进程进行数据重定向dup2给管道,管道再给子进程,最后子进程给终端(显示),可以看以下这个“块引用”,(数据传输是在目标程序(如grep)开始执行后才发生的)。
ps命令 → 标准输出 → 管道写入端 → 管道缓冲区 → 管道读取端 → 标准输入 → grep命令
PS:写代码的时候,父进程因为要写数据给管道,所以要关闭读端;子进程因为要读管道的数据,所以要关闭写端。并且用execlp函数组写父进程ps aux和子进程grep "bash"
pipe代码实战1(单个进程):
用test.c文件写内部程序,如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int ret;
int fd[2];
ret = pipe(fd);
if(ret == -1)
{
printf("pipe creat failed\n");
exit(1);
}
printf("pipe creat success!\n");
printf("fd[0] is %d\n",fd[0]);
printf("fd[1] is %d\n",fd[1]);
close(fd[0]);
close(fd[1]);
return 0;
}
~
结果图如下:

- 问:为什么读端输出是3,写端输出是4呢?为什么不能是0、1、2呢
- 答:0、1、2是标准输入、输出和报错,因此将会从3开始算
pipe代码实战2(父子进程使用管道):
用test.c文件写内部程序,根据上面所学的父子进程使用管道通信,程序如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int ret;
int fd[2];
ret = pipe(fd);
if(ret == -1)
{
printf("pipe creat failed\n");
exit(1);
}
printf("pipe creat success!\n");
pid = fork();
if(pid == -1)
{
printf("fork failed\n");
exit(1);
}
if(pid > 0)
{
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);// 将标准输出重定向到管道写入端
execlp("ps","ps","aux",NULL);
perror("excelp");
exit(1);
}
else if (pid == 0)
{
close(fd[1]);
dup2(fd[0],STDIN_FILENO);// 将标准输入重定向到管道读取端
execlp("grep","grep","bash","--color=auto",NULL);
}
return 0;
}
在这里拓展一个函数为dup函数,作用是重定向,函数原型为:
int dup2(int oldfd, int newfd); //新段指向旧端
运行之后,结果图为:

- 再次声明:父子进程在使用管道的时候,父进程写的时候要关闭读,子进程读的时候要关闭写
6.管道的读写行为
读操作:
第一种为 有数据:read(fd[1])正常读,返回读出的字节数;
第二种为 无数据:写端被全部关闭,read返回0,相当于读文件到了尾部
如果没有全部关闭,则是read阻塞
写操作:
第一种为 读端全部关闭:管道破裂,进程被终止
内核给当前进程发送信号SIGPIPE-13,默认处理动作;
第二种为 读端没全部关闭:如果缓冲区写满,则write阻塞,
如果缓冲区没满,则write继续写,直到写满,阻塞
7.查看管道缓冲区大小
查看管道缓冲区大小有两种方式:
第一种:命令为:ulimit -a

第二种,函数原型:
long fpathconf(int fd, int name);
fpathconf代码实战:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int ret;
int fd[2];
ret = pipe(fd);
if(ret == -1)
{
printf("pipe creat failed\n");
exit(1);
}
printf("pipe creat success!\n");
long size = fpathconf(fd[0],_PC_PIPE_BUF);
printf("size id %ld\n",size);
close(fd[0]);
close(fd[1]);
return 0;
}

三、有名管道
功能:创建管道文件
函数原型:
int mkfifo(const char \*filename,mode_t mode);
返回值:
- 创建成功,则返回0
- 创建失败,则返回-1
参数说明:
*filename:管道文件文件名
mode:权限,创建的文件权限仍然和umask有关系
1.特点
- 在磁盘上有这样一个文件 ls-l->p
- 也是一个伪文件,在磁盘大小永久为0
- 数据存在内核中有一个对应的缓冲区
- 半双工通信方式
2.使用场景
没有血缘关系的进程间通信
3.创建方法
命令:mkfifo 管道名
函数:mkfifo()
int mkfifo(const char \*filename,mode_t mode);
PS:fifo文件可以使用io函数进程操作,例如open/close,read/write,但不能执行lseek操作
4.有名管道之间的多进程通信
mkfifo只会产生一个用户态节点(大小永远是0),这个节点会指向内核的缓冲区,而产生fifo这类管道的时候,都是调用open函数这类系统函数才会产生。
mkfifo代码实战:
先读程序:用test.c文件来操作:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int ret;
int fd;
int nread;
char readBuff[50] = {0};
ret = mkfifo("/home/u/process/myfifo",0777);
if(ret == -1)
{
return -1;
}
printf("creat file success!\n");
fd = open("./myfifo",O_RDONLY);
if(fd < 0)
{
return -1;
}
printf("open file success!\n");
nread = read(fd,readBuff,50);
printf("read %d byte from fifo :%s\n",nread,readBuff);
close(fd);
return 0;
}
写程序,用write.c来编写:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd;
char *str = "hello world!";
fd = open("./myfifo",O_WRONLY);
if(fd < 0)
{
return -1;
}
printf("open file success!\n");
write(fd,str,strlen(str));
close(fd);
return 0;
}
~
最后运行出来的结果可以看图,如果开始就有myfifo文件的话,那么需要先删掉再去运行程序。

四、信息队列
信息队列的定义:是信息的链表,存放在内核中,一个信息队列由一个标识符(队列ID)来标识

特点:
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
- 消息队列独立于发送和接收进程,进程终止时,消息队列及其内容仍存在
- 消息队列可以实现消息的随机查询,消息不一定要先进先出的次序读取,也可以按消息的类型读取
以下讲解以下信息队列的相关函数:
1.msgget函数
作用:创建或打开消息队列
函数原型:
int msgget(key_t key,int msgflg);
返回值:
- 成功,返回队列ID
- 失败,则返回-1
参数说明:
key:和消息队列关联的key值
msgflg:是一个权限标志,表示消息队列的访问权限,他与文件的访问权限一样
msgflg拓展1:msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列;如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符
msgflg拓展2:在以下两种情况下,msgget将创建一个新的消息队列:
- 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志
- key参数为IPC_PRIVATE
2.msgsnd函数
作用:添加消息
函数原型:
int msgsnd(int msgid,const void *msg_ptr,size_t msgsz,int msgflg)
返回值:
- 成功,返回0
- 失败,返回-1
参数说明:
msgid:消息队列的ID
msg_ptr:指向消息的指针,常用结构体smgbuf如下:
struct msgbuf
{
long mtype;//消息类型
char mtext[N];//消息正文
}
msgsz:发送的正文里的字节数
msgflg:一般选用以下两个标志位的一种
- IPC_NOWAIT :消息没有发送完成函数也会立即返回
- 0:直到发送完成函数才返回
PS:添加信息可以用fgets传输信息给指针内部
3.msgrcv函数
作用:读取消息
函数原型:
ssize_t msgrcv(int msgid,void *msg_ptr,size_t msgsz,long msgtyp,int msgflg)
返回值:
- 成功,返回消息数据的长度
- 失败,返回-1
参数说明:
msgid:消息队列的ID
msg_ptr:要接收信息的缓冲区
size:接收消息的的字节数
msgtype(后续还有补充,可以看本函数的最后):
- 0:接收消息队列中第一个消息
- 大于0:接收消息队列中第一个类型为msgtyp的消息
- 小于0:返回队列中消息类型值小于或等于msgtyp绝对值的消息,如果有多个,则取类型值最小的消息
- (可以总结出,msgtype值非0时用于以非先进先出次序读取消息,也可以把msgtype看成优先级的权值)
flag:一般选用以下两个标志位的一种
- IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG。
- 0:若无消息函数一直阻塞
PS:在读取前,需要用memset先将要读取的缓冲区初始化
4.msgctl函数
作用:控制消息队列
函数原型:
int msgctl(int msqid,int cmd,struct msqid_ds *buf)
返回值:
- 成功,返回0
- 失败,返回-1
参数说明:
msgid:消息队列的队列ID
cmd:将要采取的动作,它可以取三个值
- IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值
- IPC_SET:如果进程有足够的权限,就把消息队列的当前关联值设置为msgid_ds结构中给出的值
- IPC_RMID:删除消息队列
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构
msgget函数和msgctl函数代码实战:
用test.c程序来进行实战,目的是实现消息队列的建立和删除,程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
int main()
{
int msgid;
msgid = msgget(IPC_PRIVATE,0755);
if(msgid == -1)
{
printf("creat message queue failed\n");
return -1;
}
printf("creat message queue success,msgid = %d\n",msgid);
system("ipcs -q");
msgctl(msgid,IPC_RMID,NULL);//删除消息队列
system("ipcs -q");
return 0;
}
可以通过system("ipcs -q")显示当前系统中所有的消息队列信息,结果图如下:

msgget函数、msgsnd函数和msgrcv函数代码实战:
用test.c程序来进行实战,目的是实现单进程使用消息队列,程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
struct msgbuf
{
long mtype;
char mtext[128];
};
int main()
{
int msgid;
struct msgbuf sendbuf,readbuf;
int readret;
msgid = msgget(IPC_PRIVATE,0755);
if(msgid == -1)
{
printf("creat message queue failed\n");
return -1;
}
system("ipcs -q");
printf("creat message queue success,msgid = %d\n",msgid);
//init msgbuf
sendbuf.mtype = 100;
printf("plese input message:\n");
fgets(sendbuf.mtext,128,stdin);
//send message to message queue
msgsnd(msgid,(void *)&sendbuf,strlen(sendbuf.mtext),0);
//read message
memset(readbuf.mtext,0,128);
readret = msgrcv(msgid,(void *)&readbuf,128,100,0);
printf("receive mesagge is :%s\n",readbuf.mtext);
printf("total message is %d\n",readret);
return 0;
}
读出内容的时候,节点中内容被删除,但是节点仍然存在,结果图如下:

5.ftok函数
作用:消息独队列进程间通信。一般系统建立IPC通讯(如消息队列、共享内存时)必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
函数原型:
key_t ftok(char *fname,int id)
返回值:
- 成功执行,一个key_t值会被返回
- 失败执行,-1被返回。
参数说明:
fname:指定的文件名(该文件必须是存在而且可以访问的)
id:子序号,虽然为int,但是只有8个比特被使用(0-255)
拓展:
问:消息队列进程间如何进行双工通信?
答:可以采用fork,将进程a的写用作父进程,读用作子进程;将进程b的读用作父进程,写用作子进程
ftok函数代码实战(单工):
该代码实战的目的是 实现消息队列进程间单工通信
一般包括写进程和读进程,先说写进程,写进程的流程如下图来构造:
开始 → 生成IPC key (ftok) → 创建消息队列 (msgget) → 设置消息类型为100 → 进入循环
循环图:
清空缓冲区 → 读取用户输入 → 发送到消息队列
↑ ↓ ↓
└───────────────────────────┘
用test.c程序来写“写进程”的程序,程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
struct msgbuf
{
long mtype;
char mtext[128];
char ID[4];
};
int main()
{
struct msgbuf sendbuf;
int msgid;
int readret;
key_t key;
key = ftok("a.c",1);
msgid = msgget(key,IPC_CREAT | 0755);
if(msgid == -1)
{
printf("creat message queue failed\n");
return -1;
}
system("ipcs -q");
printf("creat message queue success,msgid = %d\n",msgid);
//init message
sendbuf.mtype = 100;
//send message to message queue
while(1)
{
memset(sendbuf.mtext,0,128);
printf("please input to message queque\n");
fgets(sendbuf.mtext,128,stdin);
msgsnd(msgid,(void *)&sendbuf,strlen(sendbuf.mtext),0);
}
return 0;
}
用test2.c程序来写“读进程”的程序,程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
struct msgbuf
{
long mtype;
char mtext[128];
char ID[4];
};
int main()
{
struct msgbuf readbuf;
int msgid;
int readret;
key_t key;
key = ftok("a.c",1);
msgid = msgget(key,IPC_CREAT | 0755);
if(msgid == -1)
{
printf("creat message queue failed\n");
return -1;
}
system("ipcs -q");
printf("creat message queue success,msgid = %d\n",msgid);
//init message
readbuf.mtype = 100;
//send message to message queue
while(1)
{
memset(readbuf.mtext,0,128);
readret = msgrcv(msgid,(void *)&readbuf,128,100,0);
printf("receive message id %s\n",readbuf.mtext);
printf("total receive %d byte\n",readret);
}
return 0;
}
然后我们打开两个终端,一个执行“写进程”的test.c的程序,一个执行“读进程”的test2.c的程序,然后我们在“写进程”的终端发送我们要给读端的文字,然后会发现“读进程”的终端会显示出结果:

但我们可以看到,当我们在读端口发送消息的时候,写端口是无法显示出来的,这就是单工通信
ftok函数代码实战(全双工):
该代码实战的目的是 实现消息队列进程间全双工通信
一般包括可以分为服务进程(服务端)和客户进程(客户端),先写服务进程,用test.c程序来写“服务进程”的程序,程序如下:
int main()
{
struct msgbuf sendbuf,readbuf;
int msgid;
int readret;
int pid;
key_t key;
key = ftok("a.c",1);
msgid = msgget(key,IPC_CREAT | 0755);
if(msgid == -1)
{
printf("creat message queue failed\n");
return -1;
}
system("ipcs -q");
printf("creat message queue success,msgid = %d\n",msgid);
//init message
sendbuf.mtype = 100;
pid = fork();
if(pid > 0)//parent progress write 100
{
while(1)
{
memset(sendbuf.mtext,0,128);
printf("please input to message queque\n");
fgets(sendbuf.mtext,128,stdin);
msgsnd(msgid,(void *)&sendbuf,strlen(sendbuf.mtext),0);
}
}
else if(pid == 0)//child progress read 200
{
while(1)
{
memset(readbuf.mtext,0,128);
readret = msgrcv(msgid,(void *)&readbuf,128,200,0);
printf("receive progress is %s\n",readbuf.mtext);
printf("total receive %d byte\n",readret);
}
}
return 0;
}
用test2.c程序来写“客户进程”的程序,程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct msgbuf
{
long mtype;
char mtext[128];
char ID[4];
};
int main()
{
struct msgbuf sendbuf,readbuf;
int msgid;
int readret;
int pid;
key_t key;
key = ftok("a.c",1);
msgid = msgget(key,IPC_CREAT | 0755);
if(msgid == -1)
{
printf("creat message queue failed\n");
return -1;
}
system("ipcs -q");
printf("creat message queue success,msgid = %d\n",msgid);
//init message
sendbuf.mtype = 200;
pid = fork();
if(pid == 0)//child progress write 200
{
while(1)
{
memset(sendbuf.mtext,0,128);
printf("please input to message queque\n");
fgets(sendbuf.mtext,128,stdin);
msgsnd(msgid,(void *)&sendbuf,strlen(sendbuf.mtext),0);
}
}
else if(pid > 0)//parent progress read 200
{
while(1)
{
memset(readbuf.mtext,0,128);
readret = msgrcv(msgid,(void *)&readbuf,128,100,0);
printf("receive progress is %s\n",readbuf.mtext);
printf("total receive %d byte\n",readret);
}
}
return 0;
}
然后我们打开两个终端,一个执行“服务进程”的test.c的程序,一个执行“客户进程”的test2.c的程序,然后我们在“服务进程”的终端发送我们要给客户端的文字,然后会发现“客户进程”的终端会显示出结果,相反, 客户端也可以给服务端发送信息,看下图:

五、共享内存
概念
共享内存就是允许多个进程访问同一个内存空间,是在多个进程之间共享和传递数据最高效的方式。操作系统将不同进程之间共享内存安排为同一段物理内存,进程可以将共享内存连接到他们自己的地址空间中,如果某个进程修改了共享内存中的数据,其他的进程读到的数据也将会改变。
以下讲解以下共享内存的相关函数:
1.shmget函数
作用:用来获取或创建共享内存
函数原型:
int shmget(key_t key,size_t size,int shmflg);
返回值:
- 成功:共享内存段标识符---ID---文件描述符
- 出错:-1
参数说明:
key:IPC_PRIVATE或ftok的返回值
size:共享内存区大小
shmflg:同open函数的权限位,也可以用8进制表示法
2.shmat函数
作用:把共享内存连接映射到当前进程的地址空间
函数原型:
void *shmat(int shm_id,const void *shm_addr,int shmflg);
返回值:
- 成功:映射后的地址
- 失败:NULL
参数说明:
shm_id:ID号
shm_addr:映射到的地址,NULL为系统自动完成的映射
shmflg:SHM_RDONLY共享内存只读
默认是0,表示共享内存可读写
3.shmdt函数
作用:将进程里的地址映射删除
函数原型:
int shmdt(const void *shmaddr)
返回值:
- 成功:0
- 出错:-1
参数说明:
shmid:要操作的共享内存标识符
shmget函数、shmdt函数和shmat函数代码实战:
用test.c程序来进行实战,该代码实战的目的是 实现 共享空间创建和读取和地址映射删除
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main()
{
int shmid;
int key;
char *p;
key = ftok("a.c",0);
if(key < 0)
{
printf("ftok failure\n");
return -1;
}
printf("ftok success,key:%d\n",key);
shmid = shmget(key,128,IPC_CREAT | 0777);
if(shmid < 0)
{
printf("creat share memory failure\n");
return -2;
}
printf("creat share memory success,shmid is %d\n",shmid);
system("ipcs -m");
p = (char *)shmat(shmid,NULL,0);
if(p ==NULL)
{
printf("shmat function failure!\n");
return -3;
}
//write to share memory
fgets(p,128,stdin);
//read from share memory
printf("read share memory date is %s\n",p);
printf("read second share memory is %s\n",p);
shmdt(p);
memcpy(p,"csdn_shmdt",11);
return 0;
}
可以看一下代码,先创建一个ftok生成的值,方便我们用shmget来创建共享内存,再用shmat函数来映射到当前进程的地址空间,然后开始输出一下显示结果,在这之后,我们可以用shmdt函数将进程里的地址映射删除,然后用memcpy函数,复制进我们的地址,如果不能复制进去,会出现报错,事实证明,结果图出现了报错:

4.shmctl函数
作用:删除共享内存对象
函数原型:
int shmctl(int shm_id,int command,struct shmid_ds *buf);
返回值:
- 成功:0
- 出错:-1
参数说明:
shmid:要操作的共享内存标识符
command:IPC_STAT(获取对象属性)--------实现了命令ipcs -m
IPC_SET(设置对象属性)
IPC_RMID(删除对象)---------实现了命令ipcrm -m
buf:指定IPC_STAT/IPC_SET时用以保存/设置属性,在使用IPC_RMID的时候可以设定为NULL
5.共享内存特点
(1)共享内存创建之后,一直存在于内核中,直到被删除或系统关闭
(2)共享内存和管道不一样,读取后,内容仍然在共享内存中
shmctl函数代码实战:
用test.c程序来进行实战,该代码实战的目的是 实现 删除共享内存
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main()
{
int shmid;
key_t key;
char *p;
key = ftok("a.c",0);
if(key < 0)
{
printf("ftok failure\n");
return -1;
}
printf("ftok success,key:%d\n",key);
shmid = shmget(key,128,IPC_CREAT | 0777);
if(shmid < 0)
{
printf("creat share memory failure\n");
return -2;
}
printf("creat share memory success,shmid is %d\n",shmid);
system("ipcs -m");
p = (char *)shmat(shmid,NULL,0);
if(p ==NULL)
{
printf("shmat function failure!\n");
return -3;
}
//write to share memory
fgets(p,128,stdin);
//read from share memory
printf("read share memory date is %s\n",p);
printf("read second share memory is %s\n",p);
shmdt(p);
shmctl(shmid,IPC_RMID,NULL);
system("ipcs -m");
return 0;
}
可以看一下代码,先创建一个ftok生成的值,方便我们用shmget来创建共享内存,再用shmat函数来映射到当前进程的地址空间,然后开始输出一下显示结果,在这之后,我们可以用shmdt函数将进程里的地址映射删除,然后用shmctl函数,删除共享内存,事实证明,结果图实现功能成功:

封装ipcrm -m shmid代码实战:
用test.c程序来进行实战,该代码实战的目的是 实现 封装ipcrm -m shmid
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main(int argc,char *argv[])
{
int shmid;
if(argc < 3)
{
printf("input error\n");
return -1;
}
if(strcmp(argv[1],"-m") == 0)
{
printf("delete share memory\n");
}
else
{
return -2;
}
shmid = atoi(argv[2]);
printf("delete share memory id :%d\n",shmid);
shmctl(shmid,IPC_RMID,NULL);
system("ipcs -m");
return 0;
}
~
这里用了上节课讲的《main函数参数》,主要用来shmctl和atoi两个函数!!!

PS:atoi() asc码转int类型
六、信号
1.概念
信号通信,就是内核向用户空间进程发送信号,只有内核才能发信号,用户空间进程不能发送信号。
拓展
问:内核可以发送多少种信号?
答:使用kill -l可以查看,还有一种方法是命令:kill -9 pid(-9是信号 pid是标识进程不同的标识)
2.信号通信的框架
| 信号的发送(发送信号进程) | kill raise alarm |
| 信号的接收(接收信号进程) | pause() sleep while(1) |
| 信号的处理(接收信号进程) | signal |
3.信号的发送
信号的发送,也就是我们说的发送信号进程
信号的发送的相关函数比较多,接下来主要讲解几个重要的。
3.1 kill函数
所需头文件:
- #include<signal.h>
- #include<sys/types.h>
函数原型:
int kill(pid_t pid,int sig);
返回值:
- 成功:0
- 出错:-1
参数说明:
pid:取下面三种之一:
- 正数:要接收信号的进程的进程号
- 0:信号被发送到所有和pid进程在同一个进程组的进程
- -1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
sig:信号
3.2 raise函数
作用:发信号给自己
所需头文件:
- #include<signal.h>
- #include<sys/types.h>
函数原型:
int raise(int sig);
返回值:
- 成功:0
- 出错:-1
参数说明:
sig:信号
PS:raise也可以用kill函数来表示,如 raise(sig)== kill(getpid(),sig)
3.3 alarm函数
作用:发送闹钟信号的函数
所需头文件:
- #include<unistd.h>
函数原型:
unsigned int alarm(unsigned int seconds)
返回值:
- 成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0.
- 出错:-1
参数说明:
seconds:指定秒数
拓展
问:alarm与raise函数的比较,有哪些相同点和哪些不同点?
答:相同点:让内核发送信号给当前进程
不同点:alarm只会发送SIGALARM信号;alarm会让内核定时一段时间之后发送信号,raise会让内核立刻发信号
代码实战:
用test.c程序来进行实战,该代码实战的目的是 实现 看alarm函数经过signal函数忽略后的现象,代码如下
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void myfun(int signum)
{
int i = 0;
while(i < 4)
{
printf("signum = %d,i = %d\n",signum,i);
sleep(1);
i++;
}
}
int main()
{
int i = 0;
signal(14,myfun);
printf("Before alarm\n");
alarm(7);
printf("After alarm\n");
signal(14,SIG_IGN);
while(i < 10)
{
printf("process i is %d\n",i);
sleep(1);
i++;
}
return 0;
}
看结果图,可以发现alarm经过signal函数忽略之后,并没有正常运行。
- 7秒后闹钟超时时,由于信号已经被设置为忽略,myfun 函数永远不会被执行
- 即使闹钟触发了,信号也不会被递送给进程

4.信号的接收
一般接收信号的进程的时候,是需要条件的。也就是要想使接收的进程能收到信号,这个进程不能结束 。
例如用pause函数,显示进程状态为S,S代表sleep
4.1 pause函数
函数原型:
int pause(void);
返回值:
- 成功:0
- 出错:‐1
5.信号的处理
通过前面所涉及到的,我们可以收到信号的进程。那么这些进程又该怎样处理?
处理的方式:
5.1 进程的默认处理方法
(内核为用户进程设置的默认处理方式)
A:忽略 B:终止进程 C:暂停
2 自己的处理方式
自己处理信号的方法告诉内核,这样进程收到了这个信号就会采用自己的处理方式
所需头文件:
- #include<signal.h>
函数原型:
void(*signal(int signum,void(*handler)(int)))(int);
返回值:
- 成功:设置之前的信号处理方式
- 出错:-1
参数说明:
signum:指定信号
handler:SIG_IGN:忽略该信号
SIG_DFL:采用系统默认方式处理信号
void(*handler)(int)))(int):自定义的信号处理函数指针
PS:signal函数有二个参数,第一个参数是一个整型变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函数;这个函数的返回值是一个函数指针
6. 信号父子进程间通信
在其中父进程用while循环来执行,子进程在sleep之后发送一个kill信号,然后子进程给内核,再通过内核传给父进程。
七、信号灯
概念
信号灯是信号量的集合
- 信号灯 是 单个信号量。
- 信号灯集合 是 信号量集
IPC对象是一个信号的集合(多个信号量)
1.semget函数
作用:创建一个新的信号量或获取一个已经存在的信号量的键值
所需头文件:
- #include<sys/types.h>
- #include<sys/ipc.h>
- #include<sys/sem.h>
函数原型:
int semget(key_t key,int nsems,int semflg)
返回值:
- 成功:信号灯集ID
- 出错:-1
参数说明:
key:和信号灯集关联的key值
IPC_PRIVATE(通常为0):每次都创建新的信号量集- 其他值:可以通过
ftok()函数生成,用于多个进程访问同一信号量
nsems:信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限
- 低9位:权限位(类似文件权限,如0755)
- 7(111):所有者可读、写、执行
- 5(101):组用户可读、执行
- 5(101):其他用户可读、执行
- 可与以下标志按位或:
IPC_CREAT:不存在则创建IPC_EXCL:与IPC_CREAT一起使用,若存在则出错
2.semctl函数
作用:控制信号量,删除信号量或初始化信号量
所需头文件:
- #include<sys/types.h>
- #include<sys/ipc.h>
- #include<sys/sem.h>
函数原型:
int semcti(int semid,int semnum,int cmd,...union semun arg(不是地址))
返回值:
- 成功:0
- 出错:-1
参数说明:
semid:信号灯集ID
semnum:要修改的信号灯编号
cmd:GETVAL:获取信号灯的值
SETVAL:设置信号灯的值
IPC_RMID:从系统中删除信号灯集合
3.semop函数
作用:用户改变信号量的值,也就是使用资源还是释放资源使用权
所需头文件:
- #include<sys/sem.h>
函数原型:
int semop(int semid,struct sembuf *_sops,size_t _nsops);
返回值:
- 成功:返回信号量集的标识符
- 出错:-1,错误原因存于error中
参数说明:
semid:信号量的标识码,也就是semget()的返回值
sops:指向进行操作的信号量集结构体数组的首地址,如下:
struct sembuf{
nsigned short sem_num;//信号灯编号
short sem_op;//对该信号量的操作。-1:p操作 1:v操作
short sem_flg;0阻塞,1非阻塞
};
nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
PS:如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。
代码实战:
用test.c程序来进行实战,该代码实战的目的是 实现 看信号灯在semget函数创建和semctl函数删除的功能,代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
int main()
{
int semid;
semid = semget(IPC_PRIVATE,3,0755);
if(semid < 0)
{
printf("creat semaphore failure\n");
return -1;
}
printf("creat semaphore success,semid = %d\n",semid);
system("ipcs -s");
semctl(semid,0,IPC_RMID,NULL);
system("ipcs -s");
return 0;
}
可以看到我们先通过semget函数创建了semid=0的信号灯,然后又通过semctl函数删除了semid=0的信号灯,如结果图所示:

八、PV操作
在进行这个代码实战之前,需要科普一下阻塞与唤醒机制:
当信号量 = 0 时:
// 进程A执行P操作
P(sem) {
sem--; // sem = -1
if (sem < 0) { // -1 < 0,成立
阻塞进程A; // 进程A进入等待队列
}
}
// 进程B执行V操作
V(sem) {
sem++; // sem = 0
if (sem <= 0) { // 0 <= 0,成立
唤醒一个等待的进程; // 唤醒进程A
}
}
唤醒后的进程A:
- // 进程A被唤醒后,从P操作中返回;
- // 此时sem已经被进程B加1,但A已经减过1
- 继续执行后续代码;
PV操作分开来说就是P代表申请资源;V代表释放资源
该章节全部为代码,将前面的共享内存和信号灯结合在一起:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <unistd.h>
#define SEM_READ 0 // 读信号量在信号量集中的索引为0
#define SEM_WRITE 1 // 读信号量在信号量集中的索引为1
// 信号量操作联合体 - 用于设置信号量的初始值
union semun
{
int val; // 用于SETVAL命令的整数值
};
/**
* P操作函数(尝试获取资源)
* @param index 信号量在集合中的索引
* @param semid 信号量集ID
*
* 功能:将指定信号量的值减1,如果减1后值<0,则进程阻塞
*/
void Poperation(int index, int semid)
{
struct sembuf sop; // 信号量操作结构体
sop.sem_num = index; // 指定要操作的信号量索引
sop.sem_op = -1; // P操作:信号量值减1
sop.sem_flg = 0; // 操作标志:0表示阻塞模式
semop(semid, &sop, 1); // 执行信号量操作,操作1个信号量
}
/**
* V操作函数(释放资源)
* @param index 信号量在集合中的索引
* @param semid 信号量集ID
*
* 功能:将指定信号量的值加1,如果有进程在等待该信号量,唤醒一个
*/
void Voperation(int index, int semid)
{
struct sembuf sop; // 信号量操作结构体
sop.sem_num = index; // 指定要操作的信号量索引
sop.sem_op = 1; // V操作:信号量值加1
sop.sem_flg = 0; // 操作标志:0表示阻塞模式
semop(semid, &sop, 1); // 执行信号量操作,操作1个信号量
}
int main()
{
int shmid, semid; // 共享内存ID和信号量集ID
pid_t pid; // 进程ID
char *shmadd; // 共享内存附加地址指针
key_t key; // IPC键值
key = ftok("a.c", 0); // 根据文件"a.c"生成唯一的IPC键值
/**
* 创建信号量集
* 参数:键值,2个信号量,权限755(所有者读写执行,其他读执行)
*/
semid = semget(key, 2, IPC_CREAT | 0755);
if(semid < 0)
{
perror("semget error"); // 信号量创建失败
return -1;
}
/**
* 创建共享内存
* 参数:键值,128字节大小,权限755
*/
shmid = shmget(key, 128, IPC_CREAT | 0755);
if(shmid < 0)
{
perror("shmget error"); // 共享内存创建失败
return -2;
}
// 初始化信号量
union semun myun; // 创建联合体变量
// 初始化读信号量(索引0)
myun.val = 0; // 设置读信号量初始值为0
semctl(semid, SEM_READ, SETVAL, myun); // 将读信号量设为0(初始不可读)
// 初始化写信号量(索引1)
myun.val = 1; // 设置写信号量初始值为1
semctl(semid, SEM_WRITE, SETVAL, myun); // 将写信号量设为1(初始可写)
/**
* 创建子进程
* fork()返回:
* <0:创建失败
* =0:子进程
* >0:父进程(返回子进程PID)
*/
pid = fork();
if(pid < 0)
{
printf("fork failure\n"); // 进程创建失败
return -3;
}
if(pid == 0) // 子进程 - 负责读取数据
{
while(1) // 无限循环读取
{
/**
* 附加共享内存到子进程地址空间
* shmat返回附加后的内存地址
*/
shmadd = (char *)shmat(shmid, NULL, 0);
/**
* P操作:尝试获取读信号量
* 如果读信号量=0,进程会阻塞在这里
* 直到父进程写入数据后执行V操作
*/
Poperation(SEM_READ, semid);
// 从共享内存读取数据并打印
printf("get share memory is :%s\n", shmadd);
/**
* V操作:释放写信号量
* 将写信号量加1,表示缓冲区已空
* 唤醒可能正在等待的父进程
*/
Voperation(SEM_WRITE, semid);
}
}
else if(pid > 0) // 父进程 - 负责写入数据
{
while(1) // 无限循环写入
{
/**
* 附加共享内存到父进程地址空间
* 注意:父子进程的shmadd指向不同的虚拟地址
* 但映射到同一块物理内存
*/
shmadd = (char *)shmat(shmid, NULL, 0);
/**
* P操作:尝试获取写信号量
* 如果写信号量=0,进程会阻塞在这里
* 直到子进程读取数据后执行V操作
*/
Poperation(SEM_WRITE, semid);
// 提示用户输入
printf("Please input to share memory\n");
// 从标准输入获取字符串,最多32字符,写入共享内存
fgets(shmadd, 32, stdin);
/**
* V操作:释放读信号量
* 将读信号量加1,表示有新数据可读
* 唤醒可能正在等待的子进程
*/
Voperation(SEM_READ, semid);
}
}
return 0; // 实际不会执行到这里,因为while(1)无限循环
}
可以做个工作流程图:

在这个流程图中,我们可以发现信号量和共享内存明明是独立的IPC资源,但都可以通过key被同一组进程访问。
并且共享内存的关键用法也体现得很好;
shmadd = (char *)shmat(shmid, NULL, 0);
返回值:
- 父进程:
shmadd_parent = 0x7f123456(共享内存地址)- 子进程:
shmadd_child = 0x7f789abc(不同地址,指向同一物理内存)

九、守护进程
1.守护进程的特点
- 后台服务进程
- 独立于控制终端
- 周期性执行某任务
- 不受用户登录注销影响
- 般采用以d结尾的名字(服务)
2.进程组
进程的组长:1.组里边的第一进程
2.进程组的ID(PGID)== 进程中的组长的ID(PID)
进程中组长的选择:进程中的第一个进程
进程中ID的设定:进程组的ID就是组长的进程ID
3.会话
创建一个会话注意事项:1.不能是进程组长
2.创建会话的进程成为新进程组的组长
3.有些linux版本需要root权限执行此操作
4.创建出的新会话会丢弃原有的控制终端
5.一般步骤:fork,父亲死,子进程执行创建会话操作(setsid)
获取进程所属的会话ID: pid_t getsid(pid_t pid);
创建一个会话:pid_t setsid(void);
代码实战:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
pid_t pid;
pid = fork();
if(pid > 0)
{
kill(getpid(),SIGKILL);
//raise(SIGKILL);
//exit(1);
}
else if(pid == 0)
{
setsid();
while(1);
}
return 0;
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)