Linux应用(二)多进程编程

1 简述概念

1.1 什么是进程?

进程是程序的一次动态执行过程,是程序执行和资源管理的最小单位 。

1.1.1 程序与进程

程序是静态的,是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;
进程是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡;

  • 进程是执行一个程序所分配的资源的总称,是一个抽象实体;
  • 独立的可调度的任务;
  • 程序的一次执行过程;
  • 动态的,包括创建、调度、执行和消亡;

1.1.2 进程内存分配

注意:程序一旦被运行,就会产生一个进程,并且会被每一个进程分配一个0-4G(32OS下)的内存空间,分配空间大小和操作系统的位数有关。32位系统可以对应232 个地址,那么就有232 字节=4X1024X1024X1024个字节;
C语言内存分配图:
在这里插入图片描述
进程控制块(pcb):PID,进程用户, 进程优先级,文件描述符表
(这样分配基于一块完整的4G虚拟内存空间,实际由MMU单元完成虚拟内存地址到实际内存地址的映射;不需要程序员考虑;)

1.2 进程的类型

1、交互进程:该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行。
2、批处理进程:该类进程不属于某个终端,它被提交到一个队列中以便顺序执行,比如gcc命令。
3、守护进程:该类进程在后台运行,与终端无关。一般在Linux启动时开始执行,系统关闭时才结束。

1.3 进程的四种状态

1、运行态:此时进程或者正在运行,或者准备运行。
2、等待态:此时进程在等待一个事件的发生或某种系统资源(可中断或不可中断)
3、停止态:此时进程被中止(暂停)—》意味着还可以再继续。
4、死亡态:这是一个已终止的进程,但还在进程向量数组中占有一个task_struct结构。(僵尸态)
在这里插入图片描述

1.4 进程的运行模式

分为用户模式和内核模式;
在这里插入图片描述
内存空间分为0-4G,其中0-3G是用户空间,3G-4G是内核空间,一般用户空间不可以直接访问内核空间,除非系统调用方可。
系统调用:调用操作系统提供给用户来访问内核的一组接口

2 进程编程

2.1 系统调用

2.1.1 用fork()创建子线程

子进程的内容是完全拷贝了父进程中的所有代码

#include <sys/types.h>	//提供pid_t的定义
#include <unistd.h>
pid_t  fork(void);

返回值:

  • -1表示创建失败;
  • 0表示当前是子线程;
  • 大于0的整数是子线程PID,表示当前是父进程;

于是根据返回值就可以把父进程和子进程区分开:

pid_t res=fork();
if(0 == res)
{
	//子进程的代码,子进程就活在这对大括号里;
}
else if(res >0)
{
	//父进程
}
else
{
	perror("fork");
	return -1;
}
  • 子进程继承父进程的所有内容,包括所有代码的执行状态,比如父进程中fork之前的代码已经被执行,那么子进程中也是如此;父子进程都将从fork之后的代码开始执行;
  • 父子进程有独立的地址空间,互不影响
孤儿进程与僵尸进程
  • 若父进程先结束,子进程成为孤儿进程,被init进程收养,子进程变成后台进程
  • 若子进程先结束,父进程如果没有及时回收,子进程变成僵尸进程

在这里插入图片描述

2.1.2 exec函数

exec函数族提供了一种在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完之后,原调用进程的内容除了进程号外,其他全部都被替换了
在这里插入图片描述

  • l:代表是以列表的形式,以NULL结尾;
  • v:代表是以指针数组接收参数,数组以NULL结尾;
  • e:代表环境变量,最后一个参数可以传入自己指定的环境变量作为进程执行所用的环境变量;
  • p:代表可以只输入一个可执行文件的名字,不必输入全称,此时会自动去查找这个可执行文件所在的位置,从系统变量$PATH包含的路径中找,不加p则要指定文件路径;

exec常见错误:

  • 找不到文件或路径,此时errno被设置为ENOENT
  • 数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT
  • 没有对应可执行文件的运行权限,此时errno被设置为EACCESS

另外还有一个system函数可以直接在C程序中调用命令行命令,命令执行完后,函数返回:

#include  <stdlib.h>
int system(const char *command);

2.1.3 进程的退出

有两个函数可以用于结束进程:eixt() 和 _exit()

#include <stdlib.h> 
void  exit(int  status);
#include  <unistd.h>
void  _exit(int  status);

status用于向wait函数传递进程退出状态;
在这里插入图片描述
_exit()函数的作用最为简单:直接使进程终止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;(不会清空缓冲区)
exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。(会清空缓冲区:在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件)

2.1.4 进程回收

2.1.4.1 回收机制

子进程结束时由父进程回收;
孤儿进程由init进程回收;
若没有及时回收会出现僵尸进程;

2.1.4.2 回收函数
#include <sys/wait.h>
pid_t wait(int *status); 
pid_t waitpid(pid_t pid, int *status, int option);

两个都是用来回收子进程的退出状态;
1、wait()函数:
成功时返回回收的子进程的进程号;失败时返回EOF(-1)

  • 若子进程没有结束,父进程一直阻塞
  • 若有多个子进程,哪个先结束就先回收
  • status 指定保存子进程返回值和结束方式的地址
  • status为NULL表示直接释放子进程PCB,不接收返回值

2、waitpid()函数:
成功时返回回收的子进程的pid或0;失败时返回EOF(-1)

  • pid可用于指定回收哪个子进程或任意子进程
  • status指定用于保存子进程返回值和结束方式的地址
  • option指定回收方式,0 或 WNOHANG;

(1) pid
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
(2) status接收返回状态;
(3) options
options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用:
WNOHANG :若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0;
WUNTRACED: 返回终止子进程信息和因信号停止的子进程信息;
wait(wait_stat) 等价于waitpid(-1,wait_stat,0);

2.1.4.3 返回值的处理

子进程通过exit / _exit / return 返回某个值(0-255)
父进程调用wait(&status)和waitpid接收返回值,但不是直接接收,而是通过中间宏来处理才能得到返回的原值;

  • WIFEXITED(status) :判断子进程是否正常结束,bool;
  • WEXITSTATUS(status) :获取子进程返回值;
  • WIFSIGNALED(status) :判断子进程是否被信号结束,bool;
  • WTERMSIG(status) :获取结束子进程的信号类型;

2.2 创建守护进程

2.2.1 什么是守护进程

守护进程,也就是通常所说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件;
守护进程常常在系统启动时开始运行,在系统关闭时终止;
Linux系统有很多守护进程,大多数服务都是用守护进程实现的;

2.2.2 终端进程与守护进程

在Linux中,每一个系统与用户进行交流的界面称为终端。从该终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会被自动关闭。
守护进程能够突破这种限制,它从开始运行,直到整个系统关闭才会退出。如果想让某个进程不会因为用户或终端的变化而受到影响,就必须把这个进程变成一个守护进程

2.2.3 守护进程的特点

  • 能够运行在后台
  • 开机自启动,关机关闭

2.2.4 创建守护进程

补充:
(1) 进程组是一个或多个进程的集合。进程组由进程组ID来唯一标识,每个进程组都有一个组长进程,进程组ID就是组长进程的进程号。
(2) 会话组是一个或多个进程组的集合
在这里插入图片描述

步骤:
(1)创建子进程,父进程退出
(2)在子进程中创建新会话
(3)改变当前目录为根目录
(4)重设文件权限掩码
(5)关闭文件描述符
每一步作用:
(1)创建子进程,父进程退出
好处:可以让子进程的父进程是Init进程,也就脱离的终端(形式上)
目的:形式上先与终端脱离关系(子进程–》父进程—》shell—》terminal)
(2)在子进程中创建新会话
目的:让守护进程成为新的进程组以及会话组的组长,并且脱离终端的控制

pid_t setsid(void); //成功:返回调用进程的会话ID;失败:-1,设置errno。

(3)改变当前目录为根目录 (选做)
目的:为了防止守护进程的工作目录或者文件被删除

chdir(/);
chdir(/tmp”);

(4)重设文件权限掩码
目的:为了让守护进程变得更加灵活性(意思就是去掉掩码,不设置掩码)

if (umask(0) < 0)  
{
	exit(-1);
}

(5)关闭文件描述符
目的:将从父进程继承过来的文件描述符,没有用了,将其关闭掉,不然浪费空间,可以考虑只关闭0,1,2,这些肯定不再可用;

函数补充:

pid_t getsid(pid_t pid); //成功:返回调用进程的会话ID;失败:-1,设置errno

pid为0表示察看当前进程session ID
ps -ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程
在这里插入图片描述

2.3 线程同步与通信

2.3.1 管道通信

2.3.1.1 无名管道

无名管道是基于文件描述符的通信方式。当一个无名管道建立时,它会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道,就构成了一个半双工的通道。
在这里插入图片描述
这种无名管道的特点是:
1、只能用于具有亲缘关系的进程之间的通信,父子进程,兄弟进程间;
2、半双工的通信模式,具有固定的读端和写端
3、管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
4、管道文件存储在内核,是不可见的

#include  <unistd.h>
int  pipe(int pfd[2]);

成功时返回0,失败时返回EOF(-1)
pfd 包含两个元素的整形数组,用来保存文件描述符
pfd[0]用于读管道;pfd[1]用于写管道

通常,我们先在父进程中创建一个无名管道,然后fork出子进程,由于子进程对父进程代码和打开的资源完全复制,父子进程都对无名管道有读写两端,但是无名管道必须是半双工,使用时,比如父进程向子进程通信,那么先关闭父进程的读端和子进程的写端,构成一个半双工通道;
在这里插入图片描述
最后,要注意读写特性:
(1)读管道:

  • 管道中有数据:read返回实际读到的字节数。
  • 管道中无数据:
    ①管道写端被全部关闭,read返回0
    ② 写端没有全部被关闭,read阻塞等待

(2)写管道:

  • 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
  • 管道读端没有全部关闭:
    ①管道已满,write阻塞。(管道大小64K)
    ②管道未满,write将数据写入,并返回实际写入的字节数

示例程序看这篇:【Linux】应用篇十一–进程间的通信

2.3.1.2 有名管道/命名管道

无名管道只能用于具有亲缘关系的进程之间,而有名管道可以使互不相关的两个进程互相通信。有名管道可以通过路径名来指出,并且在文件系统中可见,进程通过文件IO来操作有名管道;有名管道遵循先进先出的FIFO规则;不支持如lseek() 操作;

在这里插入图片描述
有名管道的特点:
(1)有名管道可以适用于任意两个进程之间的通信;
(2)有名管道可以实现单工和双工通信;
(3)存储在内核中,但是在文件系统找中可以见到一个文件类型为p的管道文件,但是这个管道文件不存储信息的,大小为0;

#include  <unistd.h>
#include <fcntl.h>
int  mkfifo(const char *path, mode_t mode);
//成功时返回0,失败时返回EOF(-1)
//path:创建的管道文件路径
//mode:管道文件的权限,如0666
open(const char *path, O_RDONLY);//1
open(const char *path, O_RDONLY | O_NONBLOCK);//无阻塞读
open(const char *path, O_WRONLY);//3
open(const char *path, O_WRONLY | O_NONBLOCK);//无阻塞写
//对于以只读方式(O_RDONLY)打开的FIFO文件,如果open调用是阻塞的,除非有一个进程以写方式打开同一个FIFO,否则它不会返回;
//如果open调用是非阻塞的,则即使没有其他进程以写方式打开同一个FIFO文件,open调用将成功并立即返回。
//对于以只写方式(O_WRONLY)打开的FIFO文件,如果open调用是阻塞的,open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止;
//如果open调用是非阻塞的,open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开。

编程时,在一个进程中创建有名管道,然后所有进程都可以open()这个管道进行读写;
示例程序看这篇:【Linux】应用篇十一–进程间的通信

2.3.2 共享内存

在C的内存分区中,0-3G是用户空间,每个进程对应的物理内存都不一样,3-4G的空间是内核空间,虽然每个进程都有自己的虚拟地址空间,但对应到物理内存,内核空间只有一份;
共享内存原理大白话解释
在物理内存的内核空间找一块指定大小的空间,把这块空间的首地址映射到每个进程里明确知道的一个地址,那么每个进程都可以直接用这个映射来的地址来操作物理内存上的这个空间,这样的话不同进程借助这个独一的空间,就实现了通信;只有一点小问题,每个进程都能直接访问到这段空间,都是直接用指针访问,那必然出现同步问题,因此在使用共享内存时要借助其他工具来实现同步。
在这里插入图片描述
书面说法:
共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝;
为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间;
进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率;
由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。

这里直接来看systemV 的IPC对象中的共享内存:
在这里插入图片描述
步骤:

  1. 创建/打开共享内存
  2. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
  3. 读写内存空间
  4. 撤销共享内存映射
  5. 删除共享内存对象

下面是编程接口:

1. 创建/打开共享内存

#include<sys/shm.h>
int shmget(key_t key, int size, int shmflg);
  • 系统用key为共享内存空间命名不同进程通过相同的key可以得到相同的IPC编号,就能申请到相同的内存空间;除此之外,还可以取IPC_PRIVATE,这种只能用于父子进程通信,因为使用IPC_PRIVATE创建的IPC对象, key值属性为0,和IPC对象的编号就没有了对应关系。则毫无关系的进程,就不能通过key值来得到IPC对象的编号(因为这种方式创建的IPC对象的key值都是0);
  • size为申请空间的大小;
  • flag为所需要的操作和权限,可以用来创建一个共享存储空间并返回一个标识符或者获得一个共享标识符。
    (1) flag的值为IPC_CREAT:如果不存在key值的共享存储空间,且权限不为0,则创建共享存储空间,并返回一个共享存储标识符。如果存在,则直接返回共享存储标识符。
    (2)flag的值为 IPC_CREAT | IPC_EXCL:如果不存在key值的共享存储空间,且权限不为0,则创建共享存储空间,并返回一个共享存储标识符。如果存在,则产生错误。
    返回值:成功返回共享存储ID;出错返回-1
    例如:int id = shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);创建一个大小为4096个字节的权限为0666(所有用户可读可写,具体查询linux权限相关内容)的共享存储空间,并返回一个整形共享存储标识符,如果key值已经存在有共享存储空间了,则出错返回-1。

用ftok()创建一个key:
每一个共享存储段都有一个对应的键值(key)相关联(消息队列、信号量也同样需要)

所需头文件:#include<sys/ipc.h>
函数原型 :key_t ftok(const char *path ,int id);
  • path为一个已存在的路径名
  • id为0~255之间的一个数值,代表项目ID,自己取
  • 返回值:成功返回键值(相当于32位的int)。出错返回-1

2. 映射共享内存

#include<sys/shm.h>
void  *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid为shmget生成的共享存储标识符
  • addr指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
  • flag为对数据的操作,如果指定为SHM_RDONLY则以只读方式连接此段,其他值为读写方式连接此段。
  • 返回值:成功返回指向共享存储段的指针;错误返回-1(打印出指针的值为全F)

注:
第二个参数一般写NULL,表示自动映射;
第三参数一般写0 ,表示可读写;

3. 读写内存空间

4. 撤销共享内存映射

#include<sys/shm.h>
int shmdt(const void *addr);
  • addr为shmat函数返回的地址指针
  • 返回值:成功返回0;错误返回-1

5. 删除共享内存对象

#include<sys/shm.h>
int  shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid就是shmget函数返回的共享存储标识符
  • cmd有三个
    (1) 常用删除共享内存的为IPC_RMID;
    (2) IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中;
    (3) IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内。(内核为每个共享存储段维护着一个结构,结构名为shmid_ds,这里就不讲啦,里面存放着共享内存的大小,pid,存放时间等一些参数)
  • buf就是结构体shmid_ds
  • 返回值:成功返回0;错误返回-1

例如:shmctl(shmid, IPC_RMID, NULL);删除共享内存;

代码参考:
Linux共享内存
【Linux】应用篇十二–共享内存,建议看这个,这个里面的代码简单

2.3.3 信号灯/信号量

信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制。
信号灯种类: (1)posix有名信号灯 (2)posix基于内存的信号灯(无名信号灯) (3)System V信号灯(IPC对象)

这里说的是System V IPC的信号灯;其特点为:

  • System V 信号灯是一个或多个计数信号灯的集合
  • 可同时操作集合中的多个信号灯
  • 申请多个资源时避免死锁
  • 由内核维护

与共享内存配合实现进程间的同步和互斥;
步骤:
(1)打开/创建信号灯集 : semget
(2)信号灯集初始化 : semctl
(3)P/V操作 : semop
(4)删除信号灯集 : semctl

(1)打开/创建信号灯 : semget

#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
  • 成功时返回信号灯的id,失败时返回-1
  • key:ftok产生的key值(和信号灯关联的key值)
  • nsems:信号灯集中包含的信号灯数目
  • semflg:信号灯集的访问权限,通常为IPC_CREAT |0666

(2)信号灯初始化 : semctl

#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd,);
  • 成功时返回0,失败时返回EOF
  • semid:信号灯集ID
  • semnum: 要操作的集合中的信号灯编号
  • cmd:
    • GETVAL:获取信号灯的值,返回值是获得值
    • SETVAL:设置信号灯的值,需要用到第四个参数:共用体
    • IPC_RMID:从系统中删除信号灯集合
union  semun  myun;
myun.val = 2;
if (semctl(semid, 0, SETVAL, myun) < 0) 
{
	perror(“semctl”);     
	exit(-1);
}
myun.val = 0;
if (semctl(semid, 1, SETVAL, myun) < 0) 
{
	perror(“semctl”);     
	exit(-1);
}

(3)P/V操作 : semop

#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
  • 成功时返回0,失败时返回-1
  • semid:信号灯集ID
  • sops : 描述对信号灯操作的结构体(数组)
  • nops: 要操作的信号灯的个数 ,1个
struct  sembuf 
 {
     short  sem_num;
     short  sem_op;
     short  sem_flg;
 };
  • semnum : 信号灯编号
  • sem_op : -1-P操作; 1-V操作
  • sem_flg : 0 / IPC_NOWAIT

(4)删除信号灯 : semctl

semctl(int semid, int semnum, IPC_RMID);

代码请看:【Linux】应用篇十四–消息队列与信号灯

2.3.4 消息队列

消息队列,Unix的通信机制之一,可以理解为是一个存放消息(数据)容器。将消息写入消息队列,然后再从消息队列中取消息,一般来说是先进先出的顺序。可以解决两个进程的读写速度不同(处理数据速度不同),系统耦合等问题,而且消息队列里的消息哪怕进程崩溃了也不会消失;

  • 消息队列由消息队列ID来唯一标识
  • 消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等
  • 消息队列可以按照类型来发送/接收消息;

流程:
(1) ftok函数生成键值
(2) msgget函数创建消息队列
(3) msgsnd函数往消息队列发送消息
(4) msgrcv函数从消息队列读取消息
(5) msgctl函数进行删除消息队列

流程:
(1) ftok函数生成键值
(2) msgget函数创建消息队列

#include<sys/msg.h>
int msgget(key_t key,int flag);
  • key为ftok生成的键值
  • flag为所需要的操作和权限,可以用来控制创建一个消息队列。
    (1) flag的值为IPC_CREAT:如果不存在key值的消息队列,且权限不为0,则创建消息队列,并返回一个消息队列ID。如果存在,则直接返回消息队列ID。
    (2)flag的值为 IPC_CREAT | IPC_EXCL:如果不存在key值的消息队列,且权限不为0,则创建消息队列,并返回一个消息队列ID。如果存在,则产生错误。
    (3)要用文件权限来或,比如IPC_CREAT|666;
  • 返回值:成功返回消息队列ID;出错返回-1

(3) msgsnd函数往消息队列发送消息
消息结构体:

struct mymesg
{
	long int mtype;	//类,消息队列可以控制读取相应类型的数据,这时就不一定是先进先出的顺序了,文章后面会继续介绍
	char mtext[size_t];	//数据,传递的数据存放在这里面
};
#include<sys/msg.h>
int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag);
  • msgid:为msgget返回的消息队列ID值
  • ptr:为消息结构体mymesg指针
  • nbytes:为消息结构体mymesg里的字符数组mtext大小,sizeof(mtext)
  • flag:值可以为0、IPC_NOWAIT
    (1)为0时,当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列或者消息队列被删除。
    (2)为IPC_NOWAIT时,当消息队列满了,msgsnd函数将不会等待,会立即出错返回EAGAIN
  • 返回值:成功返回0;错误返回-1

(4) msgrcv函数从消息队列读取消息
消息结构体:

struct mymesg
{
	long int mtype;	//类,消息队列可以控制读取相应类型的数据,这时就不一定是先进先出的顺序了,文章后面会继续介绍
	char mtext[size_t];	//数据,传递的数据存放在这里面
};
#include<sys/msg.h>
ssize_t msgrcv(int msgid,void *ptr,size_t nbytes,long type,int flag);
  • msgid:为msgget返回的消息队列ID值
  • ptr:为消息结构体mymesg指针
  • nbytes:为消息结构体mymesg里的字符数组mtext大小,sizeof(mtext)
  • type:在结构体mymesg里我们定义了一个long int mtype,用于分别消息的类型
    • type ==0 返回队列中的第一个消息
    • type > 0 返回队列中消息类型为type的第一个消息
    • type < 0 返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息
  • flag:可以为0、IPC_NOWAIT、IPC_EXCEPT
    • 为0时,阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
    • 为IPC_NOWAIT时,如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
    • 为IPC_EXCEPT时,与msgtype配合使用返回队列中第一个类型不为msgtype的消息
  • 返回值:成功返回消息数据部分的长度;错误返回-1

(5) msgctl函数进行删除消息队列

#include<sys/msg.h>
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
  • msgid就是msgget函数返回的消息队列ID
  • cmd有三个
    • 常用删除消息队列的为IPC_RMID;
    • IPC_STAT:取此队列的msqid_ds结构,并将它存放在buf指向的结构中;
    • IPC_SET:改变消息队列的状态,把buf所指的msqid_ds结构中的uid、gid、mode复制到消息队列的msqid_ds结构内。
  • buf就是结构体msqid_ds
  • 返回值:成功返回0;错误返回-1

代码示例参考【Linux】应用篇十四–消息队列与信号灯

2.3.5 信号

  • 中断:
    在cpu处理程序的过程中插入另外一段程序的执行过程
  • 异步通信:
    被通信方不知道通信方何时跟它通信
  • 同步通信:
    发送方发送消息,接收方接收消息,而且需要限制在一定的时间内完成,否则会造成另外一方阻塞。
2.3.5.1 什么是信号

信号是对中断的模拟,信号通信是一种异步的通信方式(随机性);
信号的特点:信号只能由内核产生;

linux支持的信号,可以用kill -l命令查看;
信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程
在这里插入图片描述

2.3.5.2 处理信号三要素

处理信号的三要素:
1、信号源;
2、处理信号的方式;
3、将信号源和处理方式进行绑定;

2.3.5.3 信号三种响应方式

忽略信号
对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
捕捉信号
定义信号处理函数,当信号发生时,执行相应的处理函数。
执行缺省操作
Linux对每种信号都规定了默认操作

2.3.5.4 信号的产生
  • 按键产生
  • 系统调用函数产生(比如raise, kill)
  • 硬件异常
  • 命令行产生 (kill)
  • 软件条件(比如被0除,访问非法内存等)
2.3.5.5 编程接口

两个信号发送函数

#include  <unistd.h>
#include <signal.h>
int  kill(pid_t pid,  int sig);		//给任意进程发信号;
int  raise(int sig);       //给自己发信号;
  • 成功时返回0,失败时返回EOF(-1);
  • pid:
    • > 0: 发送信号给指定进程
    • = 0:发送信号给跟调用kill函数的那个进程的同组进程。
    • < -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
    • = -1:发送信号给,有权限发送的所有进程。
  • sig:待发送的信号类型

定时器/闹钟函数:
单次定时:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

功能:定时发送SIGALRM给当前进程
参数:seconds:定时秒数
返回值:完成发送返回0,如果上次还未发送信号则返回上次定时剩余时间。
循环计时:

#include <unistd.h>
useconds_t ualarm(useconds_t usecs, useconds_t interval);

功能:以useconds为单位,第一个参数为第一次产生时间,第二个参数为间隔产生;

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);

功能:定时的发送alarm信号

  • which:
    • ITIMER_REAL:以逝去时间递减。发送SIGALRM信号
    • ITIMER_VIRTUAL: 计算进程(用户模式)执行的时间。 发送SIGVTALRM信号
    • ITIMER_PROF: 进程在用户模式(即程序执行时)和核心模式(即进程调度用时)均计算时间。 发送SIGPROF信号
  • new_value:负责设定 timout 时间
  • old_value: 存放旧的timeout值,一般指定为NULL
struct itimerval 
{
	struct timeval it_interval;  // 闹钟触发周期
	struct timeval it_value;    // 闹钟触发时间
}struct timeval 
{
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
}
2.3.5.6 信号捕捉过程

定义新的信号的执行函数handle。
使用signal/sigaction函数,把自定义的handle和指定的信号相关联。

以下是signal函数原型,很好理解,signal是一个函数,参数是signum和一个函数指针,指向类型是void(函数名)(int);返回值类型是一个函数指针,指向类型是void(函数名)(int);没错,第二个参数和返回值类型一样;

void (*signal(int signum, void (*handler)(int)))(int);
  • 功能:捕捉信号执行自定义函数
  • 返回值:成功时返回原先的信号处理函数,失败时返回SIG_ERR

参数:

  • signo:要设置的信号类型
  • handler:指定的信号处理函数:
    • SIG_DFL代表缺省方式;
    • SIG_IGN 代表忽略信号;
      系统建议使用sigaction函数,因为signal在不同类unix系统的行为不完全一样。
在此我要痛批学习中的中间商赚差价行为:

先来看原型:
以下是signal函数原型,很好理解,signal是一个函数,参数是signum和一个函数指针,指向类型是void(函数名)(int);返回值类型是一个函数指针,指向类型是void(函数名)(int);没错,第二个参数和返回值类型一样;
就这么简单,就这么好理解;

void (*signal(int signum, void (*handler)(int)))(int);

再来看:
以下是大部分人认为的好理解的形式,也是老师讲课必用的形式。
第一句意思是sighandler_t变成了一个类型,用这个类型定义出来的是一个函数指针,指向void(函数名)(int);所以,第二句的定义就容易理解了;

typedef void (*sighandler_t)(int);
sighandler_t  signal(int signum, sighandler_t handler);

我要说的是
如果你能按原型理解,说明你掌握了函数与指针,函数指针和指针函数的含义和用法,那么你肯定能看懂所有C语言的函数原型,C语言的高级部分将不再是你的困惑,而是你的利器。
但是如果你按第二种来学习,很好,你掌握一种弯路学习法,你先要掌握typedef的你不熟悉的用法,然后再去理解一个你无法理解的函数,用这种方法可以让你会使用signal函数,但是你再回头去看原型,请问你理解原型了吗?你没有,那你大概率也还没搞懂函数指针和指针函数,后面再来更复杂的函数,你就又得去学习更多的弯路。
在我看来,这就是在学习中偷懒,主动把自己交给中间商待宰;
我不提倡;
我讨厌;
学习就应该直达目的地;
扯远了;

回到信号处理函数:

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
struct sigaction 
{
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}
  • signum:处理的信号
  • act,oldact: 处理信号的新行为和旧的行为,是一个sigaction结构体。
    sigaction结构体成员定义如下:
    • sa_handler: 是一个函数指针,其含义与 signal 函数中的信号处理函数类似
    • sa_sigaction: 另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。
    • sigset_t sa_mask;信号集,请看2.3.5.7;
    • sa_flags参考值如下:
      • SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
      • SA_RESTART:使被信号打断的系统调用自动重新发起。
      • SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
      • SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
  • re_restorer:是一个已经废弃的数据域;
    代码请看【Linux】应用篇十三–信号机制
2.3.5.7 信号集及信号的屏蔽

有时候不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数。这种情况可以通过阻塞信号实现。
信号的阻塞概念:信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

sigset_t set;  //自定义信号集。  
//是一个32bit  64bit  128bit的数组。
sigemptyset(sigset_t *set);	//清空信号集
sigfillset(sigset_t *set);	//全部置1
sigaddset(sigset_t *set, int signum); 
//将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); 
//将一个信号从集合中移除
sigismember(const sigset_t *set,int signum); 
//判断一个信号是否在集合中。

信号集内的信号的处理方式(阻塞或不阻塞):

#include <signal.h>
int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );
  • 返回值:若成功则返回0,若出错则返回-1
  • 若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
  • 若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
  • how可选用的值:(注意,不能阻塞SIGKILL和SIGSTOP信号
    • SIG_BLOCK : 把参数set中的信号添加到信号屏蔽字中
    • SIG_UNBLOCK: 从信号屏蔽字中删除参数set中的信号
    • SIG_SETMASK: 把信号屏蔽字设置为参数set中的信号
int pause(void);

进程一直阻塞,直到被信号中断,返回值:-1 并设置errno为EINTR;

  • 如果信号的默认处理动作是终止进程,则进程终止,pause函数没有机会返回。
  • 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回
  • 如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause返回-1。
  • pause收到的信号如果被屏蔽,那么pause就不能被唤醒
int sigsuspend(const sigset_t *sigmask);

功能:将进程的屏蔽字替换为由参数sigmask给出的信号集,然后挂起进程的执行;
参数:
sigmask:希望屏蔽的信号;

代码请看【Linux】应用篇十三–信号机制
这篇文章最后一个程序将会在while(1)中出不来,只能发kill -9 给进程强行终止;

2.3.6 通讯方式的比较

pipe: 具有亲缘关系的进程间,单工,数据在内存中
fifo: 可用于任意进程间,双工,有文件名,数据在内存
signal: 唯一的异步通信方式
msg:常用于cs模式中, 按消息类型访问 ,可有优先级
shm:效率最高(直接访问内存) ,需要同步、互斥机制
sem:配合共享内存使用,用以实现同步和互斥

3 GDB调试多进程

start: 单步运行。

set detach-on-fork on/off : 设置GDB跟踪调试单个进程或多个
on: 只调试父进程或子进程的其中一个,(根据follow-fork-mode来决定),这是默认的模式
off:父子进程都在gdb的控制之下,其中一个进程正常调试(根据follow-fork-mode来决定),另一个进程会被设置为暂停状态。

set follow-fork-mode child : 设置GDB调试子进程
set follow-fork-mode parent : 设置GDB调试父进程

info inferiors : 显示GDB调试的进程
inferiors : 进程序号(1,2,3…),切换GDB调试的进程;

参考资料来源

目前文章还有很多可以改进的地方,后期继续更新完善;
csdn上参考了:【超全面】Linux嵌入式干货学习系列教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值