杨记

碎片化学习令人焦虑,系统化学习使人进步

0%

进程和进程通信

学自电子科技大学和黑马程序员,以下是一些补充内容和参考链接

C 语言中 void* 详解及应用 | 菜鸟教程 (runoob.com)

https://www.runoob.com/w3cnote/c-general-function.html

void 是什么?_杨博东的博客的博客-CSDN博客_void是什么

find /usr/include/ -name *.h | xargs grep 'MRT6_INIT'

IPC之信号量详解_Qiuoooooo的博客-CSDN博客_ipc信号量

IPC之信号量_车小猿的博客-CSDN博客_ipc信号量

相关概念

程序和进程

程序:是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁….)

进程:是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。当程序被操作系统装载到内存并分配给它一定资源后,此时可称为进程。

程序是静态概念,进程是动态概念。

虚拟内存

操作系统会为每个进程分配和维护虚拟内存地址

image-20220527165812134

cpu 为什么要使用虚拟地址空间与物理地址空间映射?解决了什么样的问题?

  1. 方便编译器和操作系统安排程序的地址分布。程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
  2. 方便进程之间隔离。不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程使用的物理内存。
  3. 方便OS使用你那可怜的内存。程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。

并发

并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任一个时刻点上仍只有一个进程在运行。

例如,当下,我们使用计算机时可以边听音乐边聊天边上网。 若笼统的将他们均看做一个进程的话,为什么可以同时运行呢,因为并发。

image-20220531114333095

【分时复用cpu】

单道程序设计

所有进程一个一个排队执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。

多道程序设计

在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。

时钟中断即为多道程序设计模型的理论基础。 并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。

在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。

1s = 1000ms, 1ms = 1000us, 1us = 1000ns 1000000000

实质上,并发是宏观并行,微观串行!——-推动了计算机蓬勃发展,将人类引入了多媒体时代。

CPU和MMU

image-20220531115154909

中央处理器(CPU)

image-20220531115254667

内存管理单元MMU

进程

进程控制块(PCB)

进程ID、用户ID、进程状态、调度信息、文件管理、虚拟内存管理、信号(进程间通信机制)、时间和定时器、……

task_struct结构:Linux进程控制块(PCB)的结构体

  • 使用grep -r "task_struct {" /usr/src 命令查找文件所在
  • 所在文件:/usr/src/linux-headers-5.4.0-26/include/linux/sched.h

部分结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pid_t pid;	// 进程id
uid_t uid,euid; // 用户id 有效用户id
gid_t gid,egid; // 用户组id 有效用户组id
volatile long state; // 进程状态
int exit_state; // 退出状态
unsigned int rt_priority;
unsigned int policy;
struct list_head tasks;
struct task_struct *real_parent;
struct task_struct *parent;
struct list_head children,sibling;
struct fs_struct *fs; // 文件描述表
struct files_struct *files; // 文件表
struct mm_struct *mm; // 内存
struct signal_struct *signal;
struct sighand_struct *sighand;
cputime_t utime, stime;
struct timespec start_time;
struct timespec real_start_time;

task_struct主要内容:

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
  • 进程的状态,有就绪、运行、挂起、停止等状态。
  • 进程切换时需要保存和恢复的一些CPU寄存器。
  • 描述虚拟地址空间的信息。
  • 描述控制终端的信息。
  • 当前进程工作目录(Current Working Directory)。
  • umask掩码。
  • 文件描述符表,包含很多指向file结构体的指针。
  • 和信号相关的信息。
  • 用户id和组id。
  • 会话(Session)和进程组。
  • 进程可以使用的资源上限(Resource Limit)。

进程状态

volatile long state;

  • state成员的可能取值如下:

    1
    2
    3
    4
    5
    #define TASK_RUNNING 0    		// 可执行状态
    #define TASK_INTERRUPTIBLE 1 // 可中断等待状态
    #define TASK_UNINTERRUPTIBLE 2 // 不可中断等待状态
    #define TASK_ZOMBIE 4 // 僵尸状态
    #define TASK_STOPPED 8 // 停止状态

Linux进程状态

Linux进程的5种状态

在Linux系统中,一个进程被创建之后,在系统中可以有下面5种状态。进程的当前状态记录在进程控制块的state成员中。

就绪状态和运行状态(可执行状态)

就绪状态的状态标志state的值为TASK_RUNNING。此时,程序已被挂入运行队列,处于准备运行状态。一旦获得处理器使用权,即可进入运行状态。

当进程获得处理器而运行时 ,state值仍然为TASK_RUNNING,并不发生改变;但Linux会把一个专门用来指向当前运行任务的指针current指向它,以表示它是一个正在运行的进程。

可中断等待状态

状态标志state的值为TASK_INTERRUPTIBL。此时,由于进程未获得它所申请的资源而处在等待状态。一旦资源有效或者有唤醒信号,进程会立即结束等待而进入就绪状态。

不可中断等待状态

状态标志state的值为TASK_UNINTERRUPTIBL。此时,进程也处于等待资源状态。一旦资源有效,进程会立即进入就绪状态。这个等待状态与可中断等待状态的区别在于:处于TASK_UNINTERRUPTIBL状态的进程不能被信号量或者中断所唤醒,只有当它申请的资源有效时才能被唤醒。

这个状态被应用在内核中某些场景中,比如当进程需要对磁盘进行读写,而此刻正在DMA中进行着数据到内存的拷贝,如果这时进程休眠被打断(比如强制退出信号)那么很可能会出现问题,所以这时进程就会处于不可被打断的状态下。

停止状态

状态标志state的值为TASK_STOPPED。当进程收到一个SIGSTOP信号后,就由运行状态进入停止状态,当受到一个SIGCONT信号时,又会恢复运行状态。这种状态主要用于程序的调试,又被叫做“暂停状态”、“挂起状态”。

中止状态

状态标志state的值为TASK_DEAD。进程因某种原因而中止运行,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外,并且系统对它不再予以理睬,所以这种状态也叫做“僵死状态”,进程成为僵尸进程。

进程状态切换

image-20220408152319518

文件管理

struct fs_struct *fs; // 文件描述表

struct files_struct *files; // 文件表

image-20220408152511000

image-20220408153141627

内存管理

struct mm_struct *mm; // 内存

image-20220408153548237

进程组织形式

进程控制块的物理组织结构

image-20220408153641182

进程控制块的逻辑组织结构

image-20220408153746262

使用pstree查看当前进程的树形图

image-20220408153845177

Linux中创建进程的方式:

  • shell中执行命令或可执行文件
    • 由shell进程调用fork函数创建子进程
  • 在代码中(已经存在的进程中)调用fork函数创建子进程
    • 通过fork函数创建的进程为已经存在进程的子进程

Linux系统中进程0(PID=0)是由内核创建,其他所有进程都是由父进程调用fork函数所创建的

Linux系统中进程0在创建子进程(PID=1,init进程)后,进程0就转为交换进程或空闲进程

进程1(init进程)是系统中其他所有进程的共同祖先

image-20220409105538106

进程属性

使用ps -aux查看进程及其部分属性

image-20220408154448602

用户和用户组

与进程相关联的用户ID包含以下类型

image-20220408154918504

真实用户与有效用户的关系

  • 通常情况下,有效用户与真实用户相同(有效用户ID等于真实用户ID ),有效用户组与真实用户组相同(有效用户组ID等于真实用户组ID)
  • 可执行文件的文件属性可以设置特殊属性域,定义为“当执行此文件时,将进程的有效用户设置为文件的所有者”,与此类似,组ID也有类似的情况,定义为“当执行此文件时,将进程的有效用户组置为文件所有者所在组”。这两个标志位称为:设置用户ID位(setuid)和设置组ID位(setgid)
  • image-20220408155147072
  • 设置用户ID是图中11位,设置组ID位是图中10位

真实用户和真实用户组

进程真实用户为执行命令/可执行文件的用户,真实用户组为真实用户所在的组

有效用户和有效用户组

进程有效用户和有效用户组只有当可执行文件设置了setuid位或setgid位时才会发生变化

示例:passwd命令程序就设置了设置用户ID位

image-20220408155713660

使用chmod对可执行文件修改设置用户ID位

image-20220408160016081

普通用户能修改密码的原因

  • /etc/passwd文件用来存储所有用户信息,/etc/shadow用来存储用户密码
  • 所有用户都可以修改自己的密码(修改了/etc/shadow文件),但普通用户对/etc/shadow没有读写权限
  • image-20220408170102390

  • 用户通过执行passwd命令( /usr/bin/passwd文件)来修改密码;该文件设置了setuid位,在执行此命令时,该进程的有效用户不等于真实用户,而等于文件所有者(root)

  • Linux根据进程的有效用户进行权限检查,有效用户等于root则允许任何操作(包括对/ect/shadow文件的读写操作)
  • 如果清除掉/usr/bin/passwd文件的setuid权限位,普通用户就不能修改自己的密码了

进程生命周期

image-20220408151230066

C程序的启动函数是main,也是进程代码的入口点

  • main ( int argc, char *argv[] );

当内核启动C程序时,会在调用main函数前调用特殊的启动函数来获取main函数地址和传递给main函数的参数,并且将这些信息填写到进程控制块中

image-20220408171803255

进程的终止

正常终止

  • main函数中返回
  • 在任意代码中调用exit函数或_exit函数
  • 最后一个线程从其启动例程中返回
  • 最后一个线程调用pthread_exit函数

异常终止

  • 在任意代码中调用abort函数
  • 接收到终止信号

exit/_exit函数

man 3 exit,头文件stdlib.h,函数定义:void exit( int status )

man 2 _exit,头文件unistd.h,函数定义:void _exit (int status )

  • 调用这两个函数均会正常地终止一个进程
  • 调用_exit函数将会立即返回内核
  • 调用exit函数:
    • 执行预先注册的终止处理函数
    • 执行文件I/O操作的善后工作,使得所有缓冲的输出数据被更新到相应的设备
    • 返回内核

exit与return

  • return是C语言关键字,exit是POSIX API函数
  • main函数中,执行return和调用exit函数会产生相同的效果
  • 在子函数中,执行return仅仅从子函数中返回,而调用exit函数将会退出当前进程

终止处理函数

注册终止处理函数

  • 当进程终止时,程序可能需要进行一些自身的清理工作,如日志登记、资源释放等
  • 通过atexit函数或on_exit函数允许进程注册若干终止处理函数,当进程终止时,这些终止处理函数将会被自动调用

注意:

  • ANSI C规定一个进程最多能注册32个终止处理函数
  • 显示调用或者隐含调用exit函数(从main中返回、最后一个线程退出等)终止进程将会回调这些注册的终止处理函数(最先注册的函数最后被回调)
  • 显示调用_exit函数终止进程时将不会回调这些注册的终止函数
atexit函数

man 3 atexit

头文件:#include <stdlib.h>

函数原型:int atexit(void (*function)(void));

  • 返回值:成功0,失败非0

示例:atexit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//atexit_text.c
#include <stdio.h>
#include <stdlib.h>

void func1()
{
printf("func1 is called\n");
}

void func2()
{
printf("func2 is called\n");
}

void func3()
{
printf("func3 is called\n");
}

int main(int argc, char **argv)
{
atexit(func1);
atexit(func2);
atexit(func3);
printf("process exit\n");
return 0;
}

image-20220408175512633

on_exit函数

man 3 on_exit

头文件:#include <stdlib.h>

函数原型:int on_exit(void (*function)(int, void *), void *arg);

  • func第一个参数是来自最后一个exit()函数调用中的statusreturn
  • func第二个参数是来自on_exit()函数中的arg
  • 返回值:成功0,失败非0

示例:on_exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//on_exit_text.c
#include <stdio.h>
#include <stdlib.h>

int i, j ,k;

void func1(int status, void *arg)
{
printf("func1 exit status is %d\n", status);
printf("func1 arg is %d\n", *((int*)arg));
}

void func2(int status, void *arg)
{
printf("func2 exit status is %d\n", status);
printf("func2 arg is %d\n", *((int*)arg));
}

void func3(int status, void *arg)
{
printf("func3 exit status is %d\n", status);
printf("func3 arg is %d\n", *((int*)arg));
}


int main(int *argc, char **argv)
{
i = 3;
on_exit(func1, (void*)&i);
j = 4;
on_exit(func2, (void*)&j);
k = 5;
on_exit(func3, (void*)&k);
return 0; // 改成1的话,下面输出的 status值就为1
}

image-20220408191339232

进程环境

分类

1、内核空间 2、内存空间

image-20220408210223114

用户空间布局

image-20220408210750881

  • 正文:CPU执行的代码部分,正文段通常是共享、只读的
  • 初始化的数据:包含了程序中需明确赋初值的变量,如全局变量int maxcount=99;
  • 未初始化的数据:程序执行之前,将此段中的数据初始化为0,如全局变量long sum[1000];
  • 堆:用于动态分配内存
  • 栈:主要用于支撑函数调用存放参数、局部变量等
  • 命令行参数
  • 环境变量

命令行参数

  • 命令行参数:命令后面的字符都是参数
    • ls [参数] <路径或文件名>ls -l /home
    • mkdir [参数] <目录名>mkdir -p /home/xiaokun/src
    • cp [参数] <源文件路径> <目标文件路径>cp -r /usr/local/src /root

C程序中main函数参数

1
2
3
4
5
6
7
8
9
10
11
//myecho.C
#include <stdio.h>
int main(int argc, char **argv)
{
int i = 0;
for(i = 0; i < argc; ++i)
{
printf("Argument %d is %s\n", i, argv[i]);
}
return 0;
}

image-20220408212001229

注意:命令名也传入main中,占 0 位

环境变量

环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备以下特征:

① 字符串(本质) ② 有统一的格式:名=值[:值] ③ 值用来描述进程环境信息。

存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。

使用形式:与命令行参数类似。

加载位置:与命令行参数类似。位于用户区,高于stack的起始位置。

引入环境变量表:须声明环境变量。extern char ** environ;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 打印所有环境变量
#include <stdio.h>

extern char **environ;

int main()
{
int i;
for(i = 0; environ[i]; ++i)
{
printf("%s\n", environ[i]);
}
return 0;
}

环境变量表

image-20220408212220015

  • 每个进程都会有自己的环境变量表
  • 通过全局的环境指针 (environ)可以直接访问环境变量表(字符串数组)
    • 头文件unistd.h
    • extern char **environ;
  • 环境变量字符串形式为 name=value
    • name是环境变量名称
    • value为环境变量赋值

常用环境变量

1)PATH

可执行文件的搜索路径。ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在Shell中用echo命令可以查看这个环境变量的值:

echo $PATH

2)SHELL

当前Shell,它的值通常是/bin/bash

3)TERM

当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。

4)LANG

语言和locale,决定了字符编码以及时间、货币等信息的显示格式。

5)HOME

当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。

父子进程

父子进程

  • 子进程是父进程的副本
    • 子进程复制/拷贝父进程的PCB、数据空间(数据段、堆和栈)
    • 父子进程共享正文段(只读)
  • 子进程和父进程继续执行fork函数调用之后的代码
  • 为了提高效率,fork后不并立即复制父进程数据段、堆和栈,采用了读时共享写时复制机制(Copy-On-Write)
    • 当父子进程任意之一要修改数据段、堆、栈时,进行复制操作,并且仅复制修改区域

image-20220409111016348

父子进程异同

父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…

父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集

  • 子进程的 tms_utime, tms_stime,tms_cutime,tms_ustime值被设置为 0

【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解)

父子进程共享文件

image-20220409111207007

父子进程对共享文件的常见处理方式:

  • 父进程等待子进程完成。当子进程终止后,文件当前位置已经得到了相应的更新
  • 父子进程各自执行不同的程序段,各自关闭不需要的文件

进程相关函数

1)

获取环境变量:getenv函数 或 environ变量访问环境表

设置或修改环境变量:setenv函数 或 putenv

删除环境变量:unsetenv函数

2)

创建进程:fork函数 或 vfork函数

获取进程ID:getpid函数

获取父进程ID:getppid函数(和getpid用法类似)

3)

头文件:

  • #include <unistd.h>
  • #include <sys/types.h>

返回值:执行成功返回对应ID,失败返回-1(错误原因存储在error中)

获取真实用户ID:uid_t getuid(void); man 2 getuid

获取真实用户组ID:uid_t getgid(void);

获取有效用户ID:uid_t geteuid(void);

获取有效用户组ID:uid_t getegid(void);

getenv函数

man 3 getenv

getenv函数用于获取环境变量值

头文件:stdlib.h

函数原型:char* getenv(const char *name);

  • name指定环境变量名称,返回环境变量字符串指针,若未找到则返回NULL
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
char *path = getenv("PATH");
puts(path);
return 0;
}

image-20220408213845594

setenv函数

man 3 setenv

作用:设置或修改环境变量值

头文件:stdlib.h

函数原型:int setenv(const char* name, const char* value, int overwrite);

  • 如果name不存在,则添加name=value到环境变量中
  • name已经存在
    • overwrite!=0,修改原name的值为value
    • overwrite=0,不修改name
  • 返回值:成功0,失败-1

unsetenv函数

man 3 unsetenv

头文件:stdlib.h

函数原型:int unsetenv(const char* name);

  • 删除指定的环境变量字符串
  • 返回值:成功0,失败-1
  • name不存在,返回的也是0

putenv函数

man 3 putenv

putenv函数将环境变量字符串放入环境变量表中;若该字符串已经存在,则覆盖

头文件:stdlib.h

函数原型:int putenv(char *string);

  • string格式:name=value
  • 添加宏定义:_XOPEN_SOURCE

示例代码1

getenv setenv unsetenv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
char *val;
const char *name = "ABD";

val = getenv(name);
printf("1, %s = %s\n", name, val);

setenv(name, "haha-day-and-night", 0);

val = getenv(name);
printf("2, %s = %s\n", name, val);

#if 0
int ret = unsetenv("ABD=");
printf("ret = %d\n", ret);

val = getenv(name);
printf("3, %s = %s\n", name, val);

#else
int ret = unsetenv("ABD"); //name=value:value
printf("ret = %d\n", ret);

val = getenv(name);
printf("3, %s = %s\n", name, val);
#endif

return 0;
}

fork函数

man 2 fork

头文件:unistd.h

函数原型:pid_t fork(void);

返回值:

  • fork函数被正确调用后,将会在子进程中和父进程中分别返回!!
  • 在子进程中返回值为0(不合法的PID,提示当前运行在子进程中)

  • 在父进程中返回值为子进程ID(让父进程掌握所创建子进程的ID号)

  • 出错返回-1

fork用法

  • 父进程希望复制自己(共享代码,复制数据空间),但父子进程执行相同代码中的不同分支
    • 网络服务程序中,父进程等待客户端的服务请求,当请求达到时,父进程调用fork创建子进程处理该请求,而父进程继续等待下一个服务请求到达
  • 父子进程执行不同的可执行文件(父子进程具有完全不同的代码段和数据空间)
    • 子进程从fork返回后,立即调用exec类函数执行另外一个可执行文件

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
pid_t pid;
pid = fork();
if(pid == -1)
{
printf("fork error\n");
}
else if(pid == 0)
{
printf("the returned value is %d\n", pid);
printf("in child process!!\n");
printf("My PID is %d\n", getpid());
}
else
{
printf("the returned value is %d\n", pid);
printf("in father process!!\n");
printf("My PID is %d\n", getpid());
}
return 0;
}

image-20220409110646792

示例2:创建5个子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <unistd.h>

int main()
{
int i;
pid_t pid;
printf("xxxxxxxxxx\n");

for(i = 0; i < 5; ++i)
{
pid = fork();
if(pid == 0)
{
break;
}
}

if(i < 5)
{
sleep(i); //保证顺序输出
printf("I' am %d child, pid = %u\n", i+1, getpid());
}
else
{
sleep(i); //保证父进程最后结束
printf("I'm parent\n");
}
return 0;
}

vfork函数

man 2 vfork

头文件:unistd.h

函数原型:pid_t vfork(void);

  • 返回值:成功返回pid,失败返回-1

vfork用法:

  • vfork用于创建新进程,而该新进程的目的是执行一个可执行文件
  • 由于新程序将有自己的地址空间,因此vfork函数并不将父进程的地址空间完全复制到子进程中
  • 子进程在调用execexit之前,在父进程的地址空间中运行
  • vfork函数保证子进程先执行,在它调用exec或者exit之后,父进程才会继续被调度执行(父进程处于TASK_UNINTERRUPTIBLE状态),如果在
    调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁

image-20220409111815455

和fork相比:

  • fork之后,子进程拷贝父进程的数据段,代码段。(读时共享写时复制)
  • vfork共享父进程的数据段。主要适用于执行可执行文件

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid < 0)
printf("error in fork!\n");
else if(pid == 0)
{
cnt++;
printf("cnt=%d\n",cnt); // cnt=1
printf("I am the child process,ID is %d\n",getpid());
exit(0); // 注释此语句将发送段错误
}
else
{
cnt++;
printf("cnt=%d\n",cnt); // cnt=2
printf("I am the parent process,ID is %d\n",getpid());
}
return 0;

}

image-20220602192411062

getpid函数

man 2 getpid

作用:获取进程ID

头文件:

  • #include <sys/types.h>
  • #include <unistd.h>

函数:pid_t getpid( void);

  • pid_t实际就是int类型
  • 执行成功返回当前进程的ID,失败返回-1(错误原因存储在errno中)

示例代码2

getuid getgid geteuid getegid

1
2
3
4
5
6
7
8
9
10
11
//suid.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
printf("real uid: %d, real gid:%d\n",getuid(), getgid());
printf("effective uid: %d, effective gid: %d\n", geteuid(), getegid());
exit(0);
}

image-20220408165715572

exec函数族

进程调用exec系列函数在进程中加载执行另外一个可执行文件

exec系列函数替换了当前进程(执行该函数的进程)的正文段、数据段、堆和栈(来源于加载的可执行文件)

执行exec系列函数后从加载可执行文件的main函数开始重新执行

exec系列函数并不创建新进程,所以在调用exec系列函数后其进程ID并未改变已经打开的文件描述符不变

image-20220409121804687

execl execle execlp execv execve execvp 共6个函数

man 3 execl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>

extern char **environ;

int execl(const char *pathname, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *pathname, const char *arg, ...
/*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

execvpe(): _GNU_SOURCE
  • llist,每个命令行参数都说明为一个单独的参数
  • vvector,命令行参数放在数组中
  • eenvironment,表示由函数调用者提供环境变量表
  • ppath,使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量

exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。

只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。

exec函数的关系

image-20220602151137083

execl函数

函数原型:int execl(const char *pathname, const char *arg0, ...,NULL

  • pathname:要执行程序的绝对路径名
  • 可变参数:要执行程序的命令行参数,以空指针NULL结束
  • 返回值:出错返回-1,成功该函数不返回!

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>
#include <stdio.h>

int main()
{
printf("entering main process--\n");
if(fork() == 0)
{
execl("/bin/ls", "ls", "-l", NULL);
//若execl执行成功,execl后面的代码不会执行,而是运行新的可执行文件的代码
printf("exiting main process --\n"); //这句代码不会执行
}
return 0;
}

execv函数

函数原型:int execv(const char *pathname, char *const argv[]);

  • pathname:要执行程序的绝对路径名
  • argv:数组指针维护的程序命令行参数列表,该数组的最后一个成员必须为空指针

  • 返回值:出错返回-1,成功该函数不返回!

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <unistd.h>

int main()
{
int ret;
char *argv[] = {"ls", "-l", NULL};
printf("entering main process --\n");
if(fork() == 0)
{
ret = execv("/bin/ls", argv);
if(ret == -1)
{
perror("execv error\n");
}
printf("exiting main process --\n");
}
return 0;
}

execle函数

函数原型:int execle(const char *pathname, const char *arg0,... NULL, char *const envp[]);

  • pathname:要执行程序的绝对路径名
  • 可变参数:要执行程序的命令行参数,以空指针结束
  • envp指向环境字符串指针数组的指针,该数组的最后一个成员必须为空指针
  • 返回值:出错返回-1,成功该函数不返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
char *envp[] = {"PATH=/tmp", "USER=yang", NULL}
if(fork() == 0)
{
if(execle("/bin/ls", "ls", "-l", NULL, envp) < 0)
{
perror("execle error!");
}
}
return 0;
}

execlp函数

int execlp(const char *filename,const char *arg0, ...,NULL);

  • filename参数可以是相对路径(路径信息从环境变量PATH中获取)
  • 例如默认环境变量中包含的PATH=/bin:/usr/bin:/usr/local/bin/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if (pid == 0)
{
execlp("ls", "ls", "-a", "-l", NULL);
}
else
{
printf("parent\n");
}
return 0;
}

示例

ps aux的结果输出到ps.out文件中

1)法一:使用dup2dup2(fd, STDOUT_FILENO); 让标准输出指向ps.out

2)法二:使用输出重定向ps aux > ps.out 其中> 需要转义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(void)
{
int fd;

fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd < 0)
{
perror("open error");
exit(1);
}
//方法一:
//dup2(fd, STDOUT_FILENO);
//execlp("ps", "ps", "aux", NULL);
//方法二:
execlp("ps", "ps", "aux", "\>", "ps.out", NULL); // > 需要转义
return(0);
}

子进程回收

孤儿进程

孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
pid_t pid;
pid = fork();
if(pid == 0)
{
while(1)
{
printf("I am child, my parent pid = %d\n", getppid());
sleep(1);
}
}
else if(pid > 0)
{
printf("I am parent, my pid is = %d\n", getpid());
sleep(5);
printf("-------------parent going to die------------------\n");
}
else
{
perror("fork");
return 1;
}
return 0;
}

image-20220602152843510

僵尸进程

父进程先于子进程结束,未回收子进程资源。子进程在退出之前会释放进程用户空间的所有资源,但PCB等内核空间资源不会被释放。

  • 当父进程调用waitwaitpid函数后,内核将根据情况关闭该进程打开的所有文件,释放PCB(释放内核空间资源)
  • 对于已经终止但父进程尚未对其调用waitwaitpid函数的进程(TASK_ZOMBIE状态),称为僵尸进程

如果父进程在子进程终止之前终止,则子进程的父进程将变为init进程,保证每个进程都有父进程,由init进程调用wait函数进行善后

回收资源

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用waitwaitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。

获知子进程状态信息改变

当一个进程发生特定的状态变化(进程终止、暂停以及恢复)时,内核向其父进程发送SIGCHLD信号

父进程可以选择忽略该信号,也可以对信号进行处理(默认处理方式为忽略该信号)

waitwaitpid函数可以用于等待子进程状态信息改变,并获取其状态信息

wait函数

man 2 wait

功能:获取任意子进程的状态改变信息

  • 若是终止状态,则回收子进程残留资源
  • 阻塞等待子进程退出
  • 获取子进程结束状态(退出原因

头文件:sys/wait.h

函数原型:pid_t wait(int *wstatus);

  • wstatus:用于获取子进程的状态改变
  • wstatus可以为空指针,此时父进程不需要具体了解子进程的状态变化,只是为了防止子进程成为僵尸进程,或者因为同步原因需等待子进程终止
  • wstatus不是空指针,则内核将子进程状态改变信息存放在它指向的存储空间中(正常终止→退出值;异常终止→终止信号)
  • 返回值:若成功返回状态信息改变子进程ID,出错返回-1

wait函数传出参数status

子进程状态改变信息包含了多种类型的信息,可以通过系统提供的宏来快速解析子进程的状态。可使用来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:

功能说明
WIFEXITED(wstatus) 当子进程正常终止时该宏为真,对于这种情况可进一步执行WEXITSTATUS(wstatus),获取子进程传递给exit_exit函数参数的低8位或return
WIFSIGNALED(wstatus) 当子进程异常终止(信号)时该宏为真,对于这种情况可进一步执行WTERMSTG(wstatus),获取使子进程终止的信号编号
WIFSTOPPED(wstatus) 当子进程暂停时该宏为真,对于这种情况可进一步执行WSTOPSIG(wstatus),获取使子进程暂停的信号编号
WIFCONTINUED(wstatus) 若子进程在暂停后已经继续运行则该宏为真
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
pid_t pid, wpid;
pid = fork();

if(pid == 0)
{
printf("child, my parent = %d, going to sleep 10s\n", getppid());
sleep(10);
printf("----------child die----------------\n");
return 77;
}
else if(pid > 0)
{
// wpid = wait(NULL);
int status;
wpid = wait(&status);
if(wpid == -1)
{
perror("wait error");
exit(1);
}
if(WIFEXITED(status)) //判断是否正常终止
{
//如果正常终止,打印终止的退出值
printf("child exit with %d\n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status)) //判断是否是异常终止,信号导致
{
printf("child exit with %d\n", WTERMSTG(status));
}
printf("parent pid = %d\n", getpid());
}
}

调用wait函数之后,父进程可能出现的情况:

  • 如果所有子进程都还在运行,则父进程被阻塞TASK_INTERRUPTIBLE状态),直到有一个子进程终止或暂停,wait函数才返回
  • 如果已经有子进程进入终止或暂停状态,则wait函数会立即返回
  • 若进程没有任何子进程,则立即出错返回-1

等待特定子进程

如果一个进程有几个子进程,那么只要有一个子进程状态改变,wait函数就返回

如何才能使用wait函数等待某个特定子进程的状态改变?

  • 调用wait,然后将其返回的进程ID和所期望的子进程ID进行比较。

    • 如果ID不一致,则保存该ID,并循环调用wait函数,直到等到所期望的子进程ID为止
  • 使用waitpid

waitpid函数

man 2 waitpid

功能:等待某个特定子进程状态改变

头文件:sys/wait.h

函数原型:pid_t waitpid(pid_t pid, int *statloc, int options);

  • pid

    • pid = -1:等待任意子进程执行终止(同wait)
    • pid > 0:等待指定ID为pid的子进程终止
    • pid = 0:等待其组ID等于调用进程组ID的任意子进程
    • pid < -1:等待其组ID等于pid绝对值的任意子进程
  • statloc:存放子进程终止状态

  • options:可以为0(阻塞),也可以是以下常量或常量的或

    • WCONTINUED:如果暂停的子进程由于被SIGCONT唤醒而产生的SIGCHLD,则函数将返回
    • WUNTRACED:如果有处于终止状态的进程,则函数返回
    • WNOHANG非阻塞。如果没有任何已经终止的子进程则马上返回,,函数不等待,此时返回值为0
  • 返回值:成功返回终止子进程ID,失败返回-1(无子进程)

waitpid的特有功能

  • waitpid可等待一个特定的进程的状态改变信息
  • waitpid可以实现非阻塞(异步)的等待操作,有时希望取得子进程的状态改变信息,但不希望阻塞等待子进程状态改变
  • waitpid支持作业控制(进程组控制

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
pid_t pid_child, pid_return;
pid_child = fork();
if (pid_child < 0)
{
printf("Error occuredon forking.\n");
}
else if (pid_child == 0)
{
sleep(10);
exit(0);
}
do
{
pid_return = waitpid(pid_child, NULL, WNOHANG);
if (pid_return == 0)
{
printf("No child exited\n");
sleep(1);
}
} while (pid_return == 0);
if (pid_return == pid_child)
{
printf("successfullyget child %d\n", pid_return);
}
else
{
printf("some error occured\n");
}
return 0;
}

image-20220412122856363

进程通信

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,Inter-Process Communication)。

image-20220602193549547

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:

① 管道(使用最简单)

② 信号(开销最小)

③ 共享映射区(无血缘关系)

④ 本地套接字(最稳定)

管道

管道是一种最古老、最简单的UNIX进程间通信机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:

​ 1. 其本质是一个伪文件(实为内核缓冲区)

  1. 由两个文件描述符引用,一个表示读端,一个表示写端。

​ 3. 规定数据从管道的写端流入管道,从读端流出。

管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

  • ulimit -a 可以看到默认管道大小为$8\times 512b = 4k$ (8个扇区)

管道的局限性:

① 数据自己读不能自己写,自己写不能自己读。

② 数据一旦被读走,便不在管道中存在,不可反复读取。

③ 由于管道采用半双工通信方式。因此,数据只能单向流动

④ 只能在有公共祖先的进程间使用管道。

常见的通信方式有,单工通信、半双工通信、全双工通信。

pipe函数

man 2 pipe

头文件:unistd.h

函数原型:int pipe(int pipefd[2]);

  • 程序通过文件描述符pipefd[0]pipefd[1]来访问管道
  • pipefd[0]管道读操作pipefd[1]管道写操作
  • 写入pipefd[1]的数据可以按照先进先出的顺序从pipefd[0]中读出
  • 返回值:成功返回0,出错返回-1
  • 文件描述符pipefd[0]pipefd[1]无需open,但需手动close
  • 向管道文件读写数据其实是在读写内核缓冲区

管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:

image-20220604115742038

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
int fd[2];
pid_t pid;
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe");
exit(1);
}

pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
close(fd[1]);
char buf[1024];
ret = read(fd[0], buf, sizeof(buf));
if(ret == 0)
{
printf("NULL\n");
}
write(STDOUT_FILENO, buf, ret);
close(fd[0]);
}
else
{
close(fd[0]);
char *str = "hello pipe\n";
write(fd[1], str, strlen(str)+1);
close(fd[1]);
}
return 0;
}

管道读写行为

使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

  1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。

  2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

  3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。

  4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

总结:

1)读管道:

  • 管道中有数据,read返回实际读到的字节数
  • 管道中无数据:
    • 管道写端被全部关闭,read返回0(类似读到文件结尾)
    • 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)

2)写管道

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

管道缓冲区大小

可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:pipe size (512 bytes, -p) 8

也可以使用fpathconf函数,借助参数选项来查看。使用该宏应引入

头文件<unistd.h>

函数原型:long fpathconf(int fd, int name);

  • 成功:返回管道的大小 失败:-1,设置errno
  • name 是一些宏,如_PC_PIPE_BUF

管道小结

优点:简单,相比信号,套接字实现进程间通信,简单很多。

缺点:1)只能单向通信,双向通信需建立两个管道。2)只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。

允许:

代码示例

练习:使用管道实现兄弟进程间通信,实现ls | wc -l。兄:ls 弟: wc -l 父:等待回收子进程。

要求,使用“循环创建N个子进程”模型创建兄弟进程,使用循环因子i标示。注意管道读写行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
pid_t pid;
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe");
exit(1);
}

int i = 0;
for(i = 0; i < 2; ++i)
{
pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
if(pid == 0) break;
}
if(i == 0) // 兄进程 读
{
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
perror("1");
}
else if(i == 1) // 弟进程 写
{
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
perror("2");
}
else // 父进程 阻塞等待回收子进程
{
close(fd[1]);
close(fd[0]);
while(wait(NULL) != -1);
}
return 0;
}

命名管道

概念

FIFO常被称为命名管道,以区分管道(pipe)。

FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。

  • 管道只能在父子进程之间使用
  • 通过FIFO,不相关的进程也能交换数据
  • FIFO也被称为命名管道, FIFO是一种特殊的文件(创建FIFO类似于创建文件,FIFO的路径名存在于文件系统中)
  • 创建FIFO之后可以通过文件I/O对其进行操作
  • 非父子进程可以通过文件名来使用FIFO

创建FIFO的命令:mkfifo,函数mkfifo

mkfifo函数

作用:创建命名管道

man 3 mkfifo

头文件:

  • sys/types.h
  • sys/stat.h

函数原型:int mkfifo(const char *pathname, mode_t mode) ;

  • pathname:文件名(绝对路径)
  • mode:文件类型、权限等,如664

返回值:成功返回0,出错返回-1

代码示例

fifo_r.c 读,fifo_w.c写,提前创建命名管道fifo_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// fifo_r.c
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

void sys_err(char *str)
{
perror(str);
exit(1);
}

int main(int argc, char *argv[])
{
int fd, len;
char buf[4096];

if (argc < 2) {
printf("./a.out fifoname\n");
return -1;
}
fd = open(argv[1], O_RDONLY);
if (fd < 0)
sys_err("open");
while (1) {
len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
sleep(2); //多个读端时应增加睡眠秒数,放大效果.
}
close(fd);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// fifo_w.c
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

void sys_err(char *str)
{
perror(str);
exit(-1);
}

int main(int argc, char *argv[])
{
int fd, i;
char buf[4096];

if (argc < 2) {
printf("Enter like this: ./a.out fifoname\n");
return -1;
}
fd = open(argv[1], O_WRONLY);
if (fd < 0)
sys_err("open");

i = 0;
while (1) {
sprintf(buf, "hello itcast %d\n", i++);

write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);

return 0;
}

fifo的open阻塞

关于fifo的open阻塞问题_Mr_John_Liang的博客-CSDN博客_open 阻塞模式

文件通信

使用文件也可以完成IPC,理论依据是,fork后,父子进程共享文件描述符。也就共享打开的文件。

无血缘关系的进程可以打开同一个文件进行通信。

父子进程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* 
*父子进程共享打开的文件描述符------使用文件完成进程间通信.
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/wait.h>


int main(void)
{
int fd1, fd2; pid_t pid;
char buf[1024];
char *str = "---------test for shared fd in parent child process-----\n";


pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
fd1 = open("test.txt", O_RDWR);
if (fd1 < 0) {
perror("open error");
exit(1);
}
write(fd1, str, strlen(str));
printf("child wrote over...\n");

} else {
fd2 = open("test.txt", O_RDWR);
if (fd2 < 0) {
perror("open error");
exit(1);
}
sleep(1); //保证子进程写入数据

int len = read(fd2, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);

wait(NULL);
}

return 0;
}

无关系进程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* 先执行,将数据写入文件test.txt
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

#define N 5

int main(void)
{
char buf[1024];
char *str = "--------------secesuss-------------\n";
int ret;

int fd = open("test.txt", O_RDWR|O_TRUNC|O_CREAT, 0664);

//直接打开文件写入数据
write(fd, str, strlen(str));
printf("test1 write into test.txt finish\n");

sleep(N);

lseek(fd, 0, SEEK_SET);
ret = read(fd, buf, sizeof(buf));
ret = write(STDOUT_FILENO, buf, ret);

if (ret == -1) {
perror("write second error");
exit(1);
}

close(fd);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
* 后执行,尝试读取另外一个进程写入文件的内容
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main(void)
{
char buf[1024];
char *str = "----------test2 write secesuss--------\n";
int ret;

sleep(2); //睡眠2秒,保证test1将数据写入test.txt文件

int fd = open("test.txt", O_RDWR);

//尝试读取test.txt文件中test1写入的数据
ret = read(fd, buf, sizeof(buf));

//将读到的数据打印至屏幕
write(STDOUT_FILENO, buf, ret);

//写入数据到文件test.txth中, 未修改读写位置
write(fd, str, strlen(str));

printf("test2 read/write finish\n");

close(fd);

return 0;
}

存储映射I/O

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。

使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

image-20220604160436054

mmap函数

man 2 mmap

头文件:#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

  • addr:指定映射区的首地址。通常为NULL(Linux内核指定)
  • length:共享内存映射区的大小。(<= 文件的实际大小)
  • prot:共享内存映射区的读写属性。PROT_WRITEPROT_READ
  • flags:共享内存的共享属性。
    • MAP_SHARED:会将映射区所做的操作反映到物理设备(磁盘)上
    • MAP_PRIVATE:映射区所做的修改不会反映到物理设备。
  • fd:用于创建共享内存映射区的文件描述符
  • offset:偏移量,默认0。4k的整数倍
  • 返回值:
    • 成功:映射区的首地址
    • 失败:返回宏MAP_FAILED((void*)-1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>

int main(void)
{
char *p = NULL;
int fd = open("mytest.txt", O_CREAT | O_RDWR, 0664); // 创建文件
if(fd == -1)
{
perror("open");
exit(1);
}
int ret = ftruncate(fd, 4); // 设置文件大小为4
if(ret == -1)
{
perror("ftruncate");
exit(1);
}
p = mmap(NULL, 4, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);
if(p == MAP_FAILED)
{
perror("mmap");
exit(1);
}
strcpy(p, "abc");
close(fd);
ret = munmap(p, 4); // 释放映射的内存
if(ret == -1)
{
perror("munmap");
exit(1);
}
return 0;
}

munmap函数

man 2 munmap

同malloc函数申请内存空间类似的,mmap建立的映射区在使用结束后也应调用类似free的函数来释放。

头文件:#include <sys/mman.h>

函数:int munmap(void *addr, size_t length);

  • addr:映射区起始地址
  • length:映射区长度
  • 返回值:成功:0; 失败:-1

mmap注意事项

思考:

  1. 可以open的时候O_CREAT一个新文件来创建映射区吗?

  2. 如果open时O_RDONLY, mmap时PROT参数指定PROT_READ|PROT_WRITE会怎样?

  3. 文件描述符先关闭,对mmap映射有没有影响?

  4. 如果文件偏移量为1000会怎样?

  5. 对mem越界操作会怎样?

  6. 如果mem++,munmap可否成功?

  7. mmap什么情况下会调用失败?

  8. 如果不检测mmap的返回值,会怎样?

总结:使用mmap时务必注意以下事项

  1. 创建映射区的过程中,隐含着一次对映射文件的读操作

  2. MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。

  3. 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。

  4. 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。

  5. munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++、—操作。

  6. 如果文件偏移量必须为4K的整数倍

  7. mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

父子进程通信

父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:

MAP_PRIVATE: (私有映射) 父子进程各自独占映射区;

MAP_SHARED: (共享映射) 父子进程共享映射区;

结论:父子进程共享:1. 打开的文件 2. mmap建立的映射区(但必须要使用MAP_SHARED)

练习:父进程创建映射区,然后fork子进程,子进程修改映射区内容,而后,父进程读取映射区内容,查验是否共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>

int var = 100;

int main(void)
{
int *p;
pid_t pid;

int fd;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open error");
exit(1);
}
unlink("temp"); //删除临时文件目录项,使之具备被释放条件.
ftruncate(fd, 4);

p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd); //映射区建立完毕,即可关闭文件

pid = fork(); //创建子进程
if(pid == 0){
*p = 2000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);

int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}

return 0;
}

匿名映射

通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。 可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。

1)使用MAP_ANONYMOUS(或MAP_ANON),不需要打开文件,如:

1
int *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

2)需注意的是,MAP_ANONYMOUSMAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。

1
2
fd = open("/dev/zero", O_RDWR);
p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);

法一映射示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
char *p;
pid_t pid;
int size = 100;
p = (char *)mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
pid = fork(); //创建子进程
if(pid == 0){
strcpy(p, "hello, this is /dev/zero");
} else {
sleep(1);
printf("%s\n", p);
wait(NULL);
int ret = munmap(p, size); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0;
}

法二映射示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/mman.h>

int main(void)
{
char *p;
pid_t pid;
int size = 100;
int fd = open("/dev/zero", O_RDWR);
p = (char *)mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd); // 关闭文件

pid = fork(); //创建子进程
if(pid == 0){
strcpy(p, "hello, /dev/zero");
} else {
sleep(1);
printf("%s\n", p);
wait(NULL);
int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0;
}

无关系进程通信

实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。

示例:借助文件file_sharedmmap_r.c读映射区,mmap_w.c写映射区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// mmap_r.c
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>

struct STU {
int id;
char name[20];
char sex;
};

void sys_err(char *str)
{
perror(str);
exit(-1);
}

int main(int argc, char *argv[])
{
int fd;
struct STU student;
struct STU *mm;

if (argc < 2) {
printf("./a.out file_shared\n");
exit(-1);
}

fd = open(argv[1], O_RDONLY);
if (fd == -1)
sys_err("open error");

mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
if (mm == MAP_FAILED)
sys_err("mmap error");

close(fd);

while (1) {
printf("id=%d\tname=%s\t%c\n", mm->id, mm->name, mm->sex);
sleep(2);
}

munmap(mm, sizeof(student));

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// mmap_w.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>

struct STU {
int id;
char name[20];
char sex;
};

void sys_err(char *str)
{
perror(str);
exit(1);
}

int main(int argc, char *argv[])
{
int fd;
struct STU student = {10, "xiaoming", 'm'};
char *mm;

if (argc < 2) {
printf("./a.out file_shared\n");
exit(-1);
}

fd = open(argv[1], O_RDWR | O_CREAT, 0664);
ftruncate(fd, sizeof(student));

mm = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (mm == MAP_FAILED)
sys_err("mmap");

close(fd);

while (1) {
memcpy(mm, &student, sizeof(student));
student.id++;
sleep(1);
}

munmap(mm, sizeof(student));

return 0;
}

启动mmap_w后,再运行mmap_r

image-20220605114841501

XSI IPC机制

进程通信机制

image-20220409175106240

System V(“系统五”)系统上发明了三种IPC机制(消息队列、信号量和共享内存),通常称为System V IPC。又因为后来被收录到Unix的XSI标准之中故又称为XSI IPC。所以当你看到System V IPC 和 XSI IPC的时候实际上指的是同一种东西。

  • 信号量集(semaphore set),用于实现进程之间的同步与互斥
  • 共享内存(shared memory),用于在进程之间高效地共享数据,适用于数据量大,速度要求高的场景
  • 消息队列(message queue),进程之间传递数据的一种简单方法

IPC对象ipcs

ipcs命令可以查看IPC对象

ipcmk:创建IPC对象

ipcrm :删除IPC对象

image-20220611141909021

IPC对象的key值和ID

Linux系统中的IPC对象都是全局的,为每个IPC对象分配唯一的ID

在IPC操作中通信各方需要通过ID来指示操作的IPC对象,需要有机制让通信各方获取获取IPC对象的ID

  • 创建IPC对象的进程通过创建IPC对象函数的返回值可获取ID值
  • 未创建IPC对象的进程如何获取IPC对象的ID值并使用该对象呢?

IPC机制的ID值为动态分配,无法提前约定,不能跨进程传递

多个进程提前约定使用相同的key值做为参数来创建IPC对象或打开已经创建的IPC对象

如果通信各方(进程)在创建/打开IPC对象时使用相同的key值:

  • 首次使用该key值创建IPC对象的进程将真正创建该IPC对象,并获取其ID值
  • 后续使用该key值创建IPC对象的进程都将在内核中找到该IPC对象并打开它,从而获取其ID值

IPC对象与key值一一对应,因此key值不能重复

ftok函数

通过ftok函数来产生独特的key值,避免重复

man 3 ftok

1
2
3
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
  • pathname是指定的文件名,可以是特殊文件也可以是目录文件)
  • proj_id是子序号
  • 成功返回key值,失败-1

如果要确保key_t值不变,需要确保ftok所指定的文件名不被删除

原因:每个文件都一个索引节点inode号,可以使用ls -i命令查看,且inode号一定不同。ftok的一般实现方法是proj_id+inode

假设a.txt文件节点号为65538,换算成16进制0x010002。指定proj_id=380x26,则key=0x2610002

函数对比

信号量集 共享内存 消息队列 功能
semget shmget msgget 创建或打开一个IPC对象,获得对IPC机制的访问权
semop shmat shmdt msgsnd msgrcv IPC操作: 信号量操作;连接/释放共享内存;发送/接收消息;
semctl shmctl msgctl IPC控制:获得/修改IPC对象状态,“删除”IPC对象等

消息队列

  • 进程之间传递数据的一种简单方法
  • 把每个消息看作一个记录,具有特定的格式
  • 消息队列就是消息的链表
  • 写权限:按照一定的规则添加新消息
  • 读权限:从消息队列中读走消息
  • 消息队列能够克服管道或命名管道机制的一些缺点,例如实时性差等

消息队列结构

image-20220409203702015

消息队列操作

头文件:

1
2
3
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
  1. 打开或创建消息队列对象:msgget
  2. 向消息队列发送消息:msgsnd
  3. 从消息队列接收消息:msgrcv
  4. 消息队列控制操作:msgctl

msgget函数

创建消息队列 man 2 msgget

int msgget(key_t key, int msgflg);

返回值:成功返回创建或打开的消息队列对象ID;出错返回-1

  • key:创建或打开消息队列对象时指定的key值(提前约定或通过ftok函数创建)

    • 若key为IPC_PRIVATE,则只能用于父子进程间通信
  • msgflg:设置访问权限,取值可以为以下一个或多个值的或

    • IPC_CREAT :如果消息队列对象不存在则创建,否则打开已经存在的消息队列对象,创建还需同文件一样设置权限,如IPC_CREAT | 0664
    • IPC_EXCL:只有消息队列对象不存在的时候,才能创建新的消息队列对象,否则就产生错误EEXIST
  • 成功返回消息队列ID,失败返回-1

msgsnd函数

发送信息到消息队列 man 2 msgsnd

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

  • msgid:消息队列ID

  • msgp:指向mymsg的结构体(mymsg自己定义,样式如下)指针

    1
    2
    3
    4
    struct mymsg {
    long mtype; // 消息类型 >0,小于0的消息类型有特殊的指示作用
    char mtext[]; // 消息内容, 数组长度自定义 >=0 也可以是结构体
    };
  • msgsz:以字节为单位指定待发送消息的长度(mymsg结构体中消息类型mtext自定义数据的大小)

  • msgflag:可以是0,也可以是IPC_NOWAIT(该标志可以使函数工作在非阻塞模式)

  • 返回值:成功返回0;出错返回-1

出现以下情况时:

1)指定的消息队列容量已满

2)在系统范围存在太多的消息

  • 若设置了IPC_NOWAIT,则msgsnd立即返回(返回EAGAIN错误)
  • 若未指定该标志,则msgsnd导致调用进程阻塞,直到可以发送成功为止

msgrcv函数

从消息队列接收信息 man 2 msgrcv

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

返回值:成功返回实际读取数据的字节数;出错返回-1

  • 前三个参数同msgsnd函数
  • msgtyp:指定期望从消息队列中接收什么样的消息
    • msgtyp = 0:队列中第一个消息(消息队列是一个FIFO链表,所以返回的是队列中最早的消息)
    • msgtyp > 0:消息队列中类型值为msgtyp的第一个消息
    • msgtyp < 0:消息队列中类型值小于或等于msgtyp绝对值中类型值最小的第一个消息
  • msgflg:当消息队列中没有期望接收的消息时会如何操作
    • 若设置了IPC_NOWAIT标志,则函数立即返回ENOMSG错误
    • 若未设置IPC_NOWAIT标志,否则msgrcv导致调用进程阻塞直到如下某个事件发生:
      • 有其他进程向消息队列中发送了所期望接收的消息
      • 该消息队列被删除,此时返回EIDRM错误
      • 进程被某个信号中断,此时返回EINTR错误

msgctl函数

获取、设置消息队列属性,删除消息队列 man 2 msgctl

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

  • cmd:宏,决定对消息队列的操作
    • IPC_STAT:复制内核中的msqid_dsbuf
    • IPC_SET:通过buf设置内核中的msqid_ds内容
    • IPC_RMID:删除消息队列,bufNULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct msqid_ds {
struct ipc_perm msg_perm;/* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};

还有一大堆看不懂的操作

代码示例

一个发送方,一个接收方。发送方从键盘输入消息

消息发送者: msgsnd.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

struct msg_st {
long msg_type;
char text[1024];
};

int main()
{
int running = 1;
struct msg_st data;
char buffer[1024];
int msgid = -1;
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1)
{
fprintf(stderr, "megget failed with error:%d\n", errno);
// errno : int型 errno 是记录系统的最后一次错误代码 在头文件 errno.h中
exit(EXIT_FAILURE); // EXIT_FAILURE 1 stdlib.h中的宏定义
}

while(running)
{
printf("Enter some text:");
fgets(buffer, 1024, stdin);
data.msg_type = 1;
strcpy(data.text, buffer);
if(msgsnd(msgid, (void*)&data, 1024, 0) == -1)
{
fprintf(stderr, "msgsnd failed\n");
exit(EXIT_FAILURE);
}
if(strncmp(buffer, "end", 3) == 0) // 字符串比较函数 在 string.h中
{
running = 0;
sleep(1); // 在头文件 unistd.h 中定义
}
// exit(EXIT_SUCCESS); // EXIT_SUCCESS 0
}

int ret = msgctl(msgid, IPC_RMID, NULL);
if(ret == 0) {
printf("msg %d destroy success\n", msgid);
}
else {
perror("msgctl");
}
return 0;
}

消息接收者:msgrcv.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

struct msg_st {
long int msg_type;
char text[1024];
};

int main()
{
int running = 1;
int msgid = -1;
struct msg_st data;
long int msgtype = 0;
msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
if(msgid == -1)
{
fprintf(stderr, "megget failed with error:%d\n", errno);
exit(EXIT_FAILURE);
}
while(running)
{
if(msgrcv(msgid, (void*)&data, 1024, msgtype, 0) == -1)
{
fprintf(stderr, "msgrcv failed with errno:%d\n", errno);
exit(EXIT_FAILURE);
}
printf("You wrote:%s\n", data.text);
if(strncmp(data.text, "end", 3) == 0)
{
running = 0;
}
// exit(EXIT_SUCCESS);
}
return 0;
}

image-20220611170415896

信号量集

任务资源共享情况

临界资源:在一段时间内只允许一个任务访问的资源。诸任务间应采取互斥方式,实现对资源的共享

共享资源:允许多个任务同时访问同一种资源的多个实例

image-20220410122956953

信号量类型

信号量一般分为三种类型

  • 互斥信号量:任务之间互斥访问临界资源
  • 计数信号量:任务之间竞争访问共享资源
  • 二值信号量:任务之间的同步机制

信号量是操作系统提供的管理资源共享的有效手段

信号量作为操作系统核心代码执行,其地位高于任务(进程或线程),任务调度不能终止其运行

信号量的实现

信号量s一般包含以下成员:

  • 整数值s.count(实现资源计数)
  • 任务阻塞队列s.queue

信号量操作:初始化、P操作、V操作

  • 在进程初始化信号量将s.count指定为一个非负整数值,表示可用的共享资源实例总数
  • 运行中s.count可为负值(其绝对值表示当前等待访问该共享资源的进程数)

  • P操作wait(s)

    1
    2
    3
    4
    5
    6
    --s.count;//表示申请一个资源;
    if (s.count < 0)//表示没有空闲资源;
    {
    调用进程进入阻塞队列s.queue;
    阻塞调用进程;
    }
  • V操作signal(s)

    1
    2
    3
    4
    5
    6
    ++s.count; //表示释放一个资源
    if(s.count <= 0) //表示有进程处于阻塞状态
    {
    从等待队列s.queue中取出一个进程p;
    进程P进入就绪队列;
    }

信号量状态图

image-20220611173018245

信号量集结构

image-20220410124618074

1
2
3
4
5
6
7
8
9
10
11
struct semid_ds      
{
struct ipc_perm sem_perm;
struct sem *sem_base; /* 指向信号量数组的指针 */
struct sem_queue *sem_pending;
struct sem_queue **sem_pending_last;
struct sem_undo *undo;
time_t sem_otime; /* 最后一次操作的时间 */
time_t sem_ctime; /* 最后一次改变此结构的时间 */
unsigned short sem_nsems; /* 集合中信号量个数*/
};
1
2
3
4
5
6
7
struct sem
{
pid_t sempid; /* 最后操作该信号量的进程ID */
unsigned short semncnt ; /* 等待对该信号量执行P操作的进程数 */
unsigned short semzcnt; /* 等待semval为0的进程数 */
unsigned short semval; /* 信号量当前值 */
}

信号量集操作

头文件:

1
2
3
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
  1. 创建或打开信号量集对象:semget函数

  2. 信号量集操作(信号量的PV操作):semop函数,semtimedop函数

  3. 信号量集控制(信号量初始化和删除操作):semctl函数

semget函数

man 2 semget 创建或打开信号量集对象

int semget(key_t key, int nsems, int semflg);

  • key:用于创建或打开信号量集对象时指定的key值(约定或通过ftok函数创建)或者0(IPC_PRIVATE)—— 创建一个只有创建进程可以访问的信号量。
  • nsems:信号量集对象中包含的信号量数量(例如取值为1,则信号量集只包含1个信号量)
  • semflg:设置访问权限,取值可以为以下某个值或多个值的或
    • IPC_CREAT :如果消息队列对象不存在则创建,否则打开已经存在的消息队列对象,创建还需同文件一样设置权限,如IPC_CREAT | 0664
    • IPC_EXCL:只有消息队列对象不存在的时候,才能创建新的消息队列对象,否则就产生错误EEXIST
  • 返回值:信号量集对象ID;失败返回-1

semop函数

man 2 semop 信号量PV操作

1
2
3
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *timeout); // 限时
  • semid:信号量集对象ID(semget的返回值)

  • sops:指向sembuf结构体数组(ssembuf自己定义,样式如下)的指针

    1
    2
    3
    4
    5
    6
    7
    8
    struct sembuf {
    unsigned short sem_num; //信号量序号,指示本次是操作信号量集中的哪个信号量(序号从0开始)
    short sem_op; //信号量操作码
    /*该值为正,信号量V操作,增加信号量的值(为n,则加n)
    该值为负,信号量P操作,减小信号量的值(为-n,则减n)
    该值为0,对信号量的当前值是否为0的测试*/
    short sem_flg; // semop操作控制标志
    }
  • nsops:第二个参数中sembuf结构数组的元素个数

  • 成功返回0;失败返回-1

sem_flgsemop操作进行控制,主要有2个控制标志:

  • IPC_NOWAIT
    • 当指定的PV操作不能完成时,进程不会被阻塞,semop函数立即返回。返回值为-1,errno置为EAGAIN
    • 例如:信号量值在P操作后小于0,如果操作控制标志没有设置IPC_NOWAIT,则将调用进程阻塞,semop函数将不会返回直到资源可用为止;若设置了IPC_NOWAIT,则semop函数直接返回,调用进程将不会阻塞
  • SEM_UNDO
    • 进程异常退出时,执行信号量解除(undo)操作
    • 例如:进程执行了P操作后异常退出,如果操作控制标志设置了SEM_UNDO,则内核会对该进程执行V操作,保证安全性

semctl函数

man semctl 信号量初始化和删除操作

int semctl(int semid, int semnum, int cmd, ...); (变参,三个或四个)

  • semid:信号量集对象的ID(semget的返回值)

  • semnum:信号量集中信号量的编号(如果控制是针对整个信号量集,则将该值设置为0)

  • cmd:要执行的控制命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 针对 整个信号量集 的控制命令主要包括:
    IPC_RMID //删除
    IPC_SET //设置ipc_perm参数
    IPC_STAT //获取ipc_perm参数
    IPC_INFO //获取系统信息

    // 针对信号量集中 某个信号量 的控制命令主要包括:
    SETVAL //设置信号量的值(一般用于信号量初始化时设置初始值)
    GETVAL //获取信号量的值
    GETPID //获取信号量拥有者进程的PID值
  • arg:与控制命令配合的参数(可选)

    1
    2
    3
    4
    5
    6
    union semun {
    int val; /* Value for SETVAL */
    struct semid_ds *buf; /*Buffer for IPC_STAT,IPC_SET */
    unsigned short *array;/* Array for GETALL, SETALL */
    struct seminfo *__buf; /* Buffer for IPC_INFO */
    };
  • 返回值:成功返回值大于或等于0;失败返回值-1

semctl函数的控制命令通常为以下两种情况:

  • SETVAL:用来把信号量集中的某个信号量初始化为一个给定值, 这个值通过arg参数(union semun中的val成员)来指定
  • IPC_RMID:用于删除信号量集对象,此时arg参数无需赋值
  • GETVAL:返回值就是get得到的信号量的值

代码示例

父子进程

父子进程共享内存映射区的内容,使用信号量实现互斥

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

#define MYKEY 0x1a0a // 创建信号量使用的key值

union semun { // semun 要自己添加
int val;
struct semid_ds *buf;
unsigned short *arry;
struct seminfo *__buf;
};

/*信号量初始化(赋值函数)*/
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value; // init_value为初始值
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
{
perror("Initialize semaphore\n");
return -1;
}
return 0;
}

/*从系统中删除信号量的函数*/
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
{
perror("Delete semaphore\n");
return -1;
}
return 0;
}

/*P 操作函数*/
int sem_p(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0; //信号量编号,单个信号量的编号为0
sem_b.sem_op = -1; //信号量操作,-1 为 P操作
sem_b.sem_flg = SEM_UNDO; //在进程没释放信号量而退出时,系统自动释放该进程中未释放的信号量
if(semop(sem_id, &sem_b, 1) == -1) //进行P操作
{
perror("V operation\n");
return -1;
}
return 0;
}

/*V 操作函数*/
int sem_v(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0; //信号量编号,单个信号量的编号为0
sem_b.sem_op = 1; //信号量操作,1 为 v操作
sem_b.sem_flg = SEM_UNDO; //在进程没释放信号量而退出时,系统自动释放该进程中未释放的信号量
if(semop(sem_id, &sem_b, 1) == -1) //进行V操作
{
perror("V operation\n");
return -1;
}
return 0;
}

int main(void)
{
pid_t result;
int sem_id;
/*创建一个信号量*/
//sem_id = semget(ftok(".", 'a'), 1, 0666 | IPC_CREAT);
sem_id = semget(MYKEY, 1, 0666 | IPC_CREAT);
init_sem(sem_id, 1);

//内存映射
int *p = (int*)mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
if(p == MAP_FAILED) {
perror("mmap error");
exit(-1);
}

/*调用fork()函数*/
result = fork();
if(result == -1)
{
perror("fork\n");
}
else if(result == 0) /*返回值为0代表子进程*/
{
sem_p(sem_id);
printf("Child progress will wait for some seconds...\n");
*p = 0;
printf("Child change mmap int = %d\n", *p);
printf("The returned value is %d in the child progress(PID=%d)\n", result, getpid());
sem_v(sem_id);
}
else /*返回值大于0代表父进程*/
{
sem_p(sem_id);
printf("The returned value is %d in the father progress(PID=%d)\n", result, getpid());
*p = 1;
printf("Parent change mmap int = %d\n", *p);
sem_v(sem_id);
wait(NULL);
del_sem(sem_id);
if(munmap((void*)p, sizeof(NULL)) == -1) {
perror("munmap error");
exit(-1);
}
}
return 0;
}

image-20220611224521264

无关系进程

不同进程间创建相同信号量需要判定是否已存在,若已存在不能重新初始化

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

union semun
{
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
unsigned short *array; /* array for GETALL & SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
};

int sem_p(int semid, int semnum); // P操作
int sem_v(int semid, int semnum); // V操作

int main(int argc, char **argv)
{
int semid, ret;
union semun arg;
key_t key = ftok("/dev/null", 0x04);
if (key < 0) {
perror("ftok key error");
return -1;
}
// 1.创建信号量 (创建了三个信号量,实际只用0号信号量)
semid = semget(key, 3, IPC_CREAT | 0600 | IPC_EXCL);
if (semid == -1 && errno == EEXIST) { // 若信号已存在
semid = semget(key, 3, 0600);
printf("sem exists\n");
}
else { // 若信号量不存在
// 对0号信号量设置初始值
printf("init semid\n");
arg.val = 1;
ret = semctl(semid, 0, SETVAL, arg);
if (ret < 0) {
perror("semctl error");
semctl(semid, 0, IPC_RMID, arg);
return -1;
}
}

// 3.打印当前0号信号量的值
ret = semctl(semid, 0, GETVAL, arg);
// 4.开始P操作
printf("P operate begin\n");
if (sem_p(semid, 0) < 0)
{
perror("P operate error");
return -1;
}
printf("P operate end\n");
ret = semctl(semid, 0, GETVAL, arg);
printf("after P sem[0].val= %d\n", ret);
// 5.延时10s (这个时间段内去执行另一个进程sem2,会在它的P操作那阻塞等待sem执行完V操作)
sleep(10);
time_t tNow = time(NULL);
printf("delay 10S,now time is:%s\n", ctime(&tNow));
// 6.开始V操作
printf("V operate begin\n");
if (sem_v(semid, 0) < 0)
{
perror("V operate error");
return -1;
}
printf("V operate end\n");
ret = semctl(semid, 0, GETVAL, arg);
// 7.移除信号量
// 轮询 是否信号量被使用
while(1) {
ret = semctl(semid, 0, GETVAL);
if(ret == 1 || ret == -1) break;
printf("process is using semid %d, wait...\n", semid);
sleep(1);
}
ret = semctl(semid, 0, IPC_RMID, arg);

if(ret == -1 && errno == EIDRM) {
printf("semid had been removed\n");
}
else {
printf("remove semid success\n");
}
return 0;
}

//对信号量数组semnum编号的信号量做P操作
int sem_p(int semid, int semnum)
{
struct sembuf op;
op.sem_num = 0;
op.sem_op = -1;
op.sem_flg = SEM_UNDO;
return (semop(semid, &op, 1));
}

//对信号量数组semnum编号的信号量做V操作
int sem_v(int semid, int semnum)
{
struct sembuf op;
op.sem_num = 0;
op.sem_op = +1;
op.sem_flg = SEM_UNDO;
return (semop(semid, &op, 1));
}

image-20220612141517746

分别用两个终端运行./IPC_sem,等待约二十秒查看结果,注意删除信号量之前睡眠15秒是为了等待另一进程完成PV操作,即删除信号量时需保证所有使用该信号量的进程完成PV操作,否则会出错

共享内存

  • 共享内存是内核为进程间通信创建的特殊内存段
  • 不同进程可以将同一段共享内存连接到自己的地址空间
  • 最快的进程间通信方式
  • 本身不具有互斥访问机制

image-20220410144858118

共享内存操作

头文件:

1
2
#include <sys/ipc.h>
#include <sys/shm.h>
  1. 打开或创建共享内存对象:shmget函数
  2. 将共享内存连接到进程空间:shmat函数

  3. 断开进程空间和共享内存的连接:shmdt函数

  4. 共享内存控制操作:shmctl函数

shmget函数

int shmget(key_t key, size_t size, int shmflag)

  • key:创建或打开共享内存对象时指定的key值(提前约定或通过ftok函数创建)或者0(IPC_PRIVATE)—— 创建一个只有创建进程可以访问的信号量。

  • size:指定创建的共享内存大小(首次创建共享内存对象时通过该参数指定共享内存段的大小)

  • shmflag:设置共享内存的访问权限 ,取值可以为以下一个或多个值的或

    1
    2
    3
    4
    #define IPC_CREAT 01000	//如果消息队列对象不存在则创建,否则打开已经存在的消息队列对象
    #define IPC_EXCL 02000 //只有消息队列对象不存在的时候,才能创建新的消息队列对象,否则就产生错误
    #define IPC_NOWAIT 04000 //如果操作需要等待,则直接返回错误
    //还有9 bits的权限 如 0644
  • 成功返回0,失败-1

shmat函数

void *shmat(int shmid, const void *shmaddr, int shmflg);

  • shmid:共享内存对象ID
  • shmaddr:指明共享内存连接到的进程空间地址;通常指定为NULL,让Linux系统决定共享内存连接到进程空间中的哪个地址
  • shmflg:可以设置以下两个标志位之一或者不设置(值为0)
    • SHM_RND( addr参数指定的地址应被规整到内存页面大小的整数倍)
    • SHM_RDONLY(共享内存连接到进程空间时被限制为只读)
  • 成功返回共享内存在进程空间中的连接地址;失败返回 -1

shmdt函数

int shmdt(const void *shmaddr);

  • 从调用进程的地址空间中,取消由shmaddr参数所指向的,共享内存映射区域
  • shmaddr:共享内存在进程空间中的连接地址,一般为shmat函数返回的地址。
  • 内核将该共享内存的加载计数减1
  • 成功返回0;失败返回-1

shmctl函数

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

  • shmid:共享内存对象ID
  • cmd:执行的控制命令
    • IPC_RMID,从系统中删除该共享内存对象
    • IPC_STAT,获取共享内存对象的内核结构值
    • IPC_SET,设置共享内存对象的内核结构值
  • buf:指向shmid_ds结构的指正,当控制命令为IPC_STATIPC_SET时,用于获取或设置共享内存对象的内核结构
  • 成功返回0;失败返回-1

生产者消费者示例

问题描述:进程之间通过共享缓冲池(包含一定数量的缓冲区)交换数据。其中,“生产者”进程不断写入,而“消费者”进程不断读出;任何时刻只能有一个任务可对共享缓冲池进行操作。

  • 进程之间的共享缓冲池可以通过共享内存机制实现

消费者读取共享内存:consumer.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>

struct BufferPool
{
char Buffer[5][100]; // 5 个缓冲区
int Index[5]; //缓冲区状态
// 0 表示 对应的缓冲区未被生产者使用,可生产但不可消费
// 1 表示 对应的缓冲区已被生产者使用,不可生产但可消费
};

int main()
{
int running = 1;
void *shm = NULL;
struct BufferPool *shared;
int shmid;
int index = 0;
srand(time(NULL)); // 设置随机数种子
shmid = shmget((key_t)1234, sizeof(struct BufferPool), 0666 | IPC_CREAT);
if(shmid == -1) {
exit(EXIT_FAILURE);
}
shm = shmat(shmid, 0, 0);
if(shm == (void*)-1) {
exit(EXIT_FAILURE);
}
shared = (struct BufferPool*)shm;
while(running) {
printf("%d, %d\n", index, shared->Index[index]);
if(shared->Index[index] == 1) {
printf("consume buffer: %s", shared->Buffer[index]);
shared->Index[index] = 0;
if(strncmp(shared->Buffer[index], "end", 3) == 0) {
running = 0;
}
index = (index + 1) % 5;
sleep(rand() % 3);
}
else {
printf("buf is empty\n");
sleep(1);
}
}
if(shmdt(shm) == -1) {
exit(EXIT_FAILURE);
}
if(shmctl(shmid, IPC_RMID, 0) == -1) {
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

生成者写入共享内存:producer.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>

struct BufferPool
{
char Buffer[5][100]; // 5 个缓冲区
int Index[5]; //缓冲区状态
// 0 表示 对应的缓冲区未被生产者使用,可生产但不可消费
// 1 表示 对应的缓冲区已被生产者使用,不可生产但可消费
};

int main()
{
int running = 1;
void *shm = NULL;
struct BufferPool *shared;
char buffer[100];
int shmid;
int index = 0;
shmid = shmget((key_t)1234, sizeof(struct BufferPool), 0666 | IPC_CREAT);
if(shmid == -1) {
exit(EXIT_FAILURE);
}
shm = shmat(shmid, NULL, 0);
if(shm == (void*)-1) {
exit(EXIT_FAILURE);
}
printf("Memory attached at %X\n", (int*)shm);
shared = (struct BufferPool*)shm;
printf("puts in end to over\n");
while(running) {
printf("%d, %d\n", index, shared->Index[index]);
if(shared->Index[index] == 1) {
printf("buf is full, wait 1s ...\n");
sleep(1);
}
else {
printf("Enter some text:");
fgets(buffer, 100, stdin);
strncpy(shared->Buffer[index], buffer, 100);
shared->Index[index] = 1;
index = (index + 1) % 5;
if(strncmp(buffer, "end", 3) == 0) {
running = 0;
}
}
}
if(shmdt(shm) == -1) {
exit(EXIT_FAILURE);
}
if(shmctl(shmid, IPC_RMID, 0) == -1) {
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

image-20220612153541618

进程组

概念

进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理

进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。

当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID\==第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID\==其进程ID

可以使用kill -SIGKILL -进程组ID来将整个进程组内的进程全部杀死。

组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。

一个进程可以为自己或子进程设置进程组ID

操作函数

man 2 setpgid

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <unistd.h>

int setpgid(pid_t pid, pid_t pgid);

pid_t getpgid(pid_t pid);

pid_t getpgrp(void);

getpgrp函数

获取当前进程的进程组ID

getpgid函数

获取指定进程的进程组ID。如果pid = 0,那么该函数作用和getpgrp一样。

setpgid函数

改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。

  1. 如改变子进程为新的组,应fork后,exec前。

  2. 权级问题。非root进程只能改变自己创建的子进程,或有权限操作的进程

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(-1);
}
else if(pid == 0)
{
printf("child PID == %d\n", getpid());
printf("child Group ID == %d\n", getpgid(0)); // 返回组id
//printf("child Group ID == %d\n", getpgrp()); //返回组id
sleep(7);
printf("----Group ID of child is changed to %d\n", getpgid(0));
exit(0);
}
else
{
sleep(1);
setpgid(pid, pid);

sleep(10);
printf("\n");
printf("parent PID == %d\n", getpid());
printf("parent's parent process PID == %d\n", getppid());
printf("parent Group ID == %d\n", getpgid(0));

sleep(5);
setpgid(getpid(), getppid());
printf("parent Group ID == %d\n", getpgid(0));
}
return 0;
}

image-20220608150818204

会话

创建会话

创建一个会话需要注意以下6点注意事项:

  1. 调用进程不能是进程组组长,该进程变成新会话首进程(session header)

  2. 该进程成为一个新进程组的组长进程。

  3. 需有root权限(ubuntu不需要)

  4. 新会话丢弃原有的控制终端,该会话没有控制终端

  5. 该调用进程是组长进程,则出错返回

  6. 建立新会话时,先调用fork,父进程终止,子进程调用setsid

getsid函数

man 2 getsid

获取进程所属的会话ID

1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t getsid(pid_t pid);

pid为0表示察看当前进程session ID

成功:返回调用进程的会话ID;失败:-1

ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。

组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。

setsid函数

man 2 setsid

创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。

1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);

成功:返回调用进程的会话ID;失败:-1

调用了setsid函数的进程,既是新的会长,也是新的组长。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid;

if((pid = fork()) < 0)
{
perror("fork");
exit(-1);
}
else if (pid == 0)
{
printf("child process PID is %d\n", getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n", getsid(0));

sleep(10);
setsid();
printf("Changed:\n");

printf("child process PID is %d\n", getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n", getsid(0));
exit(0);
}
return 0;
}

image-20220608153322335

守护进程

Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字(如vsftpdhttpdsshd等)。

Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。

创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。

创建守护进程模型

  1. 创建子进程,父进程退出 fork()

​ 所有工作在子进程中进行形式上脱离了控制终端

  1. 在子进程中创建新会话 setsid()

   使子进程完全独立出来,脱离控制终端

  1. 改变当前目录为根目录 chdir()

   防止占用可卸载的文件系统,也可以换成其它路径

  1. 重设文件权限掩码 umask()

   防止继承的文件创建屏蔽字拒绝某些权限,增加守护进程灵活性

  1. 重定向文件描述符0/1/2 —> /dev/null dup2()

   继承的打开文件不会用到,浪费系统资源,无法卸载

  1. 开始执行守护进程核心工作

  2. 守护进程退出处理程序模型

创建示例

daemon.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

int main()
{
pid_t pid;
pid = fork(); // 1.创建子进程

if(pid > 0) return 0;

pid_t sid = setsid(); // 2.创建新会话,返回会话ID

int ret = chdir("/home/yang"); // 3.改变工作路径
if(ret < 0)
{
perror("chdir");
exit(-1);
}

umask(0002); // 4.设置文件掩码

//5.重定向文件描述符0/1/2 --> /dev/null
close(STDIN_FILENO);
open("/dev/null", O_RDWR);
dup2(0, STDOUT_FILENO);
dup2(0, STDERR_FILENO);

while(1)
{
// 6.核心工作
}

return 0;
}

欢迎关注我的其它发布渠道