目录在左侧!!!

本文系统地介绍了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代表父进程IDPIDPGID代表组IDSID代表回话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函数族

作用:让父子进程来执行不想干的操作,操作一般为以下三种:

  1. 能够替换进程地址空间的代码.text段(文本段)
  2. 执行另外的程序,不需要创建额外的地址空间
  3. 当前程序中调用另外一个应用程序

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 正常退出

正常退出方法有五种,分别为:

  1. main函数调用return
  2. 进程调用exit(),标准c库
  3. 进程调用_exit()或者 _Exit(),属于系统调用
  4. 进程最后一个线程返回
  5. 最后一个线程调用pthread_exit

10.2 异常退出

以下三种情况会发生异常退出:

1.调用abort函数

2.当进程收到某些信号时,比如ctrl+C

3.最后一个线程对取消(cancellation)请求做出相应

不管进程如何终止,最后都会执行内核中的同一段代码,这段代码和相应进程关闭所有打开描述符,释放它所使用的存储器等。

对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit,exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数,在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用waitwaitpid函数取得终止状态。

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函数参数》,主要用来shmctlatoi两个函数!!!

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):自定义的信号处理函数指针

PSsignal函数有二个参数,第一个参数是一个整型变量(信号值),第二个参数是一个函数指针,是我们自己写的处理函数;这个函数的返回值是一个函数指针

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;
}
      

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐