走进 C/C++后台开发的第三步:Linux进程控制详解



2.1 Linux 进程概述


在早期的多机处理系统中, 并发执行程序的话(由于并发程序的速度不同与资源竞争导致了程序执行的间断性),存在数据计算结果的不可再现性,这样的程序失去了其意义。

后来呢为了使得程序能并发执行且有效地对并发程序进行控制与描述,引入了进程的概念。

进程是如何产生的?

在程序运行前, 操作系统会为之分配一个PCB(进程控制块:系统通过PCB对进程控制与描述), PCB与程序段和程序相关的数据一起构成了进程实体。

进程的实质就是进程实体的一次运行过程。

进程和程序本身的区别

程序是静态的,是保存在磁盘上的指令的有序集合,进程是一个动态的概念
它是一个运行着的程序,包括了进程的动态创建,调度和消亡的过程,是Linux的基本调度单位。

进程 process:是 os 的最小单元 ,os 会为每个进程分配大小为 4g 的虚拟内存空间,其中 1g 给内核空间, 3g 给用户空间{代码区 数据区 堆栈区}

操作系统和进程间的联系

  • 系统通过进程控制块来描述进程的动态变化,进程控制块有进程的基本信息,控制信息和资源信息。
  • 操作系统内核通过进程来控制对 CPU 和其他系统资源的访问,并决定进程的CPU 使用和运作时间。

Linux 内核通过一个被称为 进程描述符的 task_struct 结构体来管理进程,这个结构体包含一个进程所需的所有信息。

它定义在 \kernel\msm-4.4\include\linux\sched.h 文件中,

struct task_struct {
    
    
  进程描述信息
  进程标识符
  进程的用户标识符
  进程控制信息
   1.进程状态 2. 优先级
  文件和文件系统
  内存管理
  信号处理
}



2.1.1 进程的标识符

  • OS 会为每个进程分配一个唯一的整数 ID, 作为其标识号(pid)。
  • 进程描述符结构体也存放了其父进程的 ID(ppid)
  • 其次,所有的进程的祖先进程是同一个进程,叫做 Init 进程,ID 号为 1,Init 是内核自举后启动的一个进程,负责引导系统,启动守护进程并且允许必要的程序。

通过 标准 c 的函数可以获取当前进程的 pid 和 ppid

在这里插入图片描述



2.1.2 进程的用户 ID 和 组ID (进程的运行身份)

进程的用户ID 和 组ID

  • 进程的用户标识了其进程的权限控制, 默认情况下,谁启动了进程,该进程的身份就有该用户的身份
  • 使用 getuid() 和 getgid() 能得到进程的真实用户 ID 和真实组 ID。

进程的有效用户ID 和 有效用户组ID

  • 内核对进程用户执行的ID 进行检查时,检查的是其有效用户ID 和 有效用户组ID,默认情况下与真实用户ID 和 真实组ID 其是相等的

  • 使用 geteuid 和 getegid 能得到进程的有效用户ID 和 有效用户组ID

在这里插入图片描述

-改变有效用户ID 使得每个用户对文件的有效ID

chmod u+s myfife:为用户设置s权限,具有文件所有者的权限
chmod g+s Code:为用户组设置s权限,具有用户组的权限,可以对该目录下的文件执行权利,Code为目录
chmod o+t myfife:为其他用户设置t权限,说明其他用户不能对其进行删除操作

设置当前进程的有效用户 ID 和 真实用户ID

让进程获取到更强大的权限。

int setuid(uid_t uid);
int seteuid(uid_t euid);  

sudo 权限是改变的用户的真实id

2.1.3 进程的状态

在这里插入图片描述



2.1.4 Linux 下的进程结构

Linux 是多进程的系统,

  • 进程间有并行性和互不干扰性,进程间是各自分离的任务,每个进程拥有各自的权力和责任。

  • 每个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生了异常,它也不会影响到系统的其他进程。

在这里插入图片描述



2.1.5 进程相关命令

1. ps -elf 查看系统中的进程。ps 命令是一个采样的信息。

2. ps -aux 可以查看进程的CPU和内存的占用率。

3. echo $$打印当前bash的进程ID。

4. top命令   动态的显示进程的信息,
展示的是系统中CPU占用率最高的20个进程。

5. kill命令,给进程发信号,
使用方式:kill -信号的编号 进程ID。通过kill -l可以查看所有信号。

6. nice命令按照指定的优先级运行进程,
  renice命令可以修改进程的nice值,nice值的范围:-20-19。

   1. nice -n 可执行程序
   2. renice -n 指定的nice值 -p  进程ID


2.1.6 进程的调度策略

  • 先来先服务调度算法FCFS:队列实现,非抢占,先请求CPU的进程先分配到CPU,可以作为作业调度算法也可以作为进程调度算法;按作业或者进程到达的先后顺序依次调度,对于长作业比较有利;

  • 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;

  • 时间片轮转调度算法(可抢占的):按到达的先后对进程放入队列中,然后给队首进程分配CPU时间片,时间片用完之后计时器发出中断,暂停当前进程并将其放到队列尾部,循环 ;队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。

  • 最短作业优先调度算法SJF:作业调度算法,算法从就绪队列中选择估计时间最短的作业进行处理,直到得出结果或者无法继续执行,平均等待时间最短,但难以知道下一个CPU区间长度;缺点:不利于长作业;未考虑作业的重要性;运行时间是预估的,并不靠谱 ;

  • 高相应比算法HRN:响应比=(等待时间+要求服务时间)/要求服务时间;

  • 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。

  • 多级反馈队列调度算法:目前公认较好的调度算法;设置多个就绪队列并为每个队列设置不同的优先级,第一个队列优先级最高,其余依次递减。优先级越高的队列分配的时间片越短,进程到达之后按FCFS放入第一个队列,如果调度执行后没有完成,那么放到第二个队列尾部等待调度,如果第二次调度仍然没有完成,放入第三队列尾部…。只有当前一个队列为空的时候才会去调度下一个队列的进程。与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。



2.1.7 会话,进程组,前台进程和后台进程

1. 会话,控制终端,会话中的首进程是bash进程,一个会话下面可以有多个进程组。包括一个前台进程组和若干个后台进程组。

2. 前台进程组,可以接受控制终端上传输的数据。

3. 后台运行一个进程,在执行程序时后面加一个&符号,变成了后台进程。,
后台进程在会话结束后会自动结束。

4. 通过jobs命令可以看到当前会话下面的后台作业,每个作业都有一个编号。可以通过fg+作业编号把后台运行的作业拉回到前台。拉回到前台之后,就可以通过控制终端跟前台进程交互。

在这里插入图片描述




2.2 进程的创建


2.2.1 fork 函数

#include <unistd.h>
pid_t fork(void);

在Linux 中 fork 函数实现了创建一个新进程的效果

  • 新进程是子进程, 原进程是父进程。
  • 执行一次fork返回两个值,因为 fork 实现了进程的分叉复制,如下图:
    原进程调用 fork 后,会从该处复制出一个新进程,新进程与原进程的执行代码是一样的,但fork的返回值有所不同。
  • 父进程的返回值是子进程的 pid, 子进程的返回值是 0, 出错则返回 -1

在这里插入图片描述

fork 函数创建子进程的过程为:

  • 使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程继
    承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,
  • 子进程所独有的只有它的进程号、资源使用和计时器等。
  • 通过这种复制方式创建出子进程后,原有进程和子进程都从函数 fork 返回,各自继续往下运行,但是原进程的 fork 返回值与子进程的 fork 返回值不同,在原进程中,fork 返回子进程的 pid,而在子进程中,fork 返回 0,如果 fork 返回负值,表示创建子进程失败。(vfork 函数)

写时复制的技术(Cow)

在这里插入图片描述

父子进程的堆栈内存变量在双方都不进行修改的情况下是指向同一虚拟地址的,也可以说目前变量归父子进程共享, 仅在修改时才会产生新的地址。

  • fork 对文件操作采用的是 dup 机制,两个文件描述符指向同一个文件对象

在这里插入图片描述



2.2.2 exec 函数族

int execl(const char *path, const char *arg, ...)

使用 add.exe 直接覆盖掉当前进程
execl(./add.exe” ,”add.exe” ,3,4, NULL):

exec 的工作原理与 fork 完全不同, fork 是在复制一份原进程,而 exec 函数是用 exec 的第一个参数指定的程序覆盖现有进程空间(也就是说执行 exec 族函数之后,它后面的所有代码不在执行)。

  • path 是包括执行文件名的全路径名
  • arg 是可执行文件的命令行参数,多个用,分割注意最后一个参数必须为 NULL。

我们输入的 bash命令可以这样理解:

bash父进程 -> fork 分割出子进程 , 
execl 将子进程转到命令为(例如ls)的进程


2.2.3 system 函数

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

在这里插入图片描述

  • system 函数通过调用 shell 程序/bin/sh –c 来执行 string 所指定的命令,该函数在内部是通过调用execve(“/bin/sh”,…)函数来实现的。

  • 通过 system 创建子进程后,原进程和子进程各自运行,相互间关联较少。如果 system 调用成功,将返回 0。

  • system函数的参数还可以是一个可执行程序,例如:
    system(“/home/wangxiao/1”);如果想要执行system后面进程的时候,不至于对当前进程进行阻塞,可以利用&将/home/wangxiao/1调到后台运行。



2.2.4 进程的控制和终止

进程的终止

进程的终止有 5 种方式:
⚫ main 函数的自然返回;
⚫ 调用 exit 函数
⚫ 调用_exit 函数
⚫ 调用 abort 函数
⚫ 接收到能导致进程终止的信号 ctrl+c SIGINT ctrl+\ SIGQUIT

exit() 与 _exit() 的区别:

在这里插入图片描述

进程的控制

孤儿进程:

用 fork 函数启动一个子进程时,子进程就有了它自己的生命并将独立运行。

如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被 PID 为 1 的进程
(即 init)接管。孤儿进程退出后,它的清理工作有祖先进程 init 自动处理。但在 init 进程,清理子进程之前,它一直消耗系统的资源,所以要尽量避免。

僵尸进程:

如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用 wait 或 waitpid 函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸(zombie)进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。

 #include <sys/types.h>
 #include <sys/wait.h>
 pid_t wait(int *status);
 pid_t waitpid(pid_t pid, int *status, int options);

wait 和 waitpid 都将暂停父进程,等待一个已经退出的子进程,并进行清理工作;
wait 函数随机地等待一个已经退出的子进程,并返回该子进程的 pid;
waitpid 等待指定 pid 的子进程;如果为-1 表示等待所有子进程。
status 参数是传出参数,存放子进程的退出状态;通常用下面的两个宏来获取状态信息:
WIFEXITED(status) 如果子进程正常结束,它就取一个非 0 值。传入整型值,
非地址
WEXITSTATUS(status) 如果 WIFEXITED 非零,它返回子进程的退出码,在bash进程下使用
echo $?  能获取上一次等待的进程的退出码。

options 用于改变 waitpid 的行为,其中最常用的是 WNOHANG,它表示无论子进程是
否退出都将立即返回,不会将调用者的执行挂起

守护进程:

守护进程是一种后台服务进程,但不是后台进程(在会话关闭时会被结束)。

  • 守护进程是后台运行,并且独立于 bash 终端和当前会话的控制, 关闭会话守护进程不会被结束
  • 跟先前运行环境已经彻底隔离开,不属于任何用户,并且未使用文件描述符

创建守护进程:

  1. 创建子进程,父进程退出
  2. 在子进程中创建新会话 setsid()
  3. 把当前工作目录切换到跟目录,chdir("")
  4. 重新设置文件掩码 umask(0)
  5. 关闭不需要的文件描述符

在这里插入图片描述

2.3 进程间通信 (Internet Process Connection)


查看和删除System V进程间通信信息的命令

在这里插入图片描述

  • 利用 ipcs 查看 已经创建的消息队列,共享内存和信号量组信息
  • 利用 ipcrm 来删除其创建的进程间通信的信息
    1. ipcrm -q msqid : 删除消息队列
    2. ipcrm -m shmid : 删除共享内存
    3. ipcrm -s semid: 删除信号量组


2.3.1 管道

定义: 连接一个读进程与一个写进程以实现它们之间通信的一个共享文件(pipe文件)。

  • 向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;
  • 而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据,发送进程于接收进程都是利用管道进行通信的,故称为 管道通信。

无名管道(PIPE)

管道是 linux 进程间通信的一种方式,如命令 ps -ef | grep ntp

无名管道的特点:

  • 只能在亲缘关系进程间通信(父子或兄弟)
  • 半双工(固定的读端和固定的写端)
  • 他是特殊的文件,可以用 read、write 等,只能在内存中
#include <unistd.h>
int pipe(int fds[2]);
  • 管道在程序中用一对文件描述符表示,其中一个文件描述符有可读属性,一个有可写的属性。fds[0]是读,fds[1]是写
  • 函数 pipe 用于创建一个无名管道,如果成功,fds[0]存放可读的文件描述符,fds[1]存放可写文件描述符,并且函数返回 0,否则返回-1。
  • 通过调用 pipe 获取这对打开的文件描述符后,一个进程就可以从 fds[0]中读数据,而另一个进程就可以往 fds[1]中写数据。当然两进程间必须有继承关系,才能继承这对打开的文件描述符。
  • 管道不象真正的物理文件,不是持久的,即两进程终止后,管道也自动消失了。
示例:创建父子进程,创建无名管道,父写子读
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
    
    
  int fds[2] = {
    
    0};
  pipe(fds);
  char szBuf[32] = {
    
    '\0'};
  if(fork() == 0){
    
     //表示子进程
    close(fds[1]); //子进程关闭写
    sleep(2); //确保父进程有时间关闭读,并且往管道中写内容
    if(read(fds[0], szBuf, sizeof(szBuf)) > 0)
      puts(buf);
    close(fds[0]); //子关闭读
    exit(0);
  }else{
    
     //表示父进程
    close(fds[0]); //父关闭读
    write(fds[1], "hello", 6);
    waitpid(-1, NULL, 0); //等子关闭读
//write(fds[1], "world",6); //此时将会出现“断开的管道”因为子的读已经关闭close(fds[1]); //父关闭写
    exit(0);
  }
  return 0;
}

管道关闭的问题

管道两端的关闭是有先后顺序的,

  • 如果先关闭写端则从另一端读数据时,read 函数将返回 0,表示管道已经关闭

  • 先关闭读端,则从另一端写数据时,将会使写数据的进程接收到 SIGPIPE 信号,如果写进程不对该信号进行处理,将导致写进程终止,如果写进程处理了该信号,则写数据的 write 函数返回一个负值,表示管道已经关闭。

命名管道(FIFO)

无名管道只能在亲缘关系的进程间通信大大限制了管道的使用,有名管道突
破了这个限制,通过指定路径名的范式实现不相关进程间的通信。

  • 保存在磁盘上的磁盘文件
  • 可以用于任何相关或不想关进程间的通信
  • 半双工

创建、删除 FIFO 文件

创建 FIFO 文件与创建普通文件很类似,只是创建后的文件用于 FIFO。

1. 用函数创建和删除 FIFO 文件
·创建 FIFO 文件的函数原型:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

参数 pathname 为要创建的 FIFO 文件的全路径名;
参数 mode 为文件访问权限
如果创建成功,则返回 0,否则-1。

2. 删除FIFO文件的函数原型为:

也可用于删除文件

#include <unistd.h>
int unlink(const char *pathname);

3. 用命令创建和删除FIFO文件

  • 用命令mkfifo创建 不能重复创建
  • 用命令unlink删除
    创建完毕之后,就可以访问FIFO文件了:
    一个终端:cat < myfifo
    另一个终端:echo “hello” > myfifo



2.3.2 共享内存机制

System V 共享内存机制: shmget shmat shmdt shmctl

原理及实现

  • 特殊的内存区段,将进程间共享的数据放于该区域中,所有需要访问该共享区域的进程都将共享区域映射到本进程的地址空间中。
  • 使用共享内存的进程可以将信息写入该空间,另一个使用共享内存的进程又可以其内存操作获取到其信息,这样就实现了进程间通信
  • 允许多进程间相互进行通信,进程对象对于共享内存的访问通过 key(键:共同决定的共享内存标识)来控制,同时通过 key 进行访问权限的检查。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
key_t ftok(const char *pathname, int proj_id);
int shmget(key_t key, int size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

函数 ftok 用于创建一个关键字,可以用该关键字关联一个共享内存段

参数 pathname 为一个全路径文件名,并且该文件必须可访问。
参数 proj_id 通常传入一非 0 字符
通过 pathname 和 proj_id 组合可以创建唯一的 key
如果调用成功,返回一关键字,否则返回-1


函数 shmget 用于创建或打开一共享内存段,该内存段由函数的第一个参数唯一创
。函数成功,则返回一个唯一的共享内存标识号(相当于进程号,唯一的标识着共享内存),失败返回-1。

参数 key 是一个与共享内存段相关联关键字,如果事先已经存在一个与指定关键字
关联的共享内存段,则直接返回该内存段的标识,表示打开,如果不存在,则创建一个新的共享内存段。key 的值既可以用 ftok 函数产生,也可以是 IPC_PRIVATE(用于创建一个只属于创建进程的共享内存,主要用于父子通信),表示总是创建新的共享内存段;
参数 size 指定共享内存段的大小,以字节为单位;

参数 shmflg 是一掩码合成值,可以是访问权限值与(IPC_CREAT 或 IPC_EXCL)的
合成。IPC_CREAT 表示如果不存在该内存段,则创建它。IPC_EXCL 表示如果该内存
段存在,则函数返回失败结果(-1)。如果调用成功,返回内存段标识,否则返回-1


函数 shmat 将共享内存段映射到进程空间的某一地址。

参数 shmid 是共享内存段的标识 通常应该是 shmget 的成功返回值
参数 shmaddr 指定的是共享内存连接到当前进程中的地址位置。通常是 NULL,表
示让系统来选择共享内存出现的地址。
参数 shmflg 是一组位标识,通常为 0 即可。
如果调用成功,返回映射后的进程空间的首地址,否则返回(char *)-1。


函数 shmdt 用于将共享内存段与进程空间分离。

参数 shmaddr 通常为 shmat 的成功返回值。
函数成功返回 0,失败时返回-1.注意,将共享内存分离并没删除它,只是使得该共
享内存对当前进程不在可用。


函数 shmctl 是共享内存的控制函数,可以用来删除共享内存段。

参数 shmid 是共享内存段标识 通常应该是 shmget 的成功返回值
参数 cmd 是对共享内存段的操作方式,可选为 IPC_STAT,IPC_SET,IPC_RMID。通
常为 IPC_RMID,表示删除共享内存段。
参数 buf 是表示共享内存段的信息结构体数据,通常为 NULL。
有进程连接,执行返回 0,标记删除成功,但是最后一个进程解除连接后,共享内
存真正被删除

在这里插入图片描述


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h> //头文件包含
#include <sys/types.h>
main() {
    
    
  key_t key = ftok("b.dat", 1); //1. 写入端先用 ftok 函数获得 key
  if (key == -1) {
    
    
    perror("ftok");
    exit(-1);
  }
  int shmid = shmget(key, 4096, IPC_CREAT); //2. 写入端用 shmget 函数创建一共享内存段
  if (shmid == -1) {
    
    
    perror("shmget");
    exit(-1);
  }
  char *pMap = (char *) shmat(shmid, NULL, 0); //3. 获得共享内存段的首地址
  memset(pMap, 0, 4096);
  strcpy(pMap, "hello world"); //4. 往共享内存段中写入内容
  if (shmdt(pMap) == -1) //5. 关闭共享内存段
  {
    
    
    perror("shmdt");
    exit(-1);
  }
}
读内存端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
main() {
    
    
  key_t key = ftok("b.dat", 1); //1. 读入端用 ftok 函数获得 key
  if (key == -1) {
    
    
    perror("ftok");
    exit(-1);
  }
  int shmid = shmget(key, 4096, 0600 | IPC_CREAT); //2. 读入端用 shmget 函数打开共享内存段
  if (shmid == -1) {
    
    
    perror("shmget");
    exit(-1);
  }
  char *pMap = (char *) shmat(shmid, NULL, 0); //3. 获得共享内存段的首地址
  printf("receive the data:%s\n", pMap); //4. 读取共享内存段中的内容
  if (shmctl(shmid, IPC_RMID, 0) == -1) //5. 删除共享内存段
  {
    
    
    perror("shmctl");
    exit(-1);
  }
}


2.3.3 信号量


概述:

信号量(Semaphore),有时被称为信号灯,是在多线程环境下/多进程下使用的一种设施,其值是可以用来保证共享资源被并发调用的次数

仅通过两个标准的原子操作(Atomic Operator)wait(S)和signal(S)来访问。

  • 访问资源时会使用 wait(s) 探测该信号量是否大于 0,大于则可用,将 信号量减1,代表获取到了该资源,等于0 ,则进程进入阻塞状态,等待资源的可用
  • 当使用完资源后,调用 signal(s), 将信号量加1,代表释放了该资源,通知其余进程该资源可使用。

在Linux操作系统下常用System V 信号量,在内核中维护,可用于进程或线程间的同步,常用于进程的同步。


SystemIPC

函数原型:

#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
int semget(key_t key,int nsems,int flag);
int semop(int semid,struct sembuf *sops,size_t num_sops);
int semctl(int semid, int semnum, int cmd,);
  • 函数 semget:

创建一个信号量集或访问一个已存在的信号量集。

返回值:成功时,返回一个称为信号量标识符的整数,semop 和 semctl 会使用它;出错时,返回-1.

参数 key 是唯一标识一个信号量的关键字,如果为 IPC_PRIVATE(值为 0,创建一
个只有创建者进程才可以访问的信号量,通常用于父子进程之间;非 0 值的 key(可以通
过 ftok 函数获得)表示创建一个可以被多个进程共享的信号量;

参数 nsems 指定需要使用的信号量数目。如果是创建新集合,则必须制定 nsems。
如果引用一个现存的集合,则将 nsems 指定为 0.

参数 flag 是一组标志,其作用与 open 函数的各种标志很相似。它低端的九个位是
该信号量的权限,其作用相当于文件的访问权限。此外,它们还可以与键值 IPC_CREAT
按位或操作,以创建一个新的信号量。即使在设置了 IPC_CREAT 标志后给出的是一个
现有的信号量的键字,也并不是一个错误。我们也可以通过 IPC_CREAT 和 IPC_EXCL
标志的联合使用确保自己将创建出一个新的独一无二的信号量来,如果该信号量已经存
在,就会返回一个错误。

  • 函数 semop

用于改变信号量对象中各个信号量的状态。
返回值:成功时,返回 0;失败时,返回-1.

参数 semid 是由 semget 返回的信号量标识符。

参数 sops 是指向一个结构体数组的指针。每个数组元素至少包含以下几
个成员:

struct sembuf{
    
    
    short sem_num; //操作信号量在信号量集合中的编号,第一个信
号量的编号是 0short sem_op; //sem_op 成员的值是信号量在一次操作中需要改变的
数值。通常只会用到两个值,一个是-1,也就是 p 操作,它等待信号量变为可用;一个
是+1,也就是 v 操作,它发送信号通知信号量现在可用。
     short sem_flg; //通常设为:SEM_UNDO,程序结束,信号量为 semop
                     调用前的值。
};

参数 nops 为 sops 指向的 sembuf 结构体数组的大小。

  • 函数 semctl

用来直接控制信号量信息。

函数返回值:成功时,返回 0;失败时,返回-1.

参数 semid 是由 semget 返回的信号量标识符。

参数 semnum 为集合中信号量的编号,当要用到成组的信号量时,从 0 开始。一
般取值为 0,表示这是第一个也是唯一的一个信号量。

参数 cmd 为执行的操作。通常为:

IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)
GETVAL(根据 semun 返回信号量的值,从 0 开始,第一个信号量编号为 0)
SETVAL(根据 semun 设定信号的值,从 0 开始,第一个信号量编号为 0)
GETALL(获取所有信号量的值,第二个参数为 0,将所有信号的值存入 semun.array中)
SETALL(将所有 semun.array 的值设定到信号集中,第二个参数为 0)等。

参数…是一个 union semun(需要由程序员自己定义),它至少包含以下几个成员:

union semun{
    
    
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
};
通常情况仅使用 val,给 val 赋值为 1
The semid_ds data structure is defined in <sys/sem.h> as follows:
 struct semid_ds {
    
    
 struct ipc_perm sem_perm; /* Ownership and permissions */
 time_t sem_otime; /* Last semop time */
 time_t sem_ctime; /* Last change time */
 unsigned long sem_nsems; /* No. of semaphores in set */
 };

 The ipc_perm structure is defined as follows (the highlighted fields are
 settable using IPC_SET):
 struct ipc_perm {
    
    
 key_t __key; /* Key supplied to semget(2) */
 uid_t uid; /* Effective UID of owner */
 gid_t gid; /* Effective GID of owner */
 uid_t cuid; /* Effective UID of creator */
 gid_t cgid; /* Effective GID of creator */
 unsigned short mode; /* Permissions */
 unsigned short __seq; /* Sequence number */
 }

2.3.4 消息队列

一个队列结构,存放了若干消息,非流式且消息间有间隔的通信方式,最常用的是消息缓冲队列,设定了缓冲区,并在进程PCB增加消息队列队首指针,用于对消息队列进行操作。


System V IPC 机制:消息队列

#include <sys/types.h>
 #include <sys/ipc.h>
 #include <sys/msg.h>
int msgget(key_t key, int msgflg);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 函数 msgget:
int msgget(key_t key, int msgflg);

创建和访问一个消息队列。
该函数成功则返回一个唯一的消息队列标识符(类似于进程 ID 一样),失败则返回-1.

参数 key 是唯一标识一个消息队列的关键字,如果为 IPC_PRIVATE(值为 0),用创
建一个只有创建者进程才可以访问的消息队列,可以用于父子间通信;非 0 值的 key(可
以通过 ftok 函数获得)表示创建一个可以被多个进程共享的消息队列;

参数 msgflg 指明队列的访问权限和创建标志,创建标志的可选值为 IPC_CREAT
和 IPC_EXCL 如果单独指定 IPC_CREAT,msgget 要么返回新创建的消息队列 id,要么返
回具有相同 key 值的消息队列 id;如果 IPC_EXCL 和 IPC_CREAT 同时指明,则要么创
建新的消息队列,要么当队列存在时,调用失败并返回-1。

  • 函数 msgsnd 和 msgrcv
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
 int msgflg);

用来将消息添加到消息队列中和从一个消息队列中获取信息。

参数 msgid 指明消息队列的 ID; 通常是 msgget 函数成功的返回值。

参数 msgbuf 是消息结构体,它的长度必须小于系统规定的上限,必须以一个长整
型成员变量开始,接收函数将用这个成员变量来确定消息的类型。必须重写这个结构体,
其中第一个参数不能改,其他自定义。如下:

struct msgbuf {
    
    
   long mtype; /* type of message */
   char mtext[1]; /* message text */
 };

字段 mtype 是用户自己指定的消息类型(必须是正整数),该结构体第 2 个成员
仅仅是一种说明性的结构,实际上用户可以使用任何类型的数据,就是消息内容;

参数 msgsz 是消息体的大小,每个消息体最大不要超过 4K;

参数 msgflg 可以为 0(通常为 0)或 IPC_NOWAIT,如果设置 IPC_NOWAIT,则
msgsnd 和 msgrcv 都不会阻塞,此时如果队列满并调用 msgsnd 或队列空时调用 msgrcv将返回错误;

参数 msgtyp 有 3 种选项:
msgtyp == 0 接收队列中的第 1 个消息(通常为 0)
msgtyp > 0 接收对列中的第 1 个类型等于 msgtyp 的消息
msgtyp < 0 接收其类型小于或等于 msgtyp 绝对值的第 1 个最低类型消

  • 函数 msgctl
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

是消息队列的控制函数,常用来删除消息队列。
参数 msqid 是由 msgget 返回的消息队列标识符。
参数 cmd 通常为 IPC_RMID 表示删除消息队列。
参数 buf 通常为 NULL 即可。



2.3.5 信号

概念:

  • 信号是进程在运行过程中,由自身产生或由进程外部发过来的消息(事件)。信号是硬件中断的软件模拟(软中断)。

  • 每个信号用一个整型常量宏表示,以 SIG 开头,它们在系统头文件<signal.h>中定义,也可以通过在 shell 下键入 kill –l 查看信号列表,或者键入 man 7 signal 查看更详细的说明。

信号的生成

信号的生成来自内核,让内核生成信号的请求来自 3 个地方:

  • ⚫用户:用户能够通过输入 CTRL+c、Ctrl+\,或者是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号;
  • ⚫ 内核:当进程执行出错时,内核会给进程发送一个信号,例如非法段存取(内存访问违规)、浮点数溢出等;
  • ⚫ 进程:一个进程可以通过系统调用 kill 给另一个进程发送信号,一个进程可以通过信号和另外一个进程进行通信。

进程的某个操作产生的信号称为同步信号(synchronous signals),例如除 0;
由像用户击键这样的进程外部事件产生的信号叫做异步信号(asynchronous signals)。

进程对于信号的处理

⚫ 接收默认处理:接收默认处理的进程通常会导致进程本身消亡。例如连接到终端的进程,用户按下 CTRL+c,将导致内核向进程发送一个 SIGINT 的信号,进程如果不对该信号做特殊的处理,系统将采用默认的方式处理该信号,即终止进程的执行; signal(SIGINT,SIG_DFL);

⚫ 忽略信号:进程可以通过代码,显示地忽略某个信号的处理,例如:signal(SIGINT,SIG_IGN);但是某些信号是不能被忽略的,例如 9 号信号;

⚫ 捕捉信号并处理:进程可以事先注册信号处理函数,当接收到信号时,由信号处理函数自动捕捉并且处理信号。

有两个信号既不能被忽略也不能被捕捉,它们是 SIGKILL 和 SIGSTOP。即进程接收到这两个信号后,只能接受系统的默认处理,即终止进程。SIGSTOP 是暂停进程。



signal 信号处理机制

可以用函数 signal 注册一个信号捕捉函数。原型为:

#include <signal.h>
typedef void (*sighandler_t)(int); //函数指针
sighandler_t signal(int signum, sighandler_t handler);
  • signal 的第 1 个参数 signum 表示要捕捉的信号,第 2 个参数是个函数指针,表示要对该信号进行捕捉的函数,该参数也可以是 SIG_DFL(表示交由系统缺省处理,相当于白注册了)或 SIG_IGN(表示忽略掉该信号而不做任何处理)。

  • signal 如果调用成功,返回以前该信号的处理函数的地址,否则返回 SIG_ERR。

  • sighandler_t 是信号捕捉函数,由 signal 函数注册,注册以后,在整个进程运行过程中均有效,并且对不同的信号可以注册同一个信号捕捉函数。该函数只有一个整型参数,表示信号值。


信号处理情况分析

信号处理情况分析 在 signal 处理机制下,还有许多特殊情况需要考虑:

1、 注册一个信号处理函数,并且处理完毕一个信号之后,是否需要重新注册,才能够捕捉下一个信号;(不需要)
2、 如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个同类型的信号,这时该怎么处理;(挨着执行),后续相同信号忽略(会多执行一次)。
3、 如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个不同类型的信号,这时该怎么处理;(跳转去执行另一个信号,之后再执行剩下的没有处理完的信号

4、 如果程序阻塞在一个系统调用(如 read(…))时,发生了一个信号,这时是让系统调用返回错误再接着进入信号处理函数,还是先跳转到信号处理函数,等信号处理完毕后,系统调用再返回。




猜你喜欢

转载自blog.csdn.net/chongzi_daima/article/details/108202626
今日推荐