0%

Linux 系统编程

Linux 系统编程入门

什么是系统编程

系统编程范畴

  • 内核态系统编程: Linux内核编程
  • 用户态系统编程: 系统调用

系统调用

  • 将留给应用程序的内核API接口统一管理
  • 硬件访问权限、特权指令
  • Linux系统调用接口
    • 文件IO、目录、文件系统
    • 进程、线程、进程间通信
    • 时间、信号、网络
    • 内存管理

一个系统编程例子(文件基本操作)

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 <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
int fd;
fd = open("hello.txt", O_RDWR|O_CREAT, 0666);
if (fd == 1) {
printf("open file failed!");
return -1;
}

char string[20] = "hello world!\n";
write(fd, string, 14);
fsync(fd);

char * buf = (char*)malloc(20);
memset(buf, 0, 20);
lseek(fd, 0, SEEK_SET);
read(fd, buf, 14);
printf("%s", buf);

free(buf);
close(fd);

return 0;
}

实现 cp 命令

基本流程:

  • 打开源文件
  • 打开目标文件
  • 读取源文件数据
  • 写入到目标文件
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
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

#define BUFFERSIZE 4096


int main(int argc, char * argv[]) {
if (argc != 3) {
printf("usage:\n copy src dst\n");
return 1;
}
int srcfd = open(argv[1], O_RDONLY);
if (srcfd == -1) {
perror("open");
return 1;
}
int dstfd = open(argv[2], O_CREAT|O_WRONLY, 0666);
if (dstfd == -1) {
perror("open");
return 1;
}

int len = 0;
char buffer[BUFFERSIZE] = {0};

while ((len = read(srcfd, buffer, BUFFERSIZE)) > 0) {
if ( write(dstfd, buffer, len) != len ) {
perror("write error");
return 2;
}
}
if (len < 0) {
perror("read error");
return 3;
}

close(srcfd);
close(dstfd);

return 0;
}

系统调用与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
#include <stdio.h>
#include <stdlib.h>

struct student {
char name[10];
int age;
float score;
} stu[2];

int main(void) {
for (int i = 0; i < 2; i ++) {
printf("please input name age score: \n");
scanf("%s%d%f", stu[i].name, &stu[i].age, &stu[i].score);
}

FILE * fp;
if ((fp = fopen("hello.dat", "w+")) == NULL) {
printf("fopen failed!\n");
return -1;
}
fwrite(&stu, sizeof(struct student), 2, fp);
if (ferror(fp) != 0) {
printf("fwrite failed!\n");
clearerr(fp);
return -1;
}

fflush(fp);
rewind(fp);

struct student* buf = (struct student *) malloc(2 * sizeof(struct student));

fread(buf, sizeof(struct student), 2, fp);
if (ferror(fp) != 0) {
printf("fread failed!\n");
clearerr(fp);
return -1;
}
printf("姓名\t年龄\t分数\n");
for (int i = 0; i < 2; i ++) {
printf("%s\t%d\t%f\n", buf[i].name, buf[i].age, buf[i].score);
}
fclose(fp);
free(buf);
buf = NULL;

return 0;
}

Linux系统调用基本流程

  • 软中断 : x86下的 int 0x80, ARM架构下的 SWI软中断指令
    • 开始CPL为3,根据IDT表中的段选择符和偏移地址合在一起设置成新的PC,更新CS和IP后,CS末尾的CPL设置为0,特权级变为0,可以进入内核态了、
  • 处理器会将当前的用户栈指针和程序计数器等信息保存在内核栈中,并跳转到系统调用处理程序的入口点
    • 寄存器传递相关的参数:打开的文件名、参数、系统调用号
  • 进入内核态,执行内核特权指令代码函数
  • 返回值保存到相应寄存器中
  • 返回到用户态、恢复上下文,切换用户栈,系统调用结束

strace 命令

基本功能

  • 监控用户进程和内核进程的交互
  • 追踪进程的系统调用、信号传递、状态变化

错误处理

  • 系统编程错误一般通过函数的返回值来表示
  • 执行成功,返回 0 或 正确值
  • 执行失败,返回-1,并把系统全局变量 errno 赋值,指示具体错误
  • 全局变量 errno 由操作系统维护:当系统调用或调用库函数出错时,会重置该值

可以通过 perror() 根据 errno 值来打印具体的错误

GNU编码风格

函数顶头写

空格

使用空格的地方

  • 左括号之前
  • 逗号之后
  • 运算符两边

对齐

多行对齐

命名

  • 函数名、变量名用小写字母、下划线进行分割
  • 宏、枚举常量使用大写字母定义
  • 函数命名:open_door
  • Windows风格:OpenDoor

Linux哲学:一切皆文件

Linux设计思想:

  • 一个程序只实现一个功能,多个程序组合完成复杂的功能
  • 数据配置在文本文件中
  • 机制与策略
  • 一切皆文件

广义上的文件

  • 普通文件
  • 目录
  • 特殊设备文件:字符设备、块设备、网络设备
  • 套接字
  • 进程、线程
  • 命名管道

VPS

虚拟文件系统

  • virtual file system
  • 对不同的文件系统进行抽象
  • 提供一个统一的操作接口

文件描述符

基本概念:

  • file descriptor 简称 fd ,用来描述一个已经打开的文件索引(非负整数)
  • 当 fd 为负时,表示文件打开失败

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
#include <string.h>

int main() {
char buf[20] = {0};
read(0, buf, 15);
write(1, buf, strlen(buf));

return 0;
}

编程实战:音频播放器

设备文件

  • 普通文件可以通过文件名与实际的存储数据进行关联
  • 设备文件通过设备结点与具体的物理设备进行关联
  • 设备号:主设备号 + 次设备号组成
  • 设备文件存放在 /dev 目录下
  • 设备结点可以自动创建、也可以手工创建

常见的设备文件

设备文件 dsp

  • 打开声卡设备文件 /dev/dsp
  • 打开音频数据文件 xx.wav
  • 从音频文件读一段数据
  • 写入声卡设备文件
  • 关闭文件
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
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>

#include <sys/ioctl.h>
#include <linux/soundcard.h>

int main (void)
{
int dst_fd;
dst_fd = open ("/dev/dsp", O_WRONLY, 0667); /*open dsp*/
if (dst_fd == -1)
{
perror ("open /dev/dsp error!");
return -1;
}

int src_fd;
src_fd = open ("./fruit.wav", O_RDONLY, 0666);
if (src_fd == -1)
{
perror ("open wav file failed!");
close (dst_fd);
return -2;
}
int read_count;
char buf[1024];
while ((read_count = read(src_fd, buf, 1024)) > 0)
{
if (write(dst_fd, buf, read_count) != read_count)
{
perror("write /dev/dsp error");
return -3;
}
}
if (read_count < 0)
{
perror("read wav file error");
return -4;
}
close(src_fd);
close(dst_fd);
return 0;
}

通过 ioctl系统调用配置声卡后改进

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
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>

#include <sys/ioctl.h>
#include <linux/soundcard.h>

int main (void)
{
int dst_fd;
dst_fd = open ("/dev/dsp", O_WRONLY, 0666); /*open dsp*/
if (dst_fd == -1)
{
perror ("open /dev/dsp error!");
return -1;
}

int rate = 44100;
ioctl (dst_fd, SNDCTL_DSP_SPEED, &rate);
int channels = 2;
ioctl (dst_fd, SNDCTL_DSP_CHANNELS, &channels);
int format = 16;
ioctl (dst_fd, SNDCTL_DSP_SETFMT, &format);

int src_fd;
src_fd = open ("./fruit.wav", O_RDONLY, 0666);
if (src_fd == -1)
{
perror ("open wav file failed!");
close (dst_fd);
return -2;
}
int read_count;
char buf[1024];
while ((read_count = read(src_fd, buf, 1024)) > 0)
{
if (write(dst_fd, buf, read_count) != read_count)
{
perror("write /dev/dsp error");
return -3;
}
}
if (read_count < 0)
{
perror("read wav file error");
return -4;
}
close(src_fd);
close(dst_fd);
return 0;
}

揭开文件系统的神秘面纱

文件系统

基本概念

  • 一切皆文件:磁盘文件、设备文件、FIFO
  • 文件系统:对文件进行管理的程序

文件主要对数据进行封装和管理,而文件系统就是对文件进行管理的程序。

  • 数据以文件形式封装
  • 以统一文件接口进行读写

文件在磁盘上的存储

磁盘

磁盘原理

磁盘控制器驱动

磁盘接口

  • SATA
  • IDE
  • SCSI

文件数据存储

扇区与地址

  • 每个文件存在一块连续的扇区上
  • 一个字节的文件也要占用扇区的整数倍

文件信息存储

这些信息也叫做文件的元数据

存储抽象

随着技术的发展,因为每个磁盘厂家设置的扇区大小不一致,为了适应不同厂家不同容量的硬盘,需要把存储单元固定下来,需要定义最小读写的逻辑单元

  • 扇区:磁盘的最小读写的物理单元
  • 簇:Windows下文件系统的最小读写逻辑单元
  • 块:Linux 下文件系统的最小读写逻辑单元
    • 一个簇/块的大小等于扇区的2、4、8、16倍
    • 一个文件占用的体积是簇/块的整数倍
    • 簇/块越小,磁盘利用率越高,但存储效率会降低
    • 文件体积越小,簇/块越大,磁盘空间利用率越低

文件在Flash上的存储

磁盘与Flash存储的异同点:

  • 由控制器驱动进行底层读写

    • 磁盘:SCSI命令、SATA命令
    • NAND Flash:芯片读写指令、周期
  • 寻址方式都是多级地址

    • 磁盘:xx磁道, xx磁头, xx扇区
    • Flash: xx块号, xx块内页号, xx页内字节号
  • 都是整片为单位进行读写

    • 磁盘以扇区为最小读写单位,Nand Flash以页为最小读写单位

    • 都可以被文件系统抽象为逻辑块,通过映射层进行地址映射

文件索引结点:inode

逻辑块

文件的存储

两部分:

  • 纯数据区
    • 文件真正的数据存储区、基本存储单位为block
  • 元数据区
    • 文件属性:磁盘中的存储位置、文件长度等信息
    • 时间戳:创建时间、修改时间
    • 读写权限:使用read/write系统调用时,要首先要进行权限检查
    • 所属组、所有者
    • 链接数

文件的元数据区是如何存储的呢?使用inode来进行存储

索引结点:inode

用来存储文件信息

  • inode:每个文件使用一个inode结构体来描述

  • 每个inode有固定编号、有单独的存储空间

  • 每个inode的大小为128/256B

  • Linux系统根据inode来查找文件的存储位置

使用 stat 命令可以查看文件 inode 信息

通过 df -i 命令可以查看某个分区的 inode 总数

索引结点表(inode table)

data block

数据块(逻辑块)

  • 格式化磁盘时划分的文件系统的最小逻辑读写单位
  • 每个block都有自己的编号,inode中存放文件的block地址信息
  • 一个block一般是扇区或页的整数倍:1K/2K/4K

查看某个分区的block信息: df

-h选项用于将大小以人类可读的方式显示,方便查看

存储映射

超级块:superblock

super block

  • 记录整个文件系统的信息
  • 一个inode、block的大小
  • inode使用情况:已使用数量、未使用数量
  • block使用情况:已使用数量、未使用数量
  • 文件系统挂载情况
  • 文件系统的挂载时间、最后一次写入数据、检验磁盘的时间
  • 当文件系统挂载时,这部分信息会加载到内存,并常驻内存

磁盘格式化

两种格式化

  • 物理格式化

    • 磁盘在使用前要进行分区和格式化:MBR中存放分区信息、开机代码(Linux下使用 fdisk命令)

    • 出厂前厂家已经做好的工作:划分磁道、扇区

    • 物理格式化完成后,磁盘就会对应不同的分区(Windows出现C盘、D盘等,Linux下出现/dev/下出现不同的设备结点)

  • 逻辑格式化

    • 使用格式化工具,在磁盘上安装文件系统(每个分区可以安装不同的文件系统,使用对应的文件系统的格式化工具就可以完成安装)
    • 将磁盘划分为不同的block
    • 将磁盘划分为不同的区段

使用文件系统格式化工具进行逻辑格式化,会将整个分区上的物理存储空间划分为不同的区段

  • boot sector:引导扇区

  • Superblock: 记录文件系统的整体信息、inode 和 block 信息

  • block group(将block划分为不同的块组)

    • 每个block group 都有一个 group descriptor,聚集存储在分区开头位置
    • block bitmap: 记录block的使用情况,哪些在使用,哪些是空的
    • inode bitmap:记录inode的使用情况
    • inode table
    • data block存储真正的数据
  • block description: 记录每一个 block group 的起始地址

块组

block group

  • 一个分区在格式化时,可以划分为多个block group

  • 每个block group包含block bitmap、 inode bitmap、inode table、data block

  • 每个block bitmap大小为一个block,每bit表示一个block

  • 每个inode bitmap大小为一个block,每一个bit表示一个inode

group descriptor

  • 存储在superblock的后面

  • 有一个block指针,指向block bitmap

  • 有一个block指针,指向inode bitmap

  • 有一个block指针,指向inode table

  • group descriptor信息存储在superblock中:group descriptor总数等信息

文件在磁盘上的存储

目录和目录项

目录

  • 是一个文件、有自己的 inode ,在 inode 中将文件类型标记为目录
  • 目录存储在 data block
  • 目录本质是一个表格:由若干目录项组成
    • 一个目录下面可以多个文件:文件名和文件对应的 inode
    • 一个目录文件有多个子目录:目录名和其对应的 inode
    • 多个子目录构成树状的文件系统结构

目录项

  • 一个目录项由文件名inode编号组成,根据 inode编号可以找到 inode table 中真正的  inode 结点
  • 目录项存储在 data block

文件路径解析

文件路径

构成

  • 由各个目录、子目录构成

  • 各个路径构成树状结构的文件系统

分类

  • 相对路径
  • 绝对路径

绝对路径

根目录:绝对路径的参考起点

  • Linux内核中的 “/”

  • Windows系统中的盘符

  • 文件系统预留的inode编号:2

相对路径

当前目录:相对目录的参考起点

  • . : 当前目录的硬链接
  • .. : 上级目录的硬链接
  • 根目录下的 . 和 ..
  • 查看当前目录 inode 编号 : ls -i -d

小结

• 一个目录下可以包含多个文件、或者嵌套多个子目录

• 各级目录构成一个路径,应用程序根据该路径来找到文件

• 路径分为绝对路径和相对路径

• 路径的本质:各级目录文件中的目录构成的一个inode链

文件系统的挂载

基本原理

  • 一个磁盘格式化、安装文件系统后,可以通过文件接口访问存储空间
  • 挂载:让磁盘与Linux根文件系统的某个目录建立关联、加入全局文件系统树
  • 用户通过挂载后的路径名来访问文件
  • 挂载点(mount point)是进入该挂载设备文件系统的入口

实验-目录挂载

udisk 挂载到 mnt

通过 sudo mount --bind udisk/ mnt/ 实现

然后在 mnt中新建 hello.c

可以看到两个目录都存在 hello.c

然后进行卸载 umount mnt/

这样数据文件就只存放在 udisk中了

解除挂载后,在 mnt 目录下新建 mnt.c,然后再进行挂载

可以看到挂载点的东西被屏蔽掉了

挂载过程

结构体:vfsmount\superblock

  • 每个挂载的文件系统,VFS都会创建一个vfsmount(用来表示挂载的文件系统)、super_block 对象
  • 该对象描述了文件系统 mount 的所有信息
  • 父文件系统的挂载点:vfsmount->mnt_mountpoint = /mnt
  • 子文件系统的根目录:vfsmount->mnt_root = superblock->s_root
  • 初始化好 vfsmount 对象后,将该对象添加到 VFSMOUNT hash table中

文件路径解析

目录项

  • 若目录项 dentry 标记为 DCACHE_MOUNTED,路径解析时对该目录 /mnt 项屏蔽
  • 计算该目录的 HASH 值, 根据值去 VMSMOUNT hash table 中查找对应的 vfsmount 对象
  • 根据 vfsmount->mnt_root,找到子文件系统的根目录
  • 查找子文件系统指定目录下的文件

文件系统的类型

虚拟文件系统:VFS

VFS

虚拟文件系统

  • virtual file system
  • 基于内核和存储设备之间的抽象层
  • 向上:统一封装了不同设备、文件系统的读写接口:系统调用API
  • 向下:新的设备、文件系统添加到Linux内核,实现VFS接口即可
  • Linux内核支持60+种不同类型的文件系统

Linux系统中的VFS

VFS对象类型

对象类型

  • super_block: 已经安装的具体的文件系统
  • inode: 代表一个具体的文件
  • dentry: 目录项,目录项路径的组成部分
  • file: 进程打开的文件

对象方法

  • super_operations: alloc_inode、write_inode、destroy_inode
  • inode_operations: link、mknod、mkdir、rename、create
  • dentry_opertions: d_hash、d_compare、d_delete、d_release
  • file_operations: read、write、open、close、fsync、mmap

通过OOP理解VFS

VFS中面向对象的思想

  • 封装:file->file_operations
  • 继承:write_inode、ext2_write_inode
  • 多态:callback

文件描述符

程序中如何操作文件

文件描述符

  • Linux进程使用文件描述符(file descriptors,简称fd)来操作文件
  • int fd = open (“/home/wit/hello.txt”, O_WRONLY, 0666);
  • read (fd, buf, 100);

结构体files_struct

  • 用来表示一个进程打开的文件列表
1
2
3
4
5
6
7
8
9
10
11
12
struct files_struct {
atomic_t count;
bool resize_in_progress;
wait_queue_head_t resize_wait;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u; //所有打开的文件构成一个系统级的全局双链表
struct path f_path;
struct inode *f_inode; //inode节点
const struct file_operations *f_op; //该文件的读写方法
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode; //打开模式
struct mutex f_pos_lock;
loff_t f_pos; //文件当前位置
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
void *private_data;
struct address_space *f_mapping;
};

一个进程打开的文件

文件描述符就是 file指针 在fd_array(进程打开的文件列表)中的偏移

文件指针

通过C库函数操作文件

文件指针

  • C标准库函数

    • C标准库函数使用文件指针来操作文件

    • FILE *fp = fopen(“/home/wit/hello.txt”, “w+”);

    • fread (buf, 4, 100, fp);

C语言中的文件指针FILE*

对文件描述符的封装

  • 文件描述符:fd

  • 文件位置:f_fops

  • 缓冲区

    • 为了提高效率,会在用户空间申请缓冲区
  • 文件标志:f_mod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct _IO_FILE FILE;
struct _IO_FILE {
int _flags;
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; // 文件描述符
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
};

标准流

硬链接和软链接

链接

Linux下一种文件共享的方式,类似于Windows下的快捷方式

一个文件使用多个别名:多个文件名共享一个inode

硬链接

硬链接和文件有相同的 inode 和 data block

软链接

  • 软链接是一个普通文件,文件内容为:指向文件的路径名
  • 有自己的 inode编号 和 data block ,文件权限、属性…
  • 删除软链接并不会影响到其指向文件本身
  • Windows 的快捷方式

实验

给文件创建一个硬链接

1
ln hello.c hello-hardlink.c

使用 ls -i 命令看到两个文件 inode编号 相同

给文件创建一个软链接

使用 ln -s 命令创建一个软链接

1
ln -s hello.c hello-softlink.c

软链接和硬链接的区别

硬链接

  • 只能对存在的文件创建硬链接
  • 不能对目录创建硬链接
  • 创建硬链接不能跨越文件系统分区

软链接

  • 可以对不存在的文件或目录创建软链接
  • 可以对目录创建软链接
  • 可以跨越文件系统分区创建软链接

为什么硬链接不能跨越文件系统或分区创建? 为什么软链接可以跨越文件系统或分区创建?

来自chatGPT的回答:在Linux和其他类UNIX系统上,硬链接是一种文件系统对象,它允许多个文件名引用同一个物理文件。硬链接与原始文件具有相同的inode号码,因此它们实际上是指向同一个数据块的不同文件名。

然而,硬链接不能跨越文件系统或分区创建是因为不同的文件系统或分区有不同的inode号码空间。在一个文件系统或分区中,每个inode号码都是唯一的,但是在不同的文件系统或分区中,相同的inode号码可能会被分配给不同的文件。因此,如果尝试在不同的文件系统或分区中创建硬链接,则会遇到inode号码冲突的问题,因此硬链接不支持跨越文件系统或分区创建。

相反,软链接(也称为符号链接)可以跨越文件系统或分区创建,因为它们只是一个指向另一个文件的符号引用,它们与原始文件具有不同的inode号码。在创建软链接时,文件系统会创建一个新文件,该文件包含指向原始文件的路径名。因此,软链接不受inode号码空间的限制。

来自Claude的回答:硬链接不能跨越文件系统或分区创建的原因是:硬链接实际上是指向同一个inode的多个目录项。inode存储在文件系统的inode表中,记录了文件的元信息和数据块索引信息。不同的文件系统或分区有独立的inode表,彼此之间的inode编号没有对应关系。所以,如果在不同的文件系统或分区创建硬链接,实际上会创建两个不同的inode,而不是指向同一个inode。这就失去了硬链接的意义。举个例子,假设有两个分区,分区1中有一个文件file,其inode号是12345。现在在分区1中创建一个硬链接link1指向file,然后在分区2中也创建一个名为link2的硬链接指向file。实际上,link2指向的不是分区1的inode 12345,而是分区2中的一个新的inode,因为两个分区的inode表是独立的。所以link1和link2指向的并不是同一个文件,这就违背了硬链接的定义。硬链接创建失败。硬链接依赖单个文件系统共享的inode资源来实现多个文件名指向同一文件。一旦跨文件系统或分区,这个前提就不复存在了。相比之下,软链接则可以跨文件系统与分区创建,这是因为软链接包含的是一个路径,path可以跨越文件系统与分区,指向不同目录下的文件。软链接的实现与文件系统或分区的内部机制无关,它只是一个包含路径的文件。而硬链接则依赖文件系统的inode机制,这也是二者最大的区别所在。所以,总结起来,硬链接不能跨文件系统或分区创建的原因有:1. 不同文件系统或分区有独立的inode表,inode编号无对应关系。2. 硬链接依赖共享同一个inode来实现同步更新多个文件名指向的文件。跨文件系统/分区时,无法找到同一个inode。3. 硬链接的本质是多个目录项指向同一文件,要求在同一个文件命名空间内。不同文件系统/分区有独立的文件命名空间。理解硬链接与软链接的区别与实现机制,是Unix/Linux系统管理员必备的知识点。这也有助于我们更深入学习文件系统与存储管理的原理。

为什么不能不能对目录创建硬链接?

来自 Claude 的回答:

  1. 硬链接实际上是多个目录项指向同一个inode。如果对目录创建硬链接,当目录中的内容发生变化时,由于硬链接的同步更新性质,可能导致目录层次混乱或循环引用。这违反了典型的目录树结构。

  2. 目录中包含的条目会随子目录和文件创建与删除动态变化。如果有多个硬链接指向同一目录,那么 inode 中的数据块索引信息就会因此无限增加,极易出现inode数据块溢出的情况。这影响文件系统的稳定性。

  3. 不同硬链接之间,子目录和文件是共享的。这可能导致权限管理混乱,从而威胁系统安全性。如果删除某个硬链接目录,会导致其他硬链接下的文件也被删除,这违背常理。

  4. 硬链接的存取时间等都与源目录相同。但目录作为一个独立的文件系统对象,其元信息应独立于其他硬链接。这也体现了目录在模型抽象级别上应该作为一个独立实体。

  5. 许多文件系统的设计和实现都假定目录无法产生循环,目录树结构必须是典型的单向树形结构。如果允许对目录创建硬链接,这一前提将不复存在,需要对文件系统的逻辑进行重大修改。

所以,总结来说,不能对目录创建硬链接的主要原因是:1) 它违反典型的目录树结构与父子关系。2) 会导致inode数据块索引信息无限增长,威胁文件系统稳定性。3) 会导致权限管理混乱,影响系统安全性。4) 硬链接具有同步更新的性质,但目录需要作为独立文件系统对象。5) 现有的文件系统设计假定目录无法产生循环,不支持对目录创建硬链接。综上,允许对目录创建硬链接将会给文件系统带来难以掌握的副作用与后果。这也违反了Unix/Linux的哲学,即“使简单事情保持简单”。所以在文件系统的设计与实现中,约定俗成地禁止了对目录设置硬链接。理解这一约束的来龙去脉,有助于我们进一步理解Unix/Linux风格的文件系统架构与设计思想。这同样也让我们对日常管理系统文件的行为有所警醒,不会误触这一潜在的“雷区”。所以这也是成为一名优秀系统管理员所必备的知识点与技能。

一些命令

跟 FS 相关

  • mount

  • mkdir/rmdir/chmod/chown/

  • df、du、wc

磁盘管理

统计磁盘使用率:$ df -h

统计目录:$ du

文件管理

• 当前目录下的C文件个数:$ find . -name “*.c” | wc -l

• 当前目录下(包括子目录)的文件个数:$ ls –lR | grep “^-” | wc –l

• 一个项目的总目录个数:$ ls –lR | grep “^d” | wc –l

• 一个项目的代码总行数:$ find . -name “*.c” |xargs cat | wc -l

实验:磁盘格式化及挂载

使用磁盘基本步骤

• 格式化、分区

• 安装文件系统

• 挂载:将磁盘挂载到我们电脑上的某个目录上

分区格式化

当新建一个硬盘,此时在 /dev/ 目录下多了一个 sdb

此时它还没有挂载

直接进行挂载,行不通

需要进行格式化、分区

通过命令来进行格式化,敲入 m 进入菜单

使用 n 添加一个分区

使用 p 添加主分区

剩下的默认即可,完成分区

添加分区后,查看 /dev/

发现多了一个 sdb1

安装文件系统

Linux 有不同的命令安装文件系统

使用 mkfs.ex4 命令安装 ext4 文件系统

划分了不同的 datablock inode superBlock, 此时就安装好了文件系统

挂载

使用 mount 命令挂载到 /mnt 目录下

1
mount -t ext4 /dev/sdb1 /mnt/

此时就可以直接对这块磁盘进行操作了

通过 umount 命令进行卸载

1
umount /mnt/

再次查看 /mnt/ 目录,就会看到原来显示的内容

实验:恢复删除的文件

rm 命令….

文件删除的背后

  • 并没有真正删除数据,当 inode 链接计数 > 1时
  • 仅删除了文件名和 inode 之间的关联:清除了目录项中 inode 指针信息

真正的文件删除

  • 当 inode 的链接计数为1时
  • 将目录项中的文件名和 inode 对应关系删除
  • 将该文件 inode 的 block 指针删除
  • 在 inode bitmap 中将文件 inode 标记为 未用
  • 将 block bitmap 中将文件的 data block 标记为 未用
  • ext 日志文件系统:把删除文件的 inode 信息和 文件名 写入日志

如果 data block 还没有被其它文件占用,还是可以恢复的

删除恢复的文件

基本步骤

• $ apt-get install extundelete

• $ extundelete –inode 2 /dev/sdb1

• $ extundelete /dev/sdb1 –restore-file /home/wit/hello.c

• $ extundelete /dev/sda1 –restore-inode 1122

• $ extundelete /dev/sdb1 –restore-directory /home/wit/test

文件IO编程实战

文件的打开模式

文件的基本操作

系统调用open

文件的打开模式

主参数和副参数可以通过 | 组合使用

使用场景

文件的关闭

当创建不存在的文件时

文件的读写权限

修改读写权限

文件的读写函数

文件读写

read函数解析

编程实战实现cat功能

  1. 打开一个文本文件
  2. 读取文本文件的内容,保存到一个内存缓冲区内
  3. 打印缓冲区里的数据到标准输出终端
  4. 循环第二步,直到文件末尾
  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
31
32
33
34
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
if (argc != 2) {
printf("usage:\n cat filename\n");
return -1;
}

int srcfd = open(argv[1], O_RDONLY);
if (srcfd == -1) {
perror("open error");
return 1;
}
int len;
char buffer[512] = {0};
while ((len = read(srcfd, buffer, 512)) > 0) {
if (len != 512) buffer[len] = '\0';
printf("%s", buffer);pGt
}
printf("\n");

if (len < 0) {
perror("read error");
return 3;
}
close(srcfd);


return 0;
}

文件的读写位置和定位

文件的读写位置

改变文件偏移量

文件空洞

有文件空洞的文件称为稀疏文件

• 字符设备、Socket、终端设备等流式设备不支持lseek

• 管道、FIFO不支持lseek

获取文件的属性信息

文件属性

文件数据的存储

  • 纯数据:存储在 datablock 中
  • 元数据:存储在 inode table 中
  • 文件名:存储在目录文件的目录项中

元数据:

  • 文件时间戳
  • 文件权限
  • 文件所有权
  • 文件存储地址
  • 链接数

获取文件的元数据

结构体:stat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
struct timespec st_atim; /* time of last access */
struct timespec st_mtim; /* time of last modification */
struct timespec st_ctim; /* time of last status change */
};

实现shell命令: ll

getpwuid函数

getgrgid函数

文件元数据

文件类型+权限

测试宏

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
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>

char* find_user_name_by_uid(uid_t uid) {
struct passwd * passwd_ptr;
passwd_ptr = getpwuid (uid);
if (passwd_ptr == NULL) {
static char uid_string[10];
sprintf(uid_string, "%d", uid);
return uid_string;
}
return passwd_ptr->pw_name;
}

char* find_group_name_by_gid (gid_t gid) {
struct group* group_ptr;

group_ptr = getgrgid(gid);
if (group_ptr == NULL) {
static char gid_string[10];
sprintf(gid_string, "%d", gid);
return gid_string;
}
return group_ptr->gr_name;
}

char* change_mode_to_string (mode_t mode) {
static char mode_string[10];

strcpy (mode_string, "----------");
if (S_ISDIR(mode)) mode_string[0] = 'd';
if (S_ISCHR(mode)) mode_string[0] = 'c';
if (S_ISBLK(mode)) mode_string[0] = 'b';
if (S_ISLNK(mode)) mode_string[0] = 'l';
if (S_ISFIFO(mode)) mode_string[0] = 'f';
if (S_ISSOCK(mode)) mode_string[0] = 's';

if (mode & S_IRUSR) mode_string[1] = 'r';
if (mode & S_IWUSR) mode_string[2] = 'w';
if (mode & S_IXUSR) mode_string[3] = 'x';
if (mode & S_IRGRP) mode_string[4] = 'r';
if (mode & S_IWGRP) mode_string[5] = 'x';
if (mode & S_IXGRP) mode_string[6] = 'w';
if (mode & S_IROTH) mode_string[7] = 'r';
if (mode & S_IWOTH) mode_string[8] = 'x';
if (mode & S_IXOTH) mode_string[9] = 'w';

return mode_string;
}

int main (int argc, char* argv[]) {
struct stat buf;
if (stat (argv[1], &buf) < 0) {
perror("stat");
return -1;
}
printf("%s ", change_mode_to_string(buf.st_mode));
printf("%lu ", buf.st_nlink);
printf("%s ", find_user_name_by_uid (buf.st_uid));
printf("%s ", find_group_name_by_gid (buf.st_gid));
printf("%ld ", buf.st_size);
printf("%s ", ctime (&buf.st_atime) );


return 0;
}

运行结果如图:

读取目录文件

文件存储

• 文件数据:存放在data block中

• 文件属性:存放在inode中

• 文件名:若干目录项组成的表格,存放在目录块中(data block)

打开目录文件

目录项

编程实现ls命令

实现ls 命令,显示当前目录下的所有文件(普通文件、目录)

• 第1步:打开一个目录文件

• 第2步:读取目录文件,返回一个目录项

• 第3步:显示这个目录项

• 第4步:循环第2、3步,直到读取文件末尾

• 第5步:关闭这个目录文件

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

int main (void) {
DIR * dir_ptr;
struct dirent * entry_ptr;
dir_ptr = opendir(".");
int count = 0;

while ((entry_ptr = readdir (dir_ptr)) != NULL) {
if (++ count % 10 == 0) printf("\n");
printf("%6s", entry_ptr->d_name);
}
printf("\n");

return 0;
}

实现ls命令:支持多个目录

实现思路

• 当ls命令无参数时,默认显示当前目录的内容

• 当有参数时,显示指定目录的内容

• 支持多个目录参数

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
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>


int do_ls (char pathname[]);

int main (int argc, char* argv[]) {
if (argc == 1) {
do_ls(".");
} else {
while ( --argc) {
printf("%s", *++argv);
do_ls(*argv);
}
}

return 0;
}

int do_ls (char pathname[]) {
DIR * dir_ptr;
struct dirent * entry_ptr;
dir_ptr = opendir(pathname);
int count = 0;

while ((entry_ptr = readdir (dir_ptr)) != NULL) {
if (++ count % 10 == 0) printf("\n");
printf("%s ", entry_ptr->d_name);
}
printf("\n");

closedir (dir_ptr);
return 0;
}

实现ls命令:支持-c参数

实现思路

• 第1步:在main函数中解析命令行参数,看一下是否有-c这个参数

• 第2步:在do_ls函数中,增加对-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
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>


int do_ls (char pathname[]);

int c_flag;

int main (int argc, char* argv[]) {
for (int i = 1; i < argc; i ++) {
if (strcmp (argv[i], "-c") == 0) c_flag = 1;
}
if (argc == 1 || (argc == 2 && c_flag == 1)) {
do_ls(".");
} else {
while ( --argc) {
if (strcmp(*++argv, "-c") == 0) {
continue;
}
printf("%s:\n", *argv);
do_ls(*argv);
}
}

return 0;
}

int do_ls (char pathname[]) {
DIR * dir_ptr;
struct dirent * entry_ptr;
int count = 0;
dir_ptr = opendir(pathname);
{
if (dir_ptr == NULL) {
perror("opendir");
return -1;
}
}

while ((entry_ptr = readdir (dir_ptr)) != NULL) {
if (strcmp (entry_ptr->d_name, ".") == 0) continue;
if (strcmp (entry_ptr->d_name, "..") == 0) continue;
if (c_flag == 1 && strcmp (entry_ptr->d_name + strlen (entry_ptr->d_name) - 2, ".c"))
continue;
if (++ count % 10 == 0) printf("\n");
printf("%s ", entry_ptr->d_name);
}
printf("\n");

closedir (dir_ptr);
return 0;
}

实现ls命令:支持-l参数

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <stdio.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>
#include <libgen.h>

int do_ls (char pathname[]);
int print_ll (char filename[]);

int c_flag;
int l_flag;
int dir_count;

void check_param (int argc, char* argv[]) {
for (int i = 1; i < argc; i++)
{
if (strcmp (argv[i], "-c") == 0)
c_flag = 1;
else if (strcmp (argv[i], "-l") == 0)
l_flag = 1;
else
dir_count ++;
}
}

int main (int argc, char *argv[])
{

check_param (argc, argv);
if (argc == 1 || dir_count == 0)
do_ls (".");
else
while (--argc)
{
if (strcmp (*++argv, "-c") == 0)
continue;
if (strcmp (*argv, "-l") == 0)
continue;
printf ("%s:\n", *argv);
do_ls (*argv);
}
return 0;
}

int do_ls (char pathname[])
{
DIR *dir_ptr;
struct dirent *entry_ptr;
int count = 0;

dir_ptr = opendir (pathname);
{
if (dir_ptr == NULL)
{
perror ("opendir");
return -1;
}
}
while ((entry_ptr = readdir (dir_ptr)) != NULL)
{
if ((strcmp (entry_ptr->d_name, ".") && strcmp (entry_ptr->d_name, "..")) == 0)
continue;
if (c_flag)
if (strcmp (entry_ptr->d_name + strlen (entry_ptr->d_name) -2, ".c"))
continue;
if (++count % 10 == 0 )
printf ("\n");

if (l_flag) {
char tmp[128];
strcpy (tmp, pathname);
strcat (tmp, entry_ptr->d_name);
print_ll (tmp);
}
else
printf ("%s ", entry_ptr->d_name);
}
printf ("\n");

closedir (dir_ptr);
return 0;
}

//
char *change_mode_to_string(mode_t mode)
{
static char mode_string[10];
strcpy (mode_string, "----------");
if (S_ISDIR(mode)) mode_string[0] = 'd';
if (S_ISCHR(mode)) mode_string[0] = 'c';
if (S_ISBLK(mode)) mode_string[0] = 'b';
if (S_ISLNK(mode)) mode_string[0] = 'l';
if (S_ISFIFO(mode)) mode_string[0] = 'f';
if (S_ISSOCK(mode)) mode_string[0] = 's';

if (mode & S_IRUSR) mode_string[1] = 'r';
if (mode & S_IWUSR) mode_string[2] = 'w';
if (mode & S_IXUSR) mode_string[3] = 'x';
if (mode & S_IRGRP) mode_string[4] = 'r';
if (mode & S_IWGRP) mode_string[5] = 'w';
if (mode & S_IXGRP) mode_string[6] = 'x';
if (mode & S_IROTH) mode_string[7] = 'r';
if (mode & S_IWOTH) mode_string[8] = 'w';
if (mode & S_IXOTH) mode_string[9] = 'x';

return mode_string;
}

char * find_user_name_by_uid (uid_t uid)
{
struct passwd *passwd_ptr;

passwd_ptr = getpwuid (uid);
if (passwd_ptr == NULL)
{
static char uid_string[10];
sprintf (uid_string, "%d", uid);
return uid_string;
}
return passwd_ptr->pw_name;
}

char *find_group_name_by_gid (gid_t gid)
{
struct group *group_ptr;

group_ptr = getgrgid (gid);
if (group_ptr == NULL)
{
static char gid_string[10];
sprintf (gid_string, "%d", gid);
return gid_string;
}
return group_ptr->gr_name;
}

int print_ll (char filename[])
{
struct stat buf;
if (stat (filename, &buf) < 0)
{
perror ("stat");
return -1;
}
printf ("%-6s ", basename(filename));
printf ("%s ", change_mode_to_string (buf.st_mode));
printf ("%lu ", buf.st_nlink);
printf ("%s ", find_user_name_by_uid (buf.st_uid));
printf ("%s ", find_group_name_by_gid (buf.st_gid));
printf ("%ld ", buf.st_size);
printf ("%s ", ctime (&buf.st_atime));
return 0;
}

目录的其它相关操作

目录操作

路径操作

链接

相对路径转绝对路径

实现方法

实现shell命令:pwd

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

int main (void) {
char buffer[128];
getcwd(buffer, 128);
printf("%s\n", buffer);

return 0;
}

将一个文件名转换为绝对路径+ 文件名的形式

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

int main (int argc, char* argv[]) {
char buffer[128];
getcwd (buffer, 128);
printf ("%s\n", buffer);

strcat (buffer, "/");
strcat (buffer, argv[1]);
printf("%s\n", buffer);

return 0;
}

编程实战:实现 wc 命令

编程实战

• 编写一个工具wc,统计Linux最新内核源码:

• 一共有多少个C文件?

• 一共有多少个H文件?

• 一共有多少个汇编文件?

• 一共有多少个words、行?

• 整个项目一共有多少行代码?

功能分析

实现思路

• 先统计单个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
int do_wc (char filename[]) {
FILE * fp;
char ch;
unsigned int lines = 0;
unsigned int words = 0;
int word_flag = 0;

fp = fopen (filename, "r");
if (fp == NULL) {
perror ("fopen");
return -1;
}

while ((ch = fgetc(fp)) != EOF) {
if (ch == '\n') lines ++;
if (ch == '\n' || ch == ' ' || ch == '\t') {
word_flag = 1;
continue;
} else {
if (word_flag == 1){
words ++;
word_flag = 0;
}
}
}
printf("lines: %d\n", lines);
printf("words: %d\n", words);

return 0;
}

• 然后遍历一个目录下的所有C文件

• 如果该目录下面还有子目录、递归遍历

• 统计、累加

• 打印输出

功能实现

文件遍历

• 各个目录、子目录递归遍历

• 当一个目录下面有文件、子目录时的处理

• 搜索指定格式的文件:C文件、H文件、汇编文件

改进后的完整代码

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
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int do_wc (char filename[]);
int foreach_dir (char dirname[] );


int main (int argc, char* argv[]) {
foreach_dir (argv[1]);

return 0;
}

int foreach_dir (char dirname[]) {
//do_wc (argv[1]);
DIR * dir_ptr;
struct dirent* entry_ptr;

dir_ptr = opendir (dirname);
if (dir_ptr == NULL) {
perror ("opendir");
return -1;
}
char full_path_name[128];
struct stat buf;
while ((entry_ptr = readdir (dir_ptr)) != NULL) {
if ((strcmp (entry_ptr->d_name, ".") && strcmp (entry_ptr->d_name, "..")) == 0)
continue;
strcpy (full_path_name, dirname);
if (full_path_name[strlen (full_path_name) - 1] != '/')
strcat (full_path_name, "/");
strcat (full_path_name, entry_ptr->d_name);

if (stat (full_path_name, &buf) < 0) {
perror("stat");
return -1;
}
if (S_ISDIR(buf.st_mode)) {
foreach_dir (full_path_name);
} else {
if (strcmp (entry_ptr->d_name + strlen(entry_ptr->d_name) - 2, ".c") && \
strcmp (entry_ptr->d_name + strlen(entry_ptr->d_name) - 2, ".h") && \
strcmp (entry_ptr->d_name + strlen(entry_ptr->d_name) - 2, ".S"))
continue;
else
do_wc (full_path_name);
}

}

closedir (dir_ptr);
return 0;
}

int do_wc (char filename[]) {
FILE * fp;
char ch;
static unsigned int lines = 0;
static unsigned int words = 0;
static unsigned int files = 0;
int word_flag = 0;

fp = fopen (filename, "r");
if (fp == NULL) {
perror ("fopen");
return -1;
}

while ((ch = fgetc(fp)) != EOF) {
if (ch == '\n') lines ++;
if (ch == '\n' || ch == ' ' || ch == '\t') {
word_flag = 1;
continue;
} else {
if (word_flag == 1){
words ++;
word_flag = 0;
}
}
}
printf ("%s\n", filename);
printf ("files: %d\n", ++files);
printf ("lines: %d\n", lines);
printf ("words: %d\n", words);

fclose(fp);

return 0;
}

文件IO缓存与内存映射

缓存的基本概念

计算机中的缓存

无处不在的缓存

  • CPU级别的缓存:cache、TLB、让 CPU 高效运行
  • 操作系统的缓存:页缓存、slab、让系统更高效运行
  • 应用层的缓存:内存管理、IO应用缓存,让应用更高效运行

I/O缓存

提高文件I/O的性能

  • 操作系统级的缓存
  • 应用层的缓存
  • 内存映射
  • API系统调用、参数

页高速缓存-页缓存读写流程

内核中的缓冲区

页高速缓存

页缓存

• 通过Linux内核缓冲区实现了一种磁盘缓存机制

• 基本原理:局部原理、时间局部、空间局部

• 缓存基于页的对象:普通文件、块设备、内存映射

• 优点:一定程度上分离了应用程序空间和物理设备、减少IO读盘次数

页缓存和读写

读流程

  • 先到缓存中看数据是否存在,存在直接读取返回到用户空间
  • 若不存在,将磁盘数据缓存到page cache中
  • 然后从page cache中读取数据到用户空间

写流程

  • 将用户空间数据写到page cache中

  • 当page cache数据达到阈值或时间超时,将数据回写到磁盘

同步方式

实验-页缓存读写实验

将10MB的空数据写到test.dat文件中

页高速缓存-内存管理

物理内存管理

ZONE_DMA : 存放连续的物理地址空间,用作设备 DMA

ZONE_NORMAL : Linux内核镜像、应用程序大部分都加载到这里

ZONE_HIGNMEM: 用作设备映射、临时映射等

伙伴算法

为了防止内存碎片化,采用伙伴系统来申请释放内存

内存申请

Linux虚拟地址空间

内核空间到物理地址的映射

用户空间到物理地址的映射

页高速缓存-页缓存的实现

页缓存对象:属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root page_tree; /* radix tree of all pages */
spinlock_t tree_lock; /* and lock protecting it */
atomic_t i_mmap_writable;/* count VM_SHARED mappings */
struct rb_root i_mmap; /* tree of private and shared mappings */
struct rw_semaphore i_mmap_rwsem; /* protect tree, count, list */
/* Protected by tree_lock together with the radix tree */
unsigned long nrpages; /* number of total pages */
unsigned long nrshadows; /* number of shadow entries */
pgoff_t writeback_index;/* writeback starts here */
const struct address_space_operations *a_ops; /* methods */
unsigned long flags; /* error bits/gfp mask */
spinlock_t private_lock; /* for use by the address_space */
struct list_head private_list; /* ditto */
void *private_data; /* ditto */
} ; //通过该对象,可以将文件系统、内存管理系统进行关联

将文件系统和内存管理系统进行关联

  • struct inode * host : 指向所属的 inode对象或块设备对象,用于获取文件的元数据信息。

  • struct rb_root i_mmap : 红黑树结构,用于管理私有映射和共享映射,每个映射对应一个vm_area_struct结构。

  • unsigned long nrpages :记录该address_space对象管理的页框数量。

  • const struct address_space_operations *a_ops;:指向address_space_operations结构体,提供了对文件数据的读写、缓存、回写等操作。

页缓存对象:方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct address_space_operations {
int (*writepage)(struct page *page, struct writeback_control *wbc);
int (*readpage)(struct file *, struct page *);
int (*writepages)(struct address_space *, struct writeback_control *);
int (*set_page_dirty)(struct page *page);
int (*readpages)(struct file *filp, struct address_space *mapping,
struct list_head *pages, unsigned nr_pages);

sector_t (*bmap)(struct address_space *, sector_t);
void (*invalidatepage) (struct page *, unsigned int, unsigned int);
int (*releasepage) (struct page *, gfp_t);
void (*freepage)(struct page *);
ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter, loff_t offset);
}; // 文件位置偏移 -> 页偏移量 -> 文件系统块号:block -> 磁盘扇区号

封装了读页缓存、写页缓存等方法

来自GPT-4的讲解:

struct address_space_operations 是一个结构体,定义了一组用于操作地址空间的函数指针。地址空间(address_space)是 Linux 内核中的一个关键概念,用于表示文件或设备的抽象内存映射。这个结构体在 Linux 的虚拟文件系统(VFS)中扮演着重要角色,它允许文件系统实现自己的操作函数,以便对文件或设备的内存映射进行管理。

下面是 struct address_space_operations 成员的简要说明:

  1. int (*writepage)(struct page *page, struct writeback_control *wbc);
    写入一个内存中的脏页(包含未写入磁盘的修改数据)到底层存储设备。
  2. int (*readpage)(struct file *, struct page *);
    从底层存储设备读取一个页面到内存。
  3. int (*writepages)(struct address_space *, struct writeback_control *);
    将一个地址空间中的所有脏页(需要写回磁盘的页面)写入底层存储设备。
  4. int (*set_page_dirty)(struct page *page);
    将页面标记为脏(表示已被修改,需要写回磁盘)。
  5. int (*readpages)(struct file *filp, struct address_space *mapping, struct list_head *pages, unsigned nr_pages);
    从底层存储设备读取多个页面到内存。
  6. sector_t (*bmap)(struct address_space *, sector_t);
    将文件位置偏移量映射到文件系统块号,再映射到磁盘扇区号。这个过程将逻辑地址转换为物理地址。
  7. void (*invalidatepage) (struct page *, unsigned int, unsigned int);
    使包含指定范围的页面无效,以便从缓存中删除它。
  8. int (*releasepage) (struct page *, gfp_t);
    释放一个页面,以便将其归还给内存分配器。
  9. void (*freepage)(struct page *);
    在页面被释放之前,对其进行清理操作。
  10. ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter, loff_t offset);
    通过直接 I/O 访问文件,从而绕过页面缓存。

struct address_space_operations 提供了多种操作地址空间的方法,使得不同的文件系统可以实现自己的操作函数,以满足特定需求。

页缓存对象:物理页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct page {
unsigned long flags;
union {
struct address_space *mapping;
void *s_mem; /* slab first object */
};
union {
struct list_head lru;
struct { /* slub per cpu partial pages */
struct page *next; /* Next partial slab */
short int pages;
short int pobjects;
};
union {
unsigned long private;
spinlock_t ptl;
struct kmem_cache *slab_cache;
};
} //一个页可以是文件页缓存、可以是交换缓存、也可以是普通内存

一个页可以是文件页缓存、可以是交换缓存、也可以是普通内存

如果成员变量 mapping 指向 address_space ,则说明这个物理页是用来缓存文件的

页缓存数据结构图

读文件-基本流程

  • 数据结构关联:inode->i_mapping 指向 address_space 对象
  • 数据结构关联:address_space->host 指向inode
  • 数据结构关联:page->mapping指向页缓存owner的address_space
  • 系统调用传参:文件描述符 + 文件位置偏移
  • 系统找到文件的address_space,根据偏移量到页缓存中查找page
  • 若查找到,返回数据到用户空间
  • 若没查到,内核新建一个page并加入页缓存,数据从磁盘载入该页
  • 调用readpage方法将数据返回给用户空间

读文件实例

写文件-基本流程

• 数据结构关联:inode->i_mapping指向address_space对象

• 数据结构关联:address_space->host指向inode

• 数据结构关联:page->mapping指向页缓存owner的address_space

• 系统调用传参:文件描述符 + 文件位置偏移

• 系统找到文件的address_space,根据偏移量到页缓存中查找page

• 若查找到,将数据写入到该页缓存中,该页成为 脏页

• 若没查到,从缓存中分配空闲页,数据从用户空间写入该页

• 当数据满足一定空间或时间阈值,将脏页中的数据回写到磁盘

• 守护进程:pdflush

块设备驱动架构

块缓存

Linux内核中的缓存

  • 早期使用块缓存,新的内核逐渐使用页缓存
  • 块缓存:比页缓存小、长度可变,依赖于设备(或文件系统)
  • 块缓存的实现:基于页缓存
    • 缓冲头:buffer_head,缓冲区的元数据信息:块号、块长度
    • 每个buffer_head指向一个缓冲区,一个页可以细分为若干个缓冲区
    • 内核物理页与磁盘物理块之间的桥梁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct buffer_head {
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head *b_this_page; /* circular list of page's buffers */
struct page *b_page; /* the page this bh is mapped to */
sector_t b_blocknr; /* start block number */
size_t b_size; /* size of mapping */
char *b_data; /* pointer to data within the page */
struct block_device *b_bdev;
bh_end_io_t *b_end_io; /* I/O completion */
void *b_private; /* reserved for b_end_io */
struct list_head b_assoc_buffers; /* associated with another mapping */
struct address_space *b_assoc_map; /* mapping this buffer is
associated with */
atomic_t b_count; /* users using this buffer_head */
};

b_this_page:由于一个页可能对应多个缓存页,因此这个字段用于将同一个页的所有缓存页串联起来,形成一个循环链表,即指向块缓存的指针

b_blocknr: 表示缓存页对应的磁盘块号

b_data:表示指向缓存页数据的指针

b_bdev:表示缓存页对应的块设备(文件)

bio结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned int bi_flags; /* status, command, etc */
unsigned long bi_rw;
struct bvec_iter bi_iter;
unsigned int bi_phys_segments;
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
atomic_t __bi_remaining;
bio_end_io_t *bi_end_io;
void *bi_private;
unsigned short bi_vcnt; /* how many bio_vec's */
unsigned short bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t __bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;
struct bio_vec bi_inline_vecs[0];
}

• main unit of I/O for the block layer and lower layers

• struct gendisk -> block_device -> /dev/sdax -> inode

底层驱动可以接受的类型读写单元是 bio,通常需要将 page cache转化成 bio

硬盘中使用 gendisk 表示一个硬盘,每个分区用block_device 来表示,对应在设备结点上,比如有 /dev/sda 表示代表 gendisk,sda1和sda2代表一个分区,只能对安装文件系统进行格式化的分区进行读写;每一个设备节点分区都有自己的 inode , 在 bio中有对应的 bi_bdev ,通过这个结构体将对应的设备进行封装

目前内核中块I/O操作的基本容器由 bio 结构体表示。该结构体代表了正在现场(活动)以片段链表形式组织的块I/O操作。一个片段是一小块连续的内存缓冲区。这样的话,就不需要保证单个缓冲区一定要连续。所以通过片段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也能对内核保证I/O操作的执行,像这样的向量 I/O 就是所谓的聚散I/O。

块设备驱动架构

各种各样的块设备

用户空间的IO缓存

系统调用的开销

  • 切换CPU到内核模式
  • 数据拷贝
  • 切换CPU到用户模式

C标准库IO缓冲区

  • 在用户空间,为每个打开的文件

    • 分配一个I/O缓冲区
    • 分配一个文件描述符
    • I/O缓冲区信息和文件描述符一起封装在FILE结构体中
  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

  • size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct _IO_FILE {
int _flags;
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
struct _IO_FILE *_chain;
int _fileno;
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
};
typedef struct _IO_FILE FILE;

_fileno : 文件描述符

文件读写流程

三种模式

块缓冲(block_buffered):

  • 固定字节的缓冲区大小,比如跟文件相关的流都是块缓冲

  • 标准IO称块缓冲为完全缓冲(full buffering)

行缓冲(line_buffered):

  • 遇到换行符,缓冲区的数据会拷贝到内核缓冲区

无缓冲(unbuffered):

  • 数据直接拷贝到内核缓冲区

  • 如:标准错误stderr采用无缓冲模式

自定义缓冲区

C标准库函数

  • int setvbuf (FILE *stream, char *buf, int mode, size_t size);

  • stream:指向流的指针

  • buf:缓冲区地址

  • mode:缓冲区类型

    • __IOFBF:当缓冲区为空,从流中读入数据;缓冲区满时向流写入数据
    • __IOLBF:每次从流中读入一行数据或向流中写入一行数据
    • __IONBF:直接从流中读入数据或直接向流中写入数据,无缓冲区
  • size:缓冲区内字节的数量

Scatter-gather I/O

分散/聚集IO

  • 优点:更进一步减少了系统调用的次数

  • 单个向量I/O操作取代多个线性I/O操作,效率更高

系统调用函数原型

1
2
3
4
5
6
7
8
ssize_t readv (int fd, const struct iovec *iov, int iovcnt);
ssize_t writev (int fd, const struct iovec *iov, int iovcnt);

struct iovec
{
void *iov_base; /* Pointer to data. */
size_t iov_len; /* Length of data. */
};

实例

writev()

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
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main (int argc, char* argv[]) {
struct iovec iov[3];
ssize_t count;
int fd;
int i;

char* buf[] = {
"hello world!\n",
"hello Ubuntu\n",
"hello Linux\n"
};
fd = open (argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
perror ("open");
return -1;
}
for (int i = 0; i < 3; i ++) {
iov[i].iov_base = buf[i];
iov[i].iov_len = strlen (buf[i]) + 1;
printf ("iov[%d] size: %d\n", i, iov[i].iov_len);
}
count = writev (fd, iov, 3);
if (count == -1) {
perror ("writev");
return -1;
}
close (fd);
if (fd == -1) {
perror ("close");
return -1;
}

return 0;
}

readv()

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
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main (int argc, char* argv[]) {
char buf1[14], buf2[14], buf3[14];
struct iovec iov[3];
ssize_t count;
int fd;

fd = open (argv[1], O_RDONLY);
if (fd == -1) {
perror ("open");
return -1;
}

iov[0].iov_base = buf1;
iov[0].iov_len = sizeof (buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof (buf2);
iov[2].iov_base = buf3;
iov[2].iov_len = sizeof (buf3);

count = readv (fd, iov, 3);
if (count == -1) {
perror ("readv");
return -1;
}
/*
for (int i = 0; i < 3; i ++) {
printf ("%s", (char *) iov[i].iov_base);
}
*/
printf ("%s", (char*) &buf1);
printf ("%s", (char*) &buf2);
printf ("%s", (char*) &buf3);
close (fd);
if (fd == -1) {
perror ("close");
return -1;
}

return 0;
}

直接I/O

用户空间的IO缓存

优缺点:

• 优点:减少了系统调用的次数,减少了系统开销

• 缺点:增加了数据拷贝次数,增大了CPU和内存开销

读:数据从内核页缓存拷贝到标准IO缓存,再拷贝到用户的buffer

写:数据从用户的buffer拷贝到标准IO缓存,再拷贝到内核缓冲区

飞跃缓冲区

可以使用 setbuf(fp, NULL) 直接将数据拷贝到页缓存不经过IO缓冲区

使用 open (file, flags, O_DIRECT) 可以直接写入磁盘、

自缓存应用程序

数据库

  • 采用直接IO,绕过缓冲区,直接读写磁盘
  • 对用户空间对IO进行缓存和读写优化
  • 频繁读写、小批量处理

直接IO

遵守的原则

  • 用于传递数据的缓冲区,其内存边界必须对齐:块的整数倍
  • 数据传递的开始点,即文件和设备的偏移量,必须是块大小的整数倍
  • 待传递数据的长度必须是块大小的整数倍
  • 如果用户空间不优化的话,可能会降低系统性能

将文件映射到内存

用户的IO缓存

优缺点:

优点:减少了系统调用的次数,减少了系统开销

缺点:增加了数据拷贝次数,增大了CPU和内存开销

  • 读:数据从内核页缓存拷贝到标准IO缓存,再拷贝到用户的buffer

  • 写:数据从用户的buffer拷贝到标准IO缓存,再拷贝到内核缓冲区

将文件映射到内存

  • 内存地址与文件数据一一对应
  • 通过内存代替 read/write 等I/O系统调用接口来访问文件
  • 减少内存拷贝、减少系统调用次数,提高系统性能

系统调用接口

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

  • addr:进程中要映射的虚拟内存起始地址,一般为NULL

  • length:要映射的内存区域大小

  • prot:内存保护标志: PROT_EXEC、 PROT_READ、 PROT_WRITE

  • flags:映射对象类型: MAP_FIXED、 MAP_SHARED、MAP_PRIVATE

  • fd:要映射文件的文件描述符

  • offset:文件位置偏移

  • mmap以页为单位操作:参数addr和offset必须按页对齐

例子

mmap_write.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
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

int main (int argc, char* argv[]) {
int fd;
int i;
char* p_map;
fd = open (argv[1], O_CREAT | O_RDWR | O_TRUNC, 0666);
write (fd, "", 1);
p_map = (char *) mmap (NULL, 20, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p_map == MAP_FAILED) {
perror ("mmap");
return -1;
}
close (fd);
if (fd == -1) {
perror ("close");
return -1;
}
memcpy (p_map, "hello world!\n", 14);
sleep (5);
if (munmap (p_map, 20) == -1) {
perror ("munmap");
return -1;
}

return 0;
}

mmap_read.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
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

int main (int argc, char* argv[]) {
int fd;
int i;
char* p_map;
fd = open (argv[1], O_CREAT | O_RDWR, 0666);
p_map = (char *) mmap (NULL, 20, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p_map == MAP_FAILED) {
perror ("mmap");
return -1;
}
close (fd);
if (fd == -1) {
perror ("close");
return -1;
}
printf ("%s", p_map);
if (munmap (p_map, 20) == -1) {
perror ("munmap");
return -1;
}

return 0;
}

文件映射的内存实现

Linux进程虚拟地址空间

Linux进程虚拟地址描述

结构体:task_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct task_struct {
volatile long state;
void *stack;
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
cpumask_t cpus_allowed;
struct list_head tasks;
struct mm_struct *mm, *active_mm;
u32 vmacache_seqnum;
int exit_state;
int exit_code, exit_signal;
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
u32 vmacache_seqnum; /* per-thread vmacache */
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
unsigned long task_size; /* size of task vm space */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
atomic_long_t nr_ptes; /* PTE page table pages */
int map_count; /* number of VMAs */
spinlock_t page_table_lock; /* Protects page tables and some counters */
struct rw_semaphore mmap_sem;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
struct vm_area_struct *vm_next, *vm_prev; /* linked list of VM areas per task*/
struct rb_node vm_rb;
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
const struct vm_operations_struct *vm_ops;
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */

};

如果内存区域用来映射文件,则vm_file 指向打开的文件

Linux虚拟地址到文件的映射

内存物理地址与Linux进程虚拟地址建立关联

并不是直接对磁盘进行读写,还是要缓存到页缓存中,用户是直接对页缓存进行读写,通过内存映射,减少了页缓存往用户空间拷贝,但是从磁盘空间到页缓存这个过程还是有的。

mmap 即减少了系统调用的次数也减少了数据拷贝的次数

将设备映射到内存

LCD显示原理

Frambuffer设备

将设备映射到内存

共享文件映射

进程与终端

进程:程序运行的“牌照”

进程与程序的区别

  • 程序:二进制文件,存储在磁盘上

  • 进程:process,一个程序运行实例

    • 将程序从磁盘加载到内存并分配对应的资源、调度运行
  • 进程实例

    • 汇编指令代码、数据、资源、状态

    • 一个虚拟计算机(进程上下文环境、CPU状态寄存器)

    • 进程资源:虚拟内存、打开的文件描述符表、信号、工作目录…

创建一个进程:fork

创建一个子进程

系统调用:fork()

  • 函数原型:pid_t fork(void);

  • 函数作用:创建一个新进程

  • 返回值:

    • -1 :创建子进程失败

    • 0 :在子进程中返回0

    • > 0 :在父进程中返回的是子进程的PID

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 <unistd.h>
#include <stdlib.h>

int main (void) {
int ret_from_fork;

ret_from_fork = fork ();
if (ret_from_fork == 0) {
printf ("child process...\n");
printf ("ret_from_fork = %d\n", ret_from_fork);
exit (0);
} else if (ret_from_fork > 0) {
printf ("father process...\n");
printf ("ret_from_fork = %d\n", ret_from_fork);
exit (0);
} else if (ret_from_fork == -1) {
perror ("fork");
exit (-1);
}
return 0;
}

子进程的运行:“借壳上市”

执行一个二进制程序文件

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

int main (int argc, char* argv[]) {
char* arglist[argc];
int i;
for (i = 0; i < argc - 1; i ++) {
arglist[i] = argv[i + 1];
}
arglist[i] = NULL;

printf ("Hello!\n");
execvp (arglist[0], arglist);
printf ("World!\n");

return 0;
}

写时复制(COW)与vfork

一个新进程的诞生:虚拟空间

一个新进程的诞生:物理空间

如果这样的话,多了一次复制,因此不可取

写时复制(copy-on-write)

对fork-exec流程的改进

  • 对于代码段、数据段等,父子进程可以共享,节省拷贝开销

  • 父子进程的页表项均指向同一块物理内存页帧

  • 当子进程进程空间的内容要修改时,才会真正将段复制到子进程

  • 写时复制:

    • 仅仅为子进程复制父进程的虚拟页表项
    • 对将要修改的页面修改页表项

系统调用:vfork

对fork的改进

  • 对fork的改进更为彻底、简单粗暴

  • vfork是为子进程立即执行exec的程序而专门设计的

    • 无需为子进程复制虚拟内存页或页表,子进程直接共享父进程的资源,直到其成功执行exec或是调用exit退出
    • 在子进程调用exec之前,将暂停执行父进程

进程的退出:exit

终止当前进程

exit函数

  • POSIX标准和ANSI C定义的标准函数

    • #include <stdlib.h>
    • 其实是对系统调用_exit的封装
  • 函数原型:void exit (int status);

  • 函数功能:终止当前进程

  • 参数说明:用于标识进程的退出状态,shell或父进程可以获取该值

    • 0:表示进程正常退出
    • -1/1:表示进程退出异常
    • 2~n:用户可自定义

exit函数的背后

执行流程

  • 调用退出处理程序(通过atexit、on_exit注册的程序)
  • 刷新 stdio 流缓冲区
  • 使用由status提供的值执行 _exit系统调用函数
    • 关闭进程打开的文件描述符、释放进程持有的文件锁
    • 关闭进程打开的信号量、消息队列
    • 取消该进程通过 mmap 创建的内存映射

atexit/on_exit

退出处理程序

• 在exit退出后可以自动执行用户注册的退出处理程序

• 执行顺序与注册顺序相反

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

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

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
#include <stdio.h>
#include <stdlib.h>

static void exit_process1 (void) {
printf ("exit process func1\n");
}

static void exit_process2 (void) {
printf ("exit process func2\n");
}

static void on_exit_process (int exit_status, void* arg) {
printf ("on_exit_process called: status = %d, args=%d\n", exit_status, (int) arg);
}

int main () {
if (atexit (exit_process1) != 0)
perror ("atexit 1");
if (atexit (exit_process2) != 0)
perror ("atexit 2");
if (on_exit (on_exit_process, (void *) 20) != 0)
perror ("on_exit");

exit (5);

return 0;
}

return与exit的区别

  • exit用来终止当前进程,将控制权交给操作系统

  • return用来退出当前函数,销毁栈帧,返回到上级函数执行

  • 终止进程:

    • 正常退出:exit、_exit、从main函数return
    • 异常退出:调用abort、信号ctrl + C

exit_group函数

• 函数原型:void exit_group (int status);

• exit:退出当前进程process

• exit_group:退出一个进程中所有threads

• Linux系统特有的系统调用,不属于POSIX标准

Tips

fork之后、exec之前,使用exit是不安全的,因为很多资源还是共享的(如文件描述符、缓冲区)

进程的退出:exit与_exit

两者的区别

  • exit是库函数是对_exit系统调用的封装

  • 在调用_exit 之前,它会执行各种动作

    • 调用退出处理程序(通过atexit和on_exit注册的回调函数)

    • 刷新stdio流缓冲区

    • 使用由status提供的值执行_exit系统调用

_exit的执行流程

• 关闭进程打开的文件描述符、释放该进程持有的文件锁

• 关闭该进程打开的信号量、消息队列

• 取消该进程通过mmap()创建的内存映射

• 将该进程的所有子进程交给init托管

• 给父进程发送一个SIGCHLD信号

• ……

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

int main (void ) {
printf ("Hello World!\n");
write (1, "hahaha\n", 7);

if (fork () == -1) {
perror ("fork");
exit (1);
}

return 0;
}

观察这样一段程序,打印到终端和log文件打印顺序相反而且多了一行

printf 在终端为行缓存,遇到换行符就将数据拷贝到内核缓冲区,而 write 系统调用没有采用用户IO缓存,直接写到内核缓冲区,因此在终端打印正常。

当重定向到文件时,文件采用完全缓存,即块缓存(和文件相关的都是块缓存),此时 printf 输出到用户块缓存,还没有满,没有写到内核页缓冲区,而write是直接写到内核页缓存,所以文件里先打印hahaha

当运行到 fork 时,子进程完全复制父进程的数据代码、各种状态、父进程的用户IO缓存也会保存,接下来退出的时候,调用 exit 都会刷新 stdout 流缓冲区,因此在 log 文件有两个 Hello World!

如果我们不想将用户IO缓存数据复制给子进程,可以提前进行 fflush() 刷新到文件里面 ,也可以在子进程使用_exit(),即 退出时不刷新 stdout

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

int main (void ) {
printf ("Hello World!\n");
// fflush(stdout);
write (1, "hahaha\n", 7);

int ret_from_fork;
ret_from_fork = fork();

if (ret_from_fork == -1) {
perror ("fork");
exit (1);
} else if (ret_from_fork == 0) {
_exit (0);
}

return 0;
}

关于_exit和exit总结

  • 在一个进程中,直接调用_exit终止进程,缓冲区的数据可能会丢失

  • 在创建子进程的应用中,只应有一个进程(一般为父进程)调用exit终止,而其他进程应调用_exit()终止。从而确保只有一个进程调用退出处理程序并刷新stdio缓冲区

  • 如果一个进程使用atexit/on_exit注册了退出管理程序,则应使用exit终止程序的运行,否则注册的回调函数无法执行

终止进程的各种方法

• 从main函数return

• 调用库函数:exit

• 调用系统调用:_exit/_Exit

• 调用abort:_exit的内部实现(POSIX)

• 信号:Ctrl+C

• …

进程的退出:vfork与exit

vfork函数

系统调用vfork

  • 子进程共享父进程的代码、数据、堆栈资源
  • 使用 vfork 后,直接运行 exec,节省了资源拷贝的时间
  • 使用 vfork ,创建子进程后直接运行子进程、父进程被阻塞

使用vfork创建子进程的退出

  • vfork创建的子进程共享父进程的代码段、数据段、堆栈,子进程退出时使用 _exit/exit,使用 return 会破坏父进程的堆栈环境、产生段错误

  • 父进程退出一般使用 exit ,而子进程退出使用 _exit

    • 子进程 exit ,输出不确定,依赖于IO库的实现
    • 子进程: __exit + fflush = exit ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main (void) {
int ret_from_fork;
ret_from_fork = vfork ();
if (ret_from_fork == -1) {
perror ("vfork");
exit (-1);
} else if (ret_from_fork == 0) {
printf ("child process\n");
return 0; // Segmentation fault (core dumped)
// exit (0); // ok
}
printf ("father process\n");

return 0;
}

等待子进程终止:wait

wait()函数

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

  • 函数功能:等待子进程的终止及信息

  • 参数说明:子进程调用exit/_exit时的status

  • 返回值

    • wait调用成功,会返回已终止子进程的pid
    • wait调用失败,返回-1,设置errno值
    • 若子进程没有终止,wait调用会阻塞父进程,直到子进程终止,子进程终止后,该调用立即返回
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 <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int child_process (void) {
printf ("child process (pid %d) start\n", getpid());
;
;
printf ("child process exit");
exit (100);
}

int main (void) {
int ret_from_fork;
ret_from_fork = fork ();

if (ret_from_fork > 0) {
int wait_return_pid;
printf ("father process(pid:%d) start\n", getpid());
wait_return_pid = wait (NULL);
if (wait_return_pid == -1) {
perror ("wait");
exit (-1);
}
printf ("wait child process(pid:%d) exit success!\n", \
wait_return_pid);
exit (0);
} else if (ret_from_fork == 0) {
child_process ();
} else {
perror ("fork");
exit (-1);
}

return 0;
}

子进程的返回状态

通过宏来解析返回状态

• WEXITSTATUS(status):返回子进程的退出状态

• WTERMSIG(status):子进程因未捕捉的信号而终止,此宏返回true

• WSTOPSIG(status):子进程因信号暂停,此宏返回true

• WIFEXITED(status):若子进程正常结束,返回true

• WIFSIGNALED(status):若通过信号杀掉子进程,此宏返回true

• WIFSTOPPED(status):若子进程因信号而停止,此宏返回true

• WIFCONTINUED(status):若子进程收到SIGCONT恢复运行,返回true

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
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int child_process (void) {
printf ("child process (pid: %d) start\n", getpid());
;
;
printf ("child process exit\n");
exit (100);
}

int main (void) {
int ret_from_fork;
int i;
ret_from_fork = fork ();
if (ret_from_fork > 0) {
int wait_return_pid;
int status = 0;
printf ("father process(pid: %d) start\n", getpid() );
wait_return_pid = wait (&status);
if (wait_return_pid == -1)
perror ("wait");
if (WIFEXITED (status) ) {
printf ("wait child process(pid: %d) return sucess, exit value: %d\n", \
wait_return_pid, WEXITSTATUS (status));
}
printf ("father process (pid: %d) exit\n", getpid ());
exit (0);
} else if (ret_from_fork == 0) {
child_process ();
} else {
perror ("fork");
return -1;
}

return 0;
}

小结

等待特定子进程运行终止

进程调度

操作系统的核心:任务管理

调度器

任务调度

  • 将有限的CPU资源分配给多个进程

  • 目的:最大化处理器效率,让多个进程同时运行、互不影响

  • 实现:

    • 协同式:一个进程运行完自己的时间片,主动退出,CPU无权过问
    • 抢占式:时间片到了或有更高优先级、调度器抢占CPU进行任务切换

Linux进程管理

进程分类

处理器消耗型

  • 渴望获取更多的CPU时间,并消耗掉调度器分配的全部时间片

  • 常见例子:无限死循环、科学计算、影视特效渲染

I/O消耗型

  • 由于等待某种资源通常处于阻塞状态,不需要较长的时间片

  • 常见例子:等待用户输入、GUI程序、文件读写I/O程序

Linux调度策略

对不同进程采取不同调度策略、实现多个调度器

  • 完全公平调度CFS

  • 实时进程调度RT

  • 最终期限调度DL

  • IDLE类调度器、STOP类调度器

不同进程由不同的调度器管理,彼此之间互不干扰

  • 处理器消耗型进程:减少优先级、分配尽可能长的时间片

  • I/O消耗进程:增加优先级、增加实时性、增强用户体验

  • 两者混合型

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
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int main () {
int ret_from_fork;
int child_count = 0;
int father_count = 0;
int i;

ret_from_fork = fork ();

if (ret_from_fork > 0) {
while (1) {
for (i = 0; i < 200000000; i ++) ;
printf ("father process! %d\n", father_count ++);
// sleep (1);
}
exit (0);
} else if (ret_from_fork == 0) {
while (1) {
for (int i = 0; i < 100000000; i ++) ;
printf ("child process! %d\n", child_count++);
// sleep (1);
}
} else {
perror ("fork");
return -1;

}

return 0;
}

进程的优先级

进程的nice值和优先级

  • $ nice -n 5 top

  • NI [-20,19] :进程的NICE值,也叫静态优先级,nice值越小,抢占CPU能力越强,nice会影响进程的优先级

  • PRI[0,139]:进程的优先级,也叫动态优先级,值越小,优先级越高

  • 进程默认优先级:#defineDEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2)

  • 实时进程与非实时进程

    • 实时进程:优先级[0,99],采用实时进程的调度算法
    • 非实时进程:优先级[100,139],采用O1/CFS等调度算法

并发与并行区别

  • 并发:concurrency,CPU通过时间片轮转同时做多件事情

  • 并行:parallellism,很多事情在多个CPU上同时进行

  • 并发可以看做并行的一个“子集”

  • 一个应用程序

    • 可以是并发的,但不是并行的

    • 可以是并行的,但不是并发的

    • 既是并发的,又是并行的

    • 既不是并发的,又不是并行的

Linux进程状态

进程状态

  • TASK_RUNNING: 就绪/可运行状态
  • TASK_INTERRUPTIBLE:进程被挂起(睡眠/被阻塞),直到等待条件为真被唤醒,处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。
  • TASK_UNINTERRUPTIBLE: 深度睡眠,睡眠期间不响应信号
  • TASK_STOPPED: 进程的执行被暂停;(通常这种状态在接收到 SIGSTOP等信号时候发生,在调试期间接受到的任何信号,都会使进程进入这种状态)
  • TASK_TRACED: 被其他进程跟踪,常用于调试
  • EXIT_ZOMBIE: 僵死状态,进程的执行被终止
  • EXIT_DEAD: 僵死撤销状态,防止wait类系统调用的竞争状态发生

进程的UID和GID

进程的PID

用户和组

进程UID和GID

通过proc查看进程资源

进程资源:task_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct task_struct {
void *stack;
atomic_t usage;
const struct sched_ class *sched_class;
struct list_head tasks;
struct mm_struct *mm, *active_mm;
struct vm_area_struct *vmacache[VMACACHE_SIZE];
int exit_state;
pid_t pid;
pid_t tgid;
struct task_struct *parent;
struct list_head children;
struct fs_struct *fs;
struct files_struct *files;
struct bio_list *bio_list;
struct reclaim_state *reclaim_state;
...
int pagefault_disabled;
struct thread_struct thread;
}

通过ps命令查看进程资源

通过proc查看进程资源

与进程通信:信号

信号

  • 信号是一种异步通信的IPC

  • 可以给一个指定进程发送一个信号

  • 进程根据接收信号类型作相应的处理

  • 系统调用接口:signal、kill

一个进程对信号的处理

三种处理方式

  • 如果注册信号处理回调函数的话,会调用注册的信号处理回调函数

  • 如果没有注册的话,按照该信号在系统中的默认处理方式

    • 忽略
    • 终止进程

信号与其对应的系统事件

终端驱动支持的信号

• CTRL + Z:SIGTSTP

• CTRL + \:SIGQUIT

• CTRL + C:SIGINT

终端与控制台

终端的演变

控制台的概念

虚拟终端

伪终端

TIPS

• 终端与控制台的界限正越来越模糊…

• $ tty:显示当前用户所在终端的文件名

进程组与会话

进程组

什么是进程组

  • 进程组:一组协同工作或关联进程的组合,每个进程组有ID(PGID)

  • 每个进程属于一个进程组,每个进程组有一个进程组长,该进程组长ID(PID)与进程组ID(PGID)相同

  • 一个信号可以发送给进程组的所有进程、让所有进程终止、暂停或继续运行

会话

什么是会话?

  • 会话是一个或多个进程组的集合
    • 当用户登录系统时,登录进程会为这个用户创建一个新的会话(session)
    • shell进程(如bash)作为会话的第一个进程,称为会话首进程(session leader)
    • 会话的ID(SID):等于会话首进程的PID
    • 会话会分配给用户一个控制终端(只能有1个),用于处理用户的输入输出
    • 一个会话包括了该登录用户的所有活动
    • 会话中的进程组由一个前台进程组和N个后台进程组构成

进程与终端的关系

控制终端:跟会话关联的终端,每个会话会分配0或1个控制终端

  • 控制进程:建立与控制终端连接的会话首进程称为控制进程

  • 终端的输入和控制信号会发送给前台进程组中的每一个进程

  • 控制终端与后台进程之间通过信号通信

会话与shell

Shell解释器(bash)

  • 进程组和会话都是为支持shell工作而存在

    • 用户登录login、登录进程login为用户创建一个login session
    • 我们登录的这个终端设备为该会话的控制终端
    • Shell进程为该会话首进程、控制进程
    • Shell进程ID为会话的ID
  • 为了完成一项任务、shell会启动多个进程(脚本、管道命令),这些进程会构成一个进程组

  • 会话的意义在于将很多一起协同工作或相关联的进程、进程组囊括在一个shell内,方便管理(如信号管理、资源管理等)

shell 解释器

什么是shell?

  • 一个读取用户命令、执行命令的程序,也被称为命令解释器

    • 用于人机交互、对shell脚本进行解释、执行

    • 内置环境变量、循环、条件语句、I/O命令、函数等

    • shell可以集成在OS内核中,也可以作为一个独立的应用进程运行

  • 登录shell进程:用户刚登录系统时,由系统创建来运行shell的进程

    • Sh:bourne shell, UNIX标配shell,支持管道、重定向、环境变量、后台执行

    • Csh:脚本语言语法与C语言类似,支持历史记录、命令行编辑等

    • Ksh:兼容sh,并吸取了csh的一些交互式特性

    • Bash:GNU项目,目前Linux上使用最广泛的shell

shell的工作流程

• Linux内核启动后启动init进程、解析/etc/inittab 登录信息启动getty进程

• getty进程调用setsid创建新的session和process group、提示用户登录

• 登录后调用exec加载login程序(/bin/login)(继承getty的PID、PGID、SID)

• Login进程对用户输入的用户名、密码进行验证

• Login根据用户输入到/etc/passwd 查找成功后,找出对应UID、GID,并与/etc/shadow文件中对应账号的UID进行匹配验证

• 验证成功后接着会设置该用户相关的主文件夹、启动shell交互环境

• 验证成功后会fork子进程并通过exec执行shell(如bash\csh等)

• 此时login和shell两个进程同属前台进程组、共享虚拟终端

• Shell进程通过setpgid创建新的进程组、分道扬镳、并将自己设置为前台进程组,跟用户交互

• 用户输入命令$ c1 | c2 |c3,shell会fork 3个子进程

• 为这3个进程创建新的进程组,并将该进程组推向前台

• 3个子进程执行:通过虚拟终端处理输入输出

• 子进程执行完毕、退出,shell进程又重新回到前台,等待用户新命令

前台进程与后台进程

进程的前后台

  • 前台进程:占有控制终端的进程,其它称为后台进程

  • 后台进程:

    • shell中耗时较久的命令可以通过$ command & 后台运行

    • 好处:下一个命令不必等到上一个进程运行完才能运行

    • 一个子进程在创建时若没指定进程组,系统自动创建一个进程组、该子进程为进程组的组长,若该进程后台执行,该进程组为后台进程组

    • 会话将这些进程组囊括在一个shell终端内,选取其中一个进程组用来接收终端的输入或信号,这个进程组成为前台进程组

    • 一个会话可以有多个后台进程组,但只能有一个前台进程组

Shell与前后台

• Shell进程一开始工作在前台,等待用户输入命令

• 用户输入命令,shell进程通过fork & exec执行命令

• shell被提到后台,运行的命令提到前台,接受用户输入

• 前台进程运行结束退出,shell自动被提到前台,等待用户输入

• …

进程的前后台转换

• Shell:前台进程+ 任意多个后台进程

• Ctrl + C:终止并退出前台进程,回到SHELL

• Ctrl + Z:暂停前台命令执行,放到后台,回到SHELL

• jobs:查看当前在后台执行的命令

• &:在后台执行命令

• fg N:将进程号码为 N 的命令放到前台执行

• bg N:将进程号码为 N 的命令放到后台执行

– 注:该号码不是PID,是通过命令jobs看到的后台命令序号

Android中的进程

应用进程的淘汰机制

• 前台进程

• 可见进程

• 服务进程

• 后台进程

守护进程

守护进程的概念

  • Daemon:运行在后台的服务程序,周期性执行系统级任务或等待处理某些发生的事件(热插拔事件、信号等)

  • 独立于终端,不与任何控制终端相关联

  • 打印信息不会打印到终端上

  • 守护进程会创建自己新的会话,避免与其它会话产生关系

守护进程的特点

• 后台运行,不与控制终端相连

• 不受用户登录或注销的影响,一直在运行,一般为系统服务进程

• 生命周期较长,一般随系统启动和关闭,一直运行系统退出

• 不受SIGINT、SIGQUIT、SIGTSTP跟终端相关的信号影响

• 关闭终端不会影响daemon进程的运行

• 守护进程命名:sshd、inetd、httpd (命名不是绝对的、通用的

查看守护进程

$ ps –axj | more

  • 参数a:列出所有用户的进程

  • 参数x:不仅列出控制终端的进程,还列出所有无控制终端的进程

  • 参数j:列出与作业控制相关的信息

守护进程的应用

守护进程的启动

后台进程与守护进程的区别

• 守护进程已经完全脱离终端控制

• 后台程序没有脱离终端控制,一些信息会输出到终端

• 关闭终端时,后台进程会随之终止,而守护进程可以继续运行

• 守护进程有独立的会话、文件描述符、工作目录,而后台进程是继承父进程的

编写一个守护进程并运行

  • 屏蔽一些控制终端操作的信号

  • 调用fork,父进程退出

  • setsid创建一个新会话

  • 禁止进程重新打开控制终端

  • 关闭打开的文件描述符

  • 改变当前工作目录

  • 重设文件创建掩模

  • 处理SIGCHLD信号

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 <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

int main (void) {
signal (SIGTTOU, SIG_IGN); // umask signal from terminal
signal (SIGTTIN, SIG_IGN);
signal (SIGTSTP, SIG_IGN);
signal (SIGHUP, SIG_IGN);

umask (0); // umask file mode inheriting from father process

pid_t ret_id = fork ();
if (ret_id < 0) {
perror ("fork");
exit (-1);
} else if (ret_id > 0) {
exit (0); // father process exit
}

setsid (); // run child process in a new session

signal (SIGCHLD, SIG_IGN); // ignore SIGCHID from child process when exited

ret_id = fork (); // fork again
if (ret_id < 0) {
perror ("fork");
exit (-1);
} else if (ret_id > 0) {
exit (0); // forbidden connect to terminal
}

if (chdir ("/") < 0) {
perror ("chdir");
exit (-1);
}

close (0);
close (1);
close (2);

while (1) {
sleep (1);
}

return 0;
}

僵尸进程

父进程和子进程的关联

  • 在父进程中使用fork创建子进程、在调度器调度下分别调度运行

  • 子进程运行结束退出,内核释放相关资源

    • 释放占用的内存、打开的文件
    • 仍保留一定的信息:进程ID、退出状态、运行时间等
  • 父进程会调用wait/waitid 获取子进程的退出状态,释放最后的资源

什么是僵尸进程?

  • 如果子进程exit退出,父进程没有调用wait获取子进程状态,那么子进程的相关资源仍然保存在系统中,这种进程称为僵尸进程

  • 僵尸进程会占用PID等资源,如果系统中存在大量僵尸进程,会影响fork子进程

创建一个僵尸进程,并观察进程的状态

进程的不同状态

• R:task_running,可执行状态

• S:task_interruptible,可中断的睡眠状态

• D:task_uninterruptible,不可中断的睡眠状态

• T:task_stopped、task_traced,暂停状态或跟踪状态

• Z:task_dead、exit_zombie,退出状态,进程成为僵尸进程

• X:task_dead、exit_dead,退出状态,进程即将被销毁

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

int main (void) {
pid_t pid;
pid = fork ();
if (pid < 0) {
perror ("fork");
exit (-1);
} else if (pid == 0) {
printf ("child process, pid: %d piid:%d\n", getpid (), getppid ());
printf ("child process exit. \n");
exit (1);
}
printf ("father process sleep 5 seconds\n");
sleep (5);
system ("ps -o pid,ppid,state,tty,command ");
printf ("father process exited.\n");

return 0;
}

孤儿进程

什么是孤儿进程?

• 每一个进程都是从父进程fork出来的

• 一般情况下父进程会通过wait/waitid系统调用等待子进程退出,获取到子进程状态,释放相关资源后才会退出

• 若父进程退出时,子进程还没退出,会将进程托管给init进程,则子进程就变成了孤儿进程

编程示例

• 创建一个孤儿进程

• 观察孤儿进程的pid 和 ppid 变化情况

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
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>

int main () {
pid_t pid;
pid = fork ();
if (pid < 0) {
perror ("fork");
exit (-1);
}
if (pid == 0) {
printf ("child process run pid: %d ppid %d\n", getpid (), getppid() );
printf ("child process sleep 5 seconds\n");
sleep (5);
printf ("child process wake pid: %d ppid: %d\n", getpid (), getppid ());
exit (0);
} else {
printf ("father process run pid: %d ppid: %d\n", getpid(), getppid());
printf ("father process sleep 1 seconds\n");
sleep (1);
printf ("father process exited!\n");
}

return 0;
}

• 在图形模式下,孤儿进程的托管进程为upstart,pid不为1

• 在文本模式虚拟终端下,孤儿进程的托管进程init的pid为1

• 为什么?

init服务进程的演变

• Sysvinit

• Upstart

• Systemd

0号进程和1号进程

Linux进程的起源

0号进程:

  • 即idle进程,Linux内核启动后创建的第一个进程

  • 唯一没有 通过fork或者kernel_thread创建的进程

1号进程:即init进程

  • 由idle进程通过kernel_thread创建,在内核空间完成初始化后,加载init程序,转变为用户空间的第一个进程

  • Linux所有用户进程都是由init进程fork创建的,init是用户进程的“祖先”

  • Init进程在系统启动后会转变为守护进程,托管孤儿进程,变为“孤儿院”

2号进程

  • 即kthreadd内核线程,由kernel_thread创建,运行在内核空间

  • 负责内核线程的调度和管理

0号进程

从0到1

  • start_kernel: 初始化内核的各个组件,包括调度器,调用 init_task
  • init_task,内核中所有进程、线程的task_struct的雏形
  • init_task 调用 kernel_thread创建内核 init进程、kthreadadd内核进程
  • 内核初始化后,init_task最终演变为 0号进程 idle
  • 内核开始调度执行,当无进程运行时,会调度 idle 进程运行

1号进程

从1到用户空间进程

  • Start_kernel->rest_init->kernel_thread(kernel_init, NULL, CLONE_FS);

  • 若用户通过init启动参数显式指定,运行用户指定的程序

  • 若没指定:kernel_init->execve(/sbin/init) 运行init进程

    • /sbin/init
    • /etc/init
    • /bin/init
    • /bin/sh
  • 1号init进程从内核态转换为用户态,变为用户进程的“祖先”

  • 用户态init进程从/etc/inittab中完成各种初始化

    • 初始化系统、启动各种服务
    • 启动登录服务
    • 用户态init进程接着执行/bin/bash启动shell进程
    • 0号进程->init内核进程->1号init用户进程->getty进程->shell进程

Linux操作系统的init服务进程

init服务进程的演变

  • sysvinit

    • 通过runlevel预定义运行模式:
      • runlevel 3为命令行模式,5为图形界面模式,0是关机,6是重启。提供各种命令:reboot、shutdown等
      • 运行位于/etc/rc*.d(一般链接到/etc/init.d)的脚本来启动各种系统服务
    • 缺点:按脚本顺序启动服务,耗时较长,不适用消费电子
  • upstart

    • 基于事件驱动机制,动态开启、关闭相关服务
    • 并行启动各种服务,启动速度快,适用于便携式设备
  • systemd

    • Linux桌面系统最新的初始化系统(init)、功能更强大
    • 采用socket与总线激活式提高各个服务的并行运行性能
    • 在Ubuntu等桌面操作系统中广泛使用

嵌入式中的init服务进程

Linux进程全景图

pstree

• 查看当前系统所有进程的关系

• -a:显示系统所有的进程

• -A:使用ASCII字符格式显示

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
sineagle@VM-20-16-ubuntu:~/study/zhaixue/program-linux/chap4/19$ pstree
systemd─┬─YDLive─┬─YDService─┬─sh───8*[{sh}]
│ │ └─23*[{YDService}]
│ └─9*[{YDLive}]
├─accounts-daemon───2*[{accounts-daemon}]
├─acpid
├─2*[agetty]
├─atd
├─barad_agent─┬─barad_agent
│ └─barad_agent───3*[{barad_agent}]
├─bash
├─cron
├─dbus-daemon
├─2*[iscsid]
├─multipathd───6*[{multipathd}]
├─named───7*[{named}]
├─networkd-dispat
├─ntpd───{ntpd}
├─polkitd───2*[{polkitd}]
├─rpcbind
├─rsyslogd───3*[{rsyslogd}]
├─sgagent───{sgagent}
├─sshd─┬─sshd───sshd───bash─┬─sh───node─┬─node───10*[{node}]
│ │ │ ├─node───11*[{node}]
│ │ │ ├─node───12*[{node}]
│ │ │ └─10*[{node}]
│ │ └─sleep
│ └─sshd───sshd───bash───tmux: client
├─systemd─┬─(sd-pam)
│ └─dbus-daemon
├─systemd-journal
├─systemd-logind
├─systemd-network
├─systemd-resolve
├─systemd-udevd
├─tat_agent───6*[{tat_agent}]
├─tmux: server─┬─4*[bash]
│ └─bash───pstree
└─unattended-upgr───{unattended-upgr}

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
systemd(1)─┬─YDLive(6932)─┬─YDService(2199463)─┬─sh(2204018)─┬─{sh}(2204020)
│ │ │ ├─{sh}(2204021)
│ │ │ ├─{sh}(2204022)
│ │ │ ├─{sh}(2204023)
│ │ │ ├─{sh}(2204024)
│ │ │ ├─{sh}(2204025)
│ │ │ ├─{sh}(2204026)
│ │ │ └─{sh}(2248543)
│ │ ├─{YDService}(2199464)
│ │ ├─{YDService}(2199465)
│ │ ├─{YDService}(2199466)
│ │ ├─{YDService}(2199467)
│ │ ├─{YDService}(2199468)
│ │ ├─{YDService}(2199476)
│ │ ├─{YDService}(2199483)
│ │ ├─{YDService}(2199484)
│ │ ├─{YDService}(2199485)
│ │ ├─{YDService}(2199668)
│ │ ├─{YDService}(2199669)
│ │ ├─{YDService}(2199670)
│ │ ├─{YDService}(2199679)
│ │ ├─{YDService}(2199680)
│ │ ├─{YDService}(2199681)
│ │ ├─{YDService}(2199682)
│ │ ├─{YDService}(2199690)
│ │ ├─{YDService}(2199691)
│ │ ├─{YDService}(2199692)
│ │ ├─{YDService}(2199693)
│ │ ├─{YDService}(2199705)
│ │ ├─{YDService}(2199709)
│ │ └─{YDService}(1793109)
│ ├─{YDLive}(6934)
│ ├─{YDLive}(6935)
│ ├─{YDLive}(6936)
│ ├─{YDLive}(6937)
│ ├─{YDLive}(6938)
│ ├─{YDLive}(6941)
│ ├─{YDLive}(11986)
│ ├─{YDLive}(3990932)
│ └─{YDLive}(2763089)
├─accounts-daemon(752)─┬─{accounts-daemon}(757)
│ └─{accounts-daemon}(907)
├─acpid(753)
├─agetty(1080)
├─agetty(1082)
├─atd(894)
├─barad_agent(2288632)─┬─barad_agent(2288633)
│ └─barad_agent(2288634)─┬─{barad_agent}(2288679)
│ ├─{barad_agent}(2288699)
│ └─{barad_agent}(2839931)
├─bash(1869207)
├─cron(882)
├─dbus-daemon(755)
├─iscsid(814)
├─iscsid(815)
├─multipathd(640)─┬─{multipathd}(641)
│ ├─{multipathd}(642)
│ ├─{multipathd}(643)
│ ├─{multipathd}(644)
│ ├─{multipathd}(645)
│ └─{multipathd}(646)
├─named(1866449)─┬─{named}(1866460)
│ ├─{named}(1866461)
│ ├─{named}(1866462)
│ ├─{named}(1866463)
│ ├─{named}(1866464)
│ ├─{named}(1866465)
│ └─{named}(1866466)
├─networkd-dispat(773)
├─ntpd(1871186)───{ntpd}(1871187)
├─polkitd(989)─┬─{polkitd}(1010)
│ └─{polkitd}(1013)
├─rpcbind(665)
├─rsyslogd(1866787)─┬─{rsyslogd}(1866798)
│ ├─{rsyslogd}(1866799)
│ └─{rsyslogd}(1866800)
├─sgagent(1195)───{sgagent}(1196)
├─sshd(1866708)─┬─sshd(2807412)───sshd(2807541)───bash(2807542)─┬─sh(2807586)───node(2807596)─┬─node(2807633)─┬─{node}(2807636)
│ │ │ │ ├─{node}(2807637)
│ │ │ │ ├─{node}(2807638)
│ │ │ │ ├─{node}(2807639)
│ │ │ │ ├─{node}(2807640)
│ │ │ │ ├─{node}(2807651)
│ │ │ │ ├─{node}(2807653)
│ │ │ │ ├─{node}(2807654)
│ │ │ │ ├─{node}(2807655)
│ │ │ │ └─{node}(2807656)
│ │ │ ├─node(2807657)─┬─{node}(2807658)
│ │ │ │ ├─{node}(2807659)
│ │ │ │ ├─{node}(2807660)
│ │ │ │ ├─{node}(2807661)
│ │ │ │ ├─{node}(2807662)
│ │ │ │ ├─{node}(2807663)
│ │ │ │ ├─{node}(2807664)
│ │ │ │ ├─{node}(2807665)
│ │ │ │ ├─{node}(2807666)
│ │ │ │ ├─{node}(2807667)
│ │ │ │ └─{node}(2807682)
│ │ │ ├─node(2807668)─┬─{node}(2807669)
│ │ │ │ ├─{node}(2807670)
│ │ │ │ ├─{node}(2807671)
│ │ │ │ ├─{node}(2807672)
│ │ │ │ ├─{node}(2807673)
│ │ │ │ ├─{node}(2807674)
│ │ │ │ ├─{node}(2807675)
│ │ │ │ ├─{node}(2807676)
│ │ │ │ ├─{node}(2807677)
│ │ │ │ ├─{node}(2807678)
│ │ │ │ ├─{node}(2807679)
│ │ │ │ └─{node}(2807680)
│ │ │ ├─{node}(2807597)
│ │ │ ├─{node}(2807598)
│ │ │ ├─{node}(2807599)
│ │ │ ├─{node}(2807600)
│ │ │ ├─{node}(2807601)
│ │ │ ├─{node}(2807602)
│ │ │ ├─{node}(2807609)
│ │ │ ├─{node}(2807610)
│ │ │ ├─{node}(2807611)
│ │ │ └─{node}(2807612)
│ │ └─sleep(2839834)
│ └─sshd(2807749)───sshd(2807821)─┬─bash(2807822)───pstree(2839932)
│ └─sftp-server(2839467)
├─systemd(9749)─┬─(sd-pam)(9751)
│ └─dbus-daemon(18835)
├─systemd-journal(1863978)
├─systemd-logind(798)
├─systemd-network(1863951)
├─systemd-resolve(1863974)
├─systemd-udevd(1865863)
├─tat_agent(3398784)─┬─{tat_agent}(3398785)
│ ├─{tat_agent}(3398786)
│ ├─{tat_agent}(3398787)
│ ├─{tat_agent}(3398788)
│ ├─{tat_agent}(3398789)
│ └─{tat_agent}(3398790)
├─tmux: server(175001)─┬─bash(175002)
│ ├─bash(175010)
│ ├─bash(1525958)
│ ├─bash(2618846)
│ └─bash(2819679)
└─unattended-upgr(958)───{unattended-upgr}(1075)

进程间通信

进程间通信IPC

进程的地址空间

进程的物理空间

进程间通信

  • 通过文件
  • 通过内核
  • 共享内存
  • 实验:两个进程通过磁盘文件交换数据

使用文件实现两个进程通讯

sender

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
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main (void) {
printf ("write process pid: %d\n", getpid() );
char buf[64] = { 0 };
int n = 0;
while (1) {
if ( (n = read (STDIN_FILENO, buf, 64)) > 0) {
int fd = open ("data.txt", O_WRONLY | O_CREAT, 0664);
if (fd < 0) {
perror ("open");
continue;
}
buf[n] = '\0';
write (fd, buf, n + 1);
close (fd);
}
}

return 0;
}

receiver

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 <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int main () {
char buf[64];
int i;
printf ("read process pid: %d\n", getpid ());
int fd = open ("data.txt", O_RDONLY);

if (fd < 0) {
perror ("open");
return -1;
}
int len = 0;
int last_len = 0;
while (1) {
if ((len = read (fd, buf, 64)) < 0) {
perror ("read");
close (fd);
return -2;
}
printf ("%s", buf);
lseek (fd, SEEK_SET, 0);
sleep (5);
}
close (fd);

return 0;
}

Linux 进程间通信

什么是IPC?

• IPC:inter-process communication

• Pipe、FIFO

• System V IPC: message queue、semaphore、share-memory

• POSIX IPC : message queue、semaphore、share-memory

• Signal

• Socket IPC

• D-BUS

• …

IPC工具的分类

通信

  • 进程之间的数据传输、交换

  • 管道、FIFO、socket、消息队列、共享内存、内存映射

同步

  • 进程或线程操作之间的同步

  • 信号量、条件变量、文件锁、读写锁

异步通信

  • 信号

不同IPC的应用场合

  • 无名管道:只能用于亲缘关系的进程

  • 有名管道:任意两进程间通信

  • 信号量:进程间同步,包括system V 信号量、POSIX信号量

  • 消息队列:数据传输,包括system V 消息队列、POSIX消息队列

  • 共享内存:数据传输,包括system V 共享内存、POSIX共享内存

  • 信号:主要用于进程间异步通信

  • Linux新增API:signalfd、timerfd、eventfd

  • Socket IPC:不同主机不同进程之间的通信

  • D-BUS:用于桌面应用程序之间的通信

无名管道:PIPE

Linux内核中的管道

在内核空间开辟一个缓冲区,不同的进程就可以通过这个缓冲区进行读写达到通信的的目的

对于这样一个缓冲区,内核空间把它看成一个文件来对待,对于用户空间的进程来说,对这片缓冲区像文件一样进行读写,但是这个文件为管道文件,比较特殊是没有名字的,只能通过文件描述符对它进行读写,因此叫做无名管道

PIPE的内核层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// $ locate pipe_fs_i.h
struct pipe_inode_info {
wait_queue_head_t wait;
char *base; //指向管道缓存首地址
unsigned int len; //管道缓存使用的长度
unsigned int start; //读缓存开始的位置
unsigned int readers;
unsigned int writers;
unsigned int waiting_writers;
unsigned int r_counter;
unsigned int w_counter;
struct fasync_struct *fasync_readers;
struct fasync_struct *fasync_writers;
};

通信原理

管道是一个文件(pipefs):

  • 内核将一个缓冲区与管道文件进行关联、封装

  • 用户可通过open/read/write/close等I/O接口进行读写

  • 像一个管道连接两个进程

  • 一个进程的输出作为另一个进程的输入

  • 用于亲缘进程之间的通信:共享资源

PIPE管道编程

创建一个管道

函数原型:

  • int pipe (int pipefd[2]);

  • int pipe2(int pipefd[2], int flags);

函数参数:管道的两个文件描述符:一个用来读、一个用来写

编程实例

  • 单向通信
  • 双向通信

单向通信
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 <string.h>
#include <unistd.h>
#include <errno.h>

#define handle_error(msg) \
{perror (msg); exit(EXIT_FAILURE);}

int main (int argc, char* argv[]) {
int pipe_fd[2];
if (pipe (pipe_fd) == -1)
handle_error ("pipe");
int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == -1)
handle_error ("fork");

if (ret_from_fork == 0) {
char str[100] = {0};
printf ("child process: \n input string: ");
scanf ("%s", str);
write (pipe_fd[1], str, strlen (str));
close (pipe_fd[1]);
_exit (EXIT_SUCCESS);
} else if (ret_from_fork > 0) {
char buf[30] = {0};
read (pipe_fd[0], buf, 30);
printf ("parent process: %s\n", buf);
close (pipe_fd[0]);
exit (EXIT_SUCCESS);
}


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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define handle_error(msg) \
{perror (msg); exit (EXIT_FAILURE); }

int main (int argc, char* argv[]) {
int pipe_fd1[2];
int pipe_fd2[2];

if (pipe (pipe_fd1) == -1 || pipe (pipe_fd2) == -1) handle_error ("pipe");

int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == -1)
handle_error ("fork");
if (ret_from_fork == 0) {
char read_buf1[100] = {0};
char write_buf1[100] = {0};
printf ("child process: ");
scanf ("%s", write_buf1);
write (pipe_fd1[1], write_buf1, strlen (write_buf1));
close (pipe_fd1[1]);

read (pipe_fd2[0], read_buf1, 100);
printf ("in child process %s\n", read_buf1);
close (pipe_fd2[0]);
_exit (EXIT_SUCCESS);
} else if (ret_from_fork > 0) {
char read_buf2[100] = {0};
char write_buf2[100] = {0};
read (pipe_fd1[0], read_buf2, 100);
printf ("in parent process: %s\n", read_buf2);
close (pipe_fd1[0]);

printf ("parent process: ");
scanf ("%s", write_buf2);
write (pipe_fd2[1], write_buf2, strlen (write_buf2));
close (pipe_fd2[1]);
exit (EXIT_SUCCESS);
}


return 0;
}

shell管道的实现

管道的作用

• Shell中具有亲缘关系的进程之间传递消息

• 管道的本质是一个字节流

基本流程和重定向功能

  • 封装成进程:fork/exec系统调用

  • 该进程默认打开的stdin 、stdout 、stderr 连接在终端上

  • 运行的命令从键盘读取数据并且把输出和错误消息写到屏幕上

  • 通过重定向,可以从指定文件读取数据,或将数据输出到指定文件

  • 重定向I/O的功能是由shell本身实现的:标准流与文件的连接

  • 程序本身并不知道数据最后流向哪里:只跟标准流打交道

  • 通过命令:cmd > file 告诉shell将 stdout 定位到文件file,于是shell就将文件描述符与指定的文件连接起来,程序的输出到file,而不是默认的屏幕

输入输出重定向

dup函数和dup2函数

  • 将管道和输入输出设备联系起来

  • 输入、输出重定向到某个设备、文件

  • #include <unistd.h>

  • int dup(int oldfd);

  • int dup2(int oldfd, int newfd);

  • int dup3(int oldfd, int newfd, int flags);

dup

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 <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main1 (void) {
int fd, new_fd;
fd = open ("write.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror ("open");
exit (EXIT_FAILURE);
}

new_fd = dup (fd);
printf ("fd = %d\nnewfd = %d\n", fd, new_fd);
write (fd, "hello", strlen ("hello"));
close (fd);
write (new_fd, "world", strlen ("world"));
close (new_fd);

return 0;
}

int main (void) {
int new_fd;
new_fd = dup (1);
write (1, "hello", strlen ("hello"));
write (new_fd, "world\n", strlen ("world\n"));
close (new_fd);

return 0;
}

dup2

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#define handler_error(msg) \
{ perror (msg); exit (EXIT_FAILURE); }

int main (void) {
int fd, new_fd;
char* buf = "hello world\n";

fd = open ("data.log", O_RDWR | O_CREAT, 0644);
if (fd == -1)
handler_error ("open");

new_fd = dup2 (fd, 1);
if (new_fd == -1)
handler_error ("dup2");

printf ("fd: %d\n new_fd = %d\n", fd, new_fd);
write (1, buf, strlen (buf));
close (fd);

return 0;
}

SHELL管道实现的原理

实现原理:

  • 复制文件描述符:dup2
  • 一个程序的标准输出重定向到管道中
  • 而另一个程序的标准输入从管道中读取

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main (void)
{
int pipe_fd[2];
if (pipe (pipe_fd) == -1) {
perror ("pipe");
exit (EXIT_FAILURE);
} else {
int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == 0) {
dup2 (pipe_fd[1], 1);
execlp ("cat", "cat", "dup.c", NULL);
} else if (ret_from_fork > 0) {
dup2 (pipe_fd[0], 0);
close (pipe_fd[1]);
execlp ("grep", "grep", "include", NULL);
}
}


return 0;
}

通过管道与shell命令进行通信

popen函数

  • FILE *popen(const char *command, const char *type);

  • 创建一个管道,并创建一个子进程来执行shell,shell会创建一个子进程来执行command

  • 将父子进程的输入/输出重定向到管道,建立一个单向的数据流

  • 返回一个fp文件指针给父进程,父进程可根据fp对管道进行读写

  • 向管道中读数据:读命令的标准输出

  • 向管道中写数据:写入该命令的标准输入

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 <unistd.h>
#include <string.h>
#include <stdlib.h>

#define handle_error(msg) \
{perror (msg); exit (EXIT_FAILURE); }

int popen_read (void) {
FILE* fp;
char buf[1024] = {0};
fp = popen ("cat popen.c", "r");
if (fp == NULL)
handle_error ("popen");
fread (buf, 1, 1024, fp);
printf ("%s\n", buf);
pclose (fp);
return 0;
}

int popen_write (void) {
FILE* fp = NULL;
char buf[128] = {0};
fp = popen ("cat > write.log", "w");
if (fp == NULL)
handle_error ("popen");
fwrite ("hello world!\n", 1, sizeof ("hello world!"), fp);
pclose (fp);

return 0;
}

int main () {
popen_read ();
popen_write ();

return 0;
}

通过管道同步进程

PIPE通信应用

通过管道同步进程

  • 管道自带同步互斥机制:

    • 管道的内核实现:fs/pipe.c
    • 通过内核的锁、等待队列等机制实现
  • Write操作可能会阻塞进程

    • 当内存缓冲区已满或被读进程锁定
    • 直到所有数据被写入到管道为止
  • Read操作进程可能会阻塞进程

    • 读进程可以休眠在等待队列,

    • 直到所有子进程都关闭了管道的写入端描述符为止

    • 父进程的写入端描述符也要关闭,否则父进程读管道时也会被阻塞

    • 只有当所有的写端描述符都已关闭,且管道中的数据都被读出,对读端描述符调用read函数才会返回0(即读到EOF标志)

    • 所有的读取端和写入端都关闭后,管道才能被销毁

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 <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

#define handler_error(msg) \
{perror (msg); exit (EXIT_FAILURE); }

int main (void) {
int pipe_fd[2];
int buf;

setbuf (stdout, NULL);
printf ("parent process(%d) run ... \n", getpid() );

if (pipe (pipe_fd) == -1)
handler_error ("pipe");
for (int i = 0; i < 4; i ++) {
int ret_from_fork;
ret_from_fork = fork ();

if (ret_from_fork == -1)
handler_error ("fork");
if (ret_from_fork == 0) {
if (close (pipe_fd[0]) == -1)
handler_error ("close");
sleep (i * i);
printf ("child process(%d) closing pipe\n", getpid());
if (close (pipe_fd[1]) == 1)
handler_error ("close");
_exit (EXIT_SUCCESS);
}
}

if (close (pipe_fd[1]) == -1)
handler_error ("close");
if (read (pipe_fd[0], &buf, 1) != 0) // 父进程阻塞在这里
perror ("parent read cant't get EOF");

printf ("parent process(%d) exit!\n", getpid());

return 0;
}

管道缓冲区的设置

管道缓冲区

  • 管道对应的内存缓冲区大小

    • PIPE_BUF的容量是有限的:默认是65536字节
    • 在不同OS下的PIPE_BUF大小设置不同:在 limits.h 头文件中定义
    • 写入管道的数据超过PIPE_BUF大小,内核会分割几块传输
    • 最大值 /proc/sys/fs/pipe-maxsize
    • 查看打开的管道文件:$ cat /proc/PID/fd
  • 设置缓冲区大小

    • 特权用户:可以修改上限值
    • 设置缓冲区大小:fcntl (fd, F_SETPIPE_SZ, size)

查看上述程序父进程打开的文件描述符,进入到 /proc/xxx 目录下

更改上述程序

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

#define handler_error(msg) \
{perror (msg); exit (EXIT_FAILURE); }

int main (void) {
int pipe_fd[2];
int buf;

setbuf (stdout, NULL);
printf ("parent process(%d) run ... \n", getpid() );

if (pipe (pipe_fd) == -1)
handler_error ("pipe");
for (int i = 0; i < 4; i ++) {
int ret_from_fork;
ret_from_fork = fork ();

if (ret_from_fork == -1)
handler_error ("fork");
if (ret_from_fork == 0) {
if (close (pipe_fd[0]) == -1)
handler_error ("close");
sleep (i * i);
printf ("child process(%d) closing pipe\n", getpid());
while (1);
if (close (pipe_fd[1]) == 1)
handler_error ("close");
_exit (EXIT_SUCCESS);
}
}
while (1);

if (close (pipe_fd[1]) == -1)
handler_error ("close");
if (read (pipe_fd[0], &buf, 1) != 0) // 父进程阻塞在这里
perror ("parent read cant't get EOF");

printf ("parent process(%d) exit!\n", getpid());

return 0;
}

小结

PIPE通信特点

  • 无名管道(匿名管道)是一个字节流

  • 可通过文件I/O接口读写、但无法lseek

  • 单向通信:一端用于写入、一端用于读出

  • 通信简单、性能单一、只能在近亲进程间通信

命名管道:FIFO(named pipe)

FIFO通信特点

FIFO文件,有文件名字

  • 可以像普通文件一样存储在文件系统之中

  • 可以像普通文件一样使用open/read/write读写

  • 跟PIPE一样,属于流式文件,不能使用lseek定位

具有写入原子性、可同时对FIFO进行写操作,如日志系统/var/log

First In First Out:最先被写入FIFO的数据,最先被读出来

  • 默认阻塞读、阻塞写的特性,可以在open的时候进行设置

  • 当一个进程打开FIFO的一端时,如果另一端没有打开,该进程会被阻塞

来自GPT-4的回答:

命名管道(Named Pipe)又称为 FIFO(First In First Out,先进先出)文件,是一种特殊类型的文件,用于在不相关的进程之间进行通信。命名管道主要存储在内存中,但它们在文件系统中具有表示,即一个特殊的文件节点。这意味着命名管道可以像常规文件一样被创建、删除和引用。

通常情况下,当两个进程需要进行通信时,它们会创建一个命名管道。一个进程将以写模式打开管道,另一个进程将以读模式打开管道。写入管道的数据将被存储在内存中的缓冲区里,然后被读取进程接收。当管道不再需要时,它将被删除,从而释放内存。

这里需要注意的是,虽然命名管道在文件系统中具有表示,但实际上数据是存储在内存中的,而不是磁盘上。因此,命名管道在性能方面通常优于将数据写入磁盘的其他通信方式。

FIFO编程实例

系统调用接口

  • shell命令:mkfifo pathname

  • 函数接口:int mkfifo (const char *pathname, mode_t mode);

  • 函数功能:创建一个FIFO有名管道

  • 函数参数:

    • pathname:FIFO管道文件名

    • mode:读写权限

进程通信示例

  • 亲缘关系进程之间通信
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 <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int main (void) {
mkfifo ("my_fifo", 0644);
int ret_from_fork;
int fifo_fd;
ret_from_fork = fork ();

if (ret_from_fork == -1) {
perror ("fork");
exit (EXIT_FAILURE);
} else if (ret_from_fork == 0) {
fifo_fd = open ("my_fifo", O_WRONLY);
char buf[100];
memset (buf, 0, 100);
printf ("child process(%d) input msg: ", getpid ());
fgets (buf, 100, stdin);
int write_len = write (fifo_fd, buf, strlen (buf));
printf ("%d bytes hace been sent\n", write_len);
_exit (EXIT_SUCCESS);
} else {
sleep (1);
fifo_fd = open ("my_fifo", O_RDONLY);
char buf[100];
memset (buf, 0, 100);
read (fifo_fd, buf, 100);
printf ("parent process(%d) read msg from FIFO: %s\n", getpid (), buf);
exit (EXIT_SUCCESS);
}


return 0;
}

  • 非亲缘关系进程之间的通信

write_fifo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int main () {
mkfifo ("my_fifo", 0644);
int fifo_fd = open ("my_fifo", O_WRONLY);
char buf[100];
memset (buf, 0, 100);

fgets (buf, 100, stdin);
int write_len = write (fifo_fd, buf, strlen (buf));
printf ("%d bytes have been sended to FIFO.\n", write_len);

return 0;
}

read_fifo.c

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>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>


int main (void) {
mkfifo ("my_fifo", 0644); // 如果存在则不创建

int fifo_fd = open ("my_fifo", O_RDONLY);

char buf[100];
memset (buf, 0, 100);
read (fifo_fd, buf, 100);
printf ("Read from FIFO: %s\n", buf);


return 0;
}

FIFO内核实现

FIFO与PIPE的区别和联系

  • 联系

    • 在内核中的实现:fs/pipe.c,本质上都是内存中的一块page cache
    • 通过向内核注册pipefs来实现,可以通过I/O接口read、write等访问
  • 区别

    • 匿名管道pipe通过自己的两端读写描述符进行读写操作
    • 命名管道有自己的文件名,可以通过文件名直接进行读写操作
    • 匿名管道pipe一般用于亲缘进程间通信
    • 命名管道FIFO可用于非亲缘进程间通信

FIFO应用:LOG日志系统的实现

Log日志系统

  • 各个进程往FIFO管道写入数据

  • 守护进程使用FIFO接收各个进程的输出日志信息

  • 并将FIFO中的数据写到对应的日志文件中

process.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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_SERVER "fifo_log_server"

int main (void) {
mkfifo (FIFO_SERVER, 0644);
int fifo_fd;

fifo_fd = open (FIFO_SERVER, O_WRONLY);
char buf[100];

while (1) {
memset (buf, 0, 100);
sprintf (buf, "process %d: log ---\n", getpid ());
write (fifo_fd, buf, strlen (buf));
sleep (5);
}

return 0;
}

mylog-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
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define handle_error(msg) \
{perror (msg); exit (EXIT_FAILURE); }
#define FIFO_SERVER "fifo_log_server"
#define LOG_PATHNAME "/var/log/process.log"

int main () {
mkfifo (FIFO_SERVER, 0644);
int ret_from_fork;
char public_buf[100];
int fifo_fd;

fifo_fd = open (FIFO_SERVER, O_RDONLY);
memset (public_buf, 0, 100);

int fd;
fd = open (LOG_PATHNAME, O_WRONLY | O_CREAT | O_APPEND);
if (fd == -1)
handle_error ("open");
int read_len;
while (1) {
read_len = read (fifo_fd, public_buf, 100);
if (read_len == -1) {
handle_error ("read");
} else if (read_len > 0) {
printf ("%s\n", public_buf);
write (fd, public_buf, strlen (public_buf));
} else {
sleep (3);
continue;
}
}
close (fd);

return 0;
}

FIFO应用:服务端与客户端通信

服务器/客户端应用程序

服务端、客户端进程通过FIFO实现双向通信

client

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_SERVER "fifo_server"
#define FIFO_CLIENT "fifo_client"

int main (void) {
int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == -1) {
perror ("fork");
exit (EXIT_FAILURE);
} else if (ret_from_fork == 0) {
int fd_fifo_write;
fd_fifo_write = open (FIFO_SERVER, O_WRONLY);
char buf[100];
while (1) {
memset (buf, 0, 100);
scanf ("%s", buf);
write (fd_fifo_write, buf, strlen (buf) + 1);
}
_exit (EXIT_SUCCESS);
} else {
int fd_fifo_read;
fd_fifo_read = open (FIFO_CLIENT, O_RDONLY);
char buf[100];
while (1) {
memset (buf, 0, 100);
if (read (fd_fifo_read, buf, 100) > 0)
printf ("server: %s\n", buf);
}
exit (EXIT_SUCCESS);
}

return 0;
}

server

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_SERVER "fifo_server"
#define FIFO_CLIENT "fifo_client"

int main () {
mkfifo (FIFO_SERVER, 0644);
mkfifo (FIFO_CLIENT, 0644);

int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == -1) {
perror ("fork");
exit (EXIT_FAILURE);
} else if (ret_from_fork == 0) {
int fd_fifo_write;
fd_fifo_write = open (FIFO_CLIENT, O_WRONLY);
char buf[100];
while (1) {
memset (buf, 0, 100);
scanf ("%s", buf);
write (fd_fifo_write, buf, strlen (buf) + 1);
}
_exit (EXIT_SUCCESS);
} else {
int fd_fifo_read;
fd_fifo_read = open (FIFO_SERVER, O_RDONLY);
char buf[100];
while (1) {
memset (buf, 0, 100);
if (read (fd_fifo_read, buf, 100) > 0)
printf ("client: %s\n", buf);
}
exit (EXIT_SUCCESS);
}

return 0;
}

2个客户端进程通过服务端实现双向通信

server

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_SERVER "fifo_server"
#define FIFO_CLIENT_1 "fifo_client1"
#define FIFO_CLIENT_2 "fifo_client2"


int main (void) {
mkfifo (FIFO_SERVER, 0644);
mkfifo (FIFO_CLIENT_1, 0644);
mkfifo (FIFO_CLIENT_2, 0644);

int ret_from_fork;
int buf_ready = 0;
char public_buf[100];

int fifo_fd_read;
fifo_fd_read = open (FIFO_SERVER, O_RDONLY);
memset (public_buf, 0, 100);

int fifo_fd_write1;
int fifo_fd_write2;
fifo_fd_write1 = open (FIFO_CLIENT_1, O_WRONLY);
fifo_fd_write2 = open (FIFO_CLIENT_2, O_WRONLY);

int read_len;
while (1) {
read_len = read (fifo_fd_read, public_buf, 100);
if (read_len == -1) {
perror ("read");
exit (EXIT_FAILURE);
} else if (read_len > 0) {
printf ("%s\n", public_buf);
if (public_buf[0] == '1') {
write (fifo_fd_write1, public_buf + 1, strlen (public_buf + 1));
}
else if (public_buf[0] == '2')
write (fifo_fd_write2, public_buf + 1, strlen (public_buf + 1));
}
sleep (1);
}



return 0;
}

client1

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_SERVER "fifo_server"
#define FIFO_CLIENT_1 "fifo_client1"

int main () {
mkfifo (FIFO_SERVER, 0644);
mkfifo (FIFO_CLIENT_1, 0644);
int ret_from_fork;

ret_from_fork = fork ();
if (ret_from_fork == -1) {
perror ("fork");
exit (EXIT_FAILURE);
} else if (ret_from_fork == 0) {
int fifo_fd_write;
fifo_fd_write = open (FIFO_SERVER, O_WRONLY);
char buf[100];
while (1) {
memset (buf, 0, 100);
buf[0] = '2';
scanf ("%s", buf + 1);
write (fifo_fd_write, buf, strlen (buf) + 1);
}
_exit (EXIT_SUCCESS);
} else {
int fifo_fd_read;
fifo_fd_read = open (FIFO_CLIENT_1, O_RDONLY);
char buf[100];
while (1) {
memset (buf, 0, 100);
if (read (fifo_fd_read, buf, 100) > 0) {
printf ("client2: %s\n", buf);
}
}
exit (EXIT_SUCCESS);
}

return 0;
}

client2

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_SERVER "fifo_server"
#define FIFO_CLIENT_2 "fifo_client2"

int main () {
mkfifo (FIFO_SERVER, 0644);
mkfifo (FIFO_CLIENT_2, 0644);
int ret_from_fork;

ret_from_fork = fork ();
if (ret_from_fork == -1) {
perror ("fork");
exit (EXIT_FAILURE);
} else if (ret_from_fork == 0) {
int fifo_fd_write;
fifo_fd_write = open (FIFO_SERVER, O_WRONLY);
char buf[100];
while (1) {
memset (buf, 0, 100);
buf[0] = '1';
scanf ("%s", buf + 1);
write (fifo_fd_write, buf, strlen (buf) + 1);
}
_exit (EXIT_SUCCESS);
} else {
int fifo_fd_read;
fifo_fd_read = open (FIFO_CLIENT_2, O_RDONLY);
char buf[100];
while (1) {
memset (buf, 0, 100);
if (read (fifo_fd_read, buf, 100) > 0) {
printf ("client1: %s\n", buf);
}
}
exit (EXIT_SUCCESS);
}

return 0;
}

不同客户端进程通过服务端实现通信

IPC对象和IPC key

进程间通信对象: IPC

什么是 IPC 对象?

  • IPC: Inter-process communication
  • 管道通信:FIFO、PIPE、流式数据
  • 消息队列:message queue
  • 信号量:semaphore
  • 共享内存:share memory

IPC分类

  • System V IPC
    • 消息队列: system V message queue
    • 信号量:system V semaphore
    • 共享内存:system V share memory
  • POSIX IPC
    • 消息队列:POSIX message queue
    • 信号量:POSIX semaphore
    • 共享内存:POSIX share memory

IPC对象

  • 不同进程通过IPC对象通信,IPC对象存储在内核中,全局可见
  • 每个IPC对象在内核中有自己的数据结构,定义在各自头文件中
  • 对IPC对象的引用
    • 普通文件:文件名 – 文件描述符
    • IPC对象:IPC key – IPC标识符

IPC标识符和IPC key

对IPC对象的标识

  • 类似于文件描述符,可通过一个IPC标识符来引用一个IPC对象

  • IPC标识符类似于文件描述符,是一个整数,是IPC对象的内部名

  • 当多个进程引用同一个IPC对象时,需要一个统一的外部命名。

  • 类似于文件名,每个IPC对象与一个键(key)相关联

  • IPC key,是IPC对象的外部名,是一个独一无二的整数,确保唯一性

    • 该键数据类型为key_t,在sys/types.h中被定义为长整型。
    • 普通文件:通过open打开一个文件名,获得文件描述符
    • IPC对象:通过get可根据给定的key去创建一个IPC对象,并返回IPC标识符

IPC key

创建IPC key的三种方法

  • 随机选取一个整数值作为key值。

    • 所有整数放到一个头文件中,使用IPC对象的程序包含这个头文件即可。
  • 在get系统调用中将IPC_PRIVATE常量作为key值。

    • 每个调用都会创建一个全新的IPC对象
    • 从而确保每个对象都拥有一个唯一的key
  • 使用ftok函数生成一个(接近唯一)key

IPC对象的引用

使用基本流程

  • 通过 get 系统调用创建或打开一个 IPC 对象
    • 给定一个整数 key , get系统调用会返回一个整数标识符,即 IPC 标识符
  • 通过这个标识符来引用IPC对象、进行各种操作
  • 通过 ctl 系统调用获取或设置IPC对象的属性,或者删除一个对象
  • IPC对象具有的权限定义在 /Linux/ipc.h 文件中

各种IPC对象的标识符和句柄

System V 消息队列

System V 消息队列

通信方法

  • 支持不同进程之间以消息(message)的形式交换数据。

  • 发送者

    • 获取消息队列的ID(IPC标识符)
    • 将数据放入一个带有标识的消息结构体,发送到消息队列
  • 接收者

    • 获取消息队列的ID
    • 指定标识的消息从消息队列中读出,然后进一步后续处理

编程接口

  • key_t ftok (const char *pathname, int proj_id);

    • 用来生成一个key,类似于文件名
  • int msgget(key_t key, int msgflg);

    • 创建或打开一个消息队列

    • 首先从既有消息队列中搜索与指定key对应的队列,返回该对象的标识符

    • 若没找到,msgflg指定了IPC_CREAT,则创建一个队列,并返回IPC标识符

  • 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);

    • IPC_STAT:获取消息队列的属性信息

    • IPC_SET:设置消息队列的属性

    • IPC_RMID:删除消息队列

通过ipcs命令可以查看内核中创建的消息队列在这里显示出来,以及共享内存和信号量

编程实例

发送消息: msg_snd.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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_TYPE1 1
#define MSG_TYPE2 2

struct msgbuf {
long mtype;
char mtext[80];
};

int main () {
key_t key = 12345;
int msg_id = msgget (key, IPC_CREAT | 0666); // IPC标识符
struct msgbuf msg;
memset (&msg, 0, sizeof (msg));
msg.mtype = MSG_TYPE2;
strncpy (msg.mtext, "hello world\n", 80);

if (msgsnd (msg_id, (void *)&msg, strlen (msg.mtext), 0) == -1 ) {
perror ("msgsnd");
exit (EXIT_FAILURE);
}


return 0;
}

观察到内核中就有了一个消息队列

通过 msg_rcv.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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_TYPE1 1
#define MSG_TYPE2 2

struct msgbuf {
long mtype;
char mtext[80];
};

int main (int argc, char* argv[]) {
key_t key = 12345;
int msg_id = msgget (key, IPC_CREAT | 0666);

struct msgbuf msg;
memset (&msg, 0, sizeof (msg));
if (msgrcv (msg_id, (void*)&msg, sizeof(msg.mtext), MSG_TYPE2, 0) == -1) {
perror ("msgrcv");
exit (EXIT_FAILURE);
}
printf ("%s", msg.mtext);
msgctl (msg_id, IPC_RMID, NULL);


return 0;
}

Linux中的消息队列

内核实现:消息队列模型

  • 相关数据结构:/usr/include/linux/msg.h、/ipc/msg.c

  • msqid_ds:标识整个消息队列的基本情况:消息队列权限、所有者、操作权限,和2个指针,分别指向消息队列中的第一和最后一个消息

  • msg:整个消息队列的主体,一个消息队列有若干个消息,每个消息数据结构的基本信息包括消息类型、消息大小、消息内容指针和下一个消息数据结构位置

  • 消息队列是消息的链表,存储在内核中,由消息队列标识符标识。

  • IPC标识符:消息队列的ID

消息队列的应用:点对点通信

不同进程之间的点对点通信

  • 不同进程之间通过各自指定的消息类型进行点对点通信
  • 不需要经过服务端“中转”分发,由内核充当“代理人”角色
  • 不同的进程可以操作同一个消息队列
  • 各自发送、接收自定义类型的消息,互不影响

client1.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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define handle_error(msg) \
{ perror (msg); exit (EXIT_FAILURE); }

#define MSG_TYPE1 1
#define MSG_TYPE2 2

struct msgbuf {
long mtype;
char mtext[80];
};

int main () {
key_t key = 511;
int msg_id = msgget (key, IPC_CREAT | 0666);
struct msgbuf msg1, msg2;

int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == -1) {
handle_error ("fork");
}
else if (ret_from_fork == 0) {
while (1) {
gets (msg1.mtext);
msg1.mtype = MSG_TYPE1;
msgsnd (msg_id, &msg1, 80, 0);
}
} else {
while (1) {
memset (&msg2, 0, sizeof (msg2));
if (msgrcv (msg_id, (void *)&msg2, sizeof (msg2.mtext), MSG_TYPE2, 0) == -1) {
handle_error ("msgrcv");
}
printf ("client2: %s\n", msg2.mtext);
}
}
msgctl (msg_id, IPC_RMID, NULL);


return 0;
}

client2.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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define handle_error(msg) \
{ perror (msg); exit (EXIT_FAILURE); }

#define MSG_TYPE1 1
#define MSG_TYPE2 2

struct msgbuf {
long mtype;
char mtext[80];
};

int main () {
key_t key = 511;
int msg_id = msgget (key, IPC_CREAT | 0666);
struct msgbuf msg1, msg2;

int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == -1) {
handle_error ("fork");
} else if (ret_from_fork == 0) {
while (1) {
gets (msg2.mtext);
msg2.mtype = MSG_TYPE2;
msgsnd (msg_id, &msg2, 80, 0);
}
} else {
while (1) {
memset (&msg1, 0, sizeof (msg1));
if (msgrcv (msg_id, (void *)&msg1, sizeof (msg1.mtext), MSG_TYPE1, 0) == -1) {
handle_error ("msgrcv");
}
printf ("client1: %s\n", msg1.mtext);
}
}
msgctl (msg_id, IPC_RMID, NULL);


return 0;
}

消息队列的应用:多人聊天室

多用户本地聊天室实例

  • 支持多人同时聊天(3人以上、可以修改程序设置)
  • 每个用户端进程以ID登录,ID作为服务端要发送的消息类型
  • 服务端实现消息的广播转发功能

server.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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define TO_SERVER_MSGTYPE 1000
#define CLIENT_NUMBER 3

struct msgbuf {
long mtype;
int client_id;
char mtext[80];
};

int main () {
key_t key = 512;
int msg_id = msgget (key, IPC_CREAT | 0666);
struct msgbuf msg;

while (1) {
memset (&msg, 0, sizeof (msg));
if (msgrcv (msg_id, (void *)&msg, sizeof (msg.mtext), TO_SERVER_MSGTYPE, 0) == -1 ) {
perror ("msgrcv");
exit (EXIT_FAILURE);
} else {
for (int i = 1; i <= CLIENT_NUMBER; i ++) {
if (i == msg.client_id) continue;
msg.mtype = i;
msgsnd (msg_id, &msg, 80, 0);
}
}
printf ("server: %s\n", msg.mtext);
}
msgctl (msg_id, IPC_RMID, NULL);

return 0;
}

client.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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define handle_error(msg) \
{ perror (msg); exit (EXIT_FAILURE); }
#define TO_SERVER_MSGTYPE 1000

struct msgbuf {
long mtype;
int client_id;
char mtext[80];
};

int main (void) {
key_t key = 512;
int msg_id = msgget (key, IPC_CREAT | 0666);
struct msgbuf msg_snd, msg_rcv;

printf ("input guest ID:");
int client_id;
scanf ("%d", &client_id);
printf ("client_id: %d\n", client_id);

int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == -1) {
handle_error ("fork");
}
else if (ret_from_fork == 0) {
while (1) {
gets (msg_snd.mtext);
msg_snd.mtype = TO_SERVER_MSGTYPE;
msg_snd.client_id = client_id;
msgsnd (msg_id, &msg_snd, 80, 0);
}
} else {
while (1) {
memset (&msg_rcv, 0, sizeof (msg_rcv));
if (msgrcv (msg_id, (void *)&msg_rcv, sizeof (msg_rcv.mtext), client_id, 0) == -1) {
handle_error ("msg_rcv");
} else {
printf ("client %d: %s\n", msg_rcv.client_id, msg_rcv.mtext);
}
}
}


return 0;
}

小结

消息队列与FIFO比较

  • 引用方式

    • 用来标识消息队列的是一个key,而不是普通文件所用的pathname
    • 用来引用消息队列的句柄是一个由msgget()调用返回的标识符。
    • 这些标识符类似于普通文件I/O通过open返回的文件描述符。
  • 传输的数据

    • FIFO发送的数据是流式数据、raw数据
    • 通过消息队列进行的通信是面向消息的
    • 除了包含数据之外,每条消息还有一个用整数表示的类型。
  • 消息队列比FIFO优越的地方

    • 消息队列双方通过消息通信,无需花费精力从字节流中解析出完整信息
    • 每条消息都有type字段,read进程可通过消息类型选择自己感兴趣的消息

消息队列的优点

  • 降低系统耦合:

    • 生产者-消费者模式,自助餐模式,多个读写进程通过容器建立联系、互不影响,实现解耦
    • 消息是跟平台和语言无关的。
  • 提速系统性能:

    • 非核心流程异步化,非阻塞模式节省时间,不需要双方同时在线
  • 广播:

    • 一个消息可以发送给多个进程,只需要发送到队列就可以了
  • 削峰:

    • 生产者-消费者的负载平衡、秒杀活动

消息队列的缺陷

  • 效率低:“代理人”通信机制

  • 陷入内核:为使用者分配内存、检查边界、设置阻塞、权限监控

  • 消息队列的的总数、消息的大小、单个队列的容量是有限制的

    • 注:Linux没有限制

    • MSGMNI:系统中所能创建的消息队列标识符

    • MSGMAX:单条消息中最多可写入的字节数

    • MSGMNB:一个消息队列中一次最多保存的字节数(mtext)

    • MSGTQL:系统中所有消息队列所能存放的消息总数

    • MSGPOOL:消息队列中用于存放数据的缓冲区的大小

  • 使用标识符而不是文件描述符来引用,使用键而不是文件名来标识消息队列,使用复杂

  • 消息队列是无连接的,内核不会像对待管道、FIFO、socket那样维护引用队列的进程数

system V 共享内存

通信原理

  • 多个进程共享物理内存的同一块区域(通常称之为段:segment)
  • 抛弃了内核 “代理人” 角色,让两个进程直接通过一块内存通信

优势

  • 减少了内存拷贝(从用户拷贝到内核、从内核拷贝到用户空间)
  • 减少了2次系统调用,提高了系统性能

操作流程

  • 获取共享内存对象的ID
  • 将共享内存映射到本进程虚拟地址空间的某个区域
  • 不同进程对这块共享内存进行读写、传输数据
  • 当进程不再使用这块共享内存时,解除映射关系
  • 当没有进程再需要这块共享内存时,删除它

相关API

  • 获取共享内存对象的ID: int shmget(key_t key, size_t 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);

  • 查看IPC对象信息:$ ipcs -m

获取共享内存对象的ID

attach共享内存

detach共享内存

设置共享内存的属性

编程实例

shm_read.c

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

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>


int main () {
key_t key = ftok ("./", 514);
int shm_id = shmget (key, 4096, 0666);

char *shm_p = shmat (shm_id, NULL, 0);
printf ("from SHM: %s\n", shm_p);

shmdt (shm_p);

return 0;
}

shm_write.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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <strings.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>


int main () {
key_t key = ftok ("./", 514);
int shm_id;
shm_id = shmget (key, 4096, IPC_CREAT | 0666);
printf ("========== shmid = %d ===========\n", shm_id);

char* shm_p = shmat (shm_id, NULL, 0);
memset (shm_p, 0, 4096);
fgets (shm_p, 4096, stdin);

sleep (30);
shmctl (shm_id, IPC_RMID, NULL);

return 0;
}

共享内存的通信限制

共享内存通信特点

  • 共享内存抛弃了“内核代理人”角色,提升了系统性能

  • 需要进程本身维护共享内存的各种问题:同步、互斥…

  • 一般需要信号量、互斥锁、文件锁等配合使用,在各个进程之间在高效通信的同时,防止发生数据的践踏、破坏。

内存映射mmap和共享内存shmat之间有什么关系?

  • 内存映射mmap可应用与IPC及其他很多方面

system V 信号量

信号量的基本概念

  • 英文:semaphore,简称SEM,主要用于进程间同步
  • 本质:内核维护的一个整数值,可对其进行各种操作+/-操作
  • 分类:system V信号量、POSIX有名信号量、POSIX无名信号量
  • 用途:用来标识系统中可用的共享资源的个数,协调各进程有序的使用这些资源,防止发生冲突
  • 信号量类似于酒店房间的房卡,房卡资源是有限的、房卡也是有限的
  • P操作:程序在进入临界区之前先要对资源进行申请
  • V操作:程序离开临界区后要释放相应的资源,如房卡交给房东

通信原理

• 类似于房卡,不是单个值,而是一组(实际上是数组)信号量元素构成

• 将信号量设置成一个绝对值

• 在信号量当前值的基础上加上一个数量

• 在信号量当前值的基础上减去一个数量,降到0以下可能会引起阻塞

• 阻塞进程一直等待其它进程修改信号量的值,直到恢复正常运行

• 信号量本身无意义,通常会与一块临界资源(如共享内存)关联使用

使用system V 信号量

相关API

  • 获取信号量ID:int semget (key_t key, int nsems, int semflg);
  • P/V操作:int semop (int semid, struct sembuf* sops, size_t nsops);
    • 操作术语:荷兰语中的首字母,由荷兰科学家Edsger Dijkstra确定
    • 其它操作术语:down(减小信号量 )、up(增大信号量)
    • POSIX标准:wait 、post
  • 信号量设置: int semctl (int semid, int semnum, int cmd, ….);

创建或打开一个信号量

  • 函数原型:int semget (key_t key, int nsems, int semflg);

  • 包含头文件:sys/ipc.h sys/sem.h

  • 函数参数:

    • key:用来表示信号量的键,通常使用值IPC_PRIVATE或由ftok创建

    • nsems:信号的数量,所有的信号放在一个数组内

    • semflg:位掩码,用来设置信号量的权限或检查一个已有信号量的权限

      • IPC_CREAT:如果找不到指定key相关联的信号量,创建一个新信号量集合
      • IPC_EXCL:若指定了IPC_CREAT且指定key关联的信号量存在,报EEXIST错误
  • 函数返回值

    • 成功:返回用于操作信号量的句柄ID
    • 失败:-1,并设置errno全局变量

信号量设置

  • 函数原型:int semctl (int semid, int semnum, int cmd, …);

  • 包含头文件:

    • #include <sys/ipc.h>
    • #include <sys/sem.h>
  • 函数参数:

    • semid :用于操作信号量的句柄ID、标识符
    • semnum :信号量集中信号量的编号
    • cmd:
      • IPC_RMID:删除信号量集及相关联的内核semid_ds数据结构
      • IPC_STAT:获取semid_ds 副本
      • IPC_SET:设置semid_ds 数据结构
      • GETVAL:获取信号集中第semnum个信号量的值
      • GETALL:获取所有的信号量的值
      • SETVAL:设置信号集中的第semnum个信号量的值
  • 函数返回值

    • 成功:根据cmd命令,返回不同的值
    • 失败:-1,并设置errno全局变量

信号量P/V操作

  • 函数原型:int semop(int semid, struct sembuf *sops, size_t nsops);

  • 包含头文件:

    • #include <sys/ipc.h>

    • #include <sys/sem.h>

  • 函数参数:

    • semid :用于操作信号量的IPC标识符

    • sops :指向数组的指针,数组中包含了需要执行的操作

    • nsops :sembuf结构体数组的长度

  • 函数返回值

    • 成功:根据cmd命令,返回不同的值

    • 失败:-1,并设置errno全局变量

结构体:sembuf

  • sem_num:用来标识要操作的信号集中的信号量的编号

  • sem_op:

    • 若大于0:将sem_op的值加到信号量值上
    • 若等于0:对信号量值进行检查,确定其当前值是否为0,若为0操作结束,若不为0,则一直阻塞,直到信号量的值变为0为止
    • 若小于0:将信号量值减去sem_op。最后结果大于或等于0,操作立即结束;若最后结果小于0,则当前进程会阻塞
  • sem_flag:

    • SEM_UNDO
    • IPC_NOWAIT
1
2
3
4
5
6
struct sembuf {
unsigned short sem_num;
/* semaphore index in array */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};

使用流程

  • 使用 semget 创建或打开一个信号量集
  • 使用 semctl SETVALSETVAL操作初始化集合中的信号量(其中一个进程操作即可,内核中维护,对其它进程全局可见)
  • 使用 semop 操作信号量值。多个进程通过多信号量值的操作来表示一些临界资源的获取和释放
  • 当所有的进程不再需要信号量集时,使用 semctl IPC_RMID 操作删除这个信号量集(其中一个进程操作即可)

编程实例

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 <unistd.h>

#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>

#define handle_error(msg) \
{perror (msg); exit (EXIT_FAILURE); }

// create a semphore
// print and set semphore
// P/V sem value
// delete sem

union semun {
int val;
struct semid_ds* buf;
unsigned short int* array;
struct seminfo* __buf;
};

int main (void) {
int sem_id;
key_t key;
if ((key = ftok (".", 514)) == -1)
handle_error ("ftok");
if ((sem_id = semget (key, 3, IPC_CREAT | 0770)) == -1)
handle_error ("semget");
printf ("sem id: %d\n", sem_id);

// get sem value
int sem_value;
sem_value = semctl (sem_id, 0, GETVAL);
printf ("sem value : %d\n", sem_value);


union semun sem_union;
sem_union.val = 2;
semctl (sem_id, 0, SETVAL, sem_union);
sem_value = semctl (sem_id, 0, GETVAL);
printf ("sem value : %d\n", sem_value);

// sem p/v operations
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = -1; // if sem value = 0, block
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1)
handle_error ("sem_op");
sem_value = semctl (sem_id, 0, GETVAL);
printf ("sem_value: %d\n", sem_value);

sleep (30);
semctl (sem_id, 0, IPC_RMID);

return 0;
}

信号量编程应用:对共享内存的同步访问

对共享内存的同步访问

  • 通过读、写信号量实现对共享内存的同步互斥访问
  • 实现一个二元信号量协议
    • 读信号量:当为1时,读进程才能进行P操作、读取数据,否则会阻塞
    • 写信号量:当为1时,写进程才能进行P操作,写入数据,否则会阻塞

shm_read.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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/types.h>

union semnu {
int val;
struct semid_ds * buf;
unsigned short int * array;
struct seminfo* __buf;
};

int sem_id;

void sem_init (int semid, int nsignum, int sem_value) {
union semnu sem_union;
sem_union.val = sem_value;
if (semctl (semid, nsignum, SETVAL, sem_union) == -1) {
perror ("semctl");
exit (EXIT_FAILURE);
}
}

void sem_p (int semid, int nsignum) {
struct sembuf sops;
sops.sem_num = nsignum;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1) {
perror ("semop");
exit (EXIT_FAILURE);
}
}

void sem_v (int semid, int nsignum) {
struct sembuf sops;
sops.sem_num = nsignum;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1) {
perror ("semop");
exit (EXIT_FAILURE);
}
}

void sem_print (int sem_id, int nsignum) {
int sem_value;
sem_value = semctl (sem_id, nsignum, GETVAL);
printf ("sem[%d] = %d\n", nsignum, sem_value);
}

int main () {
int shm_id;
key_t shm_key = ftok ("./", 5151);
key_t sem_key = ftok ("./", 5152);

shm_id = shmget (shm_key, 1028, IPC_CREAT | 0644);
char * shm_addr = shmat (shm_id, NULL, 0);

sem_id = semget (sem_key, 2, IPC_CREAT | 0644);
if (sem_id == -1) {
sem_id = semget (sem_key, 2, 0644);
} else {
sem_init (sem_id, 0, 0); // sem[0]: for read
sem_init (sem_id, 1, 1); // sem[1]: for write
}

while (1) {
sem_p (sem_id, 0);
printf ("from shm: %s", shm_addr);
sem_v (sem_id, 1);
}


return 0;
}

shm_write.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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/types.h>

union semnu {
int val;
struct semid_ds * buf;
unsigned short int * array;
struct seminfo* __buf;
};

int sem_id;

void sem_init (int semid, int nsignum, int sem_value) {
union semnu sem_union;
sem_union.val = sem_value;
if (semctl (semid, nsignum, SETVAL, sem_union) == -1) {
perror ("semctl");
exit (EXIT_FAILURE);
}
}

void sem_p (int semid, int nsignum) {
struct sembuf sops;
sops.sem_num = nsignum;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1) {
perror ("semop");
exit (EXIT_FAILURE);
}
}

void sem_v (int semid, int nsignum) {
struct sembuf sops;
sops.sem_num = nsignum;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1) {
perror ("semop");
exit (EXIT_FAILURE);
}
}

void sem_print (int sem_id, int nsignum) {
int sem_value;
sem_value = semctl (sem_id, nsignum, GETVAL);
printf ("sem[%d] = %d\n", nsignum, sem_value);
}

int main () {
int shm_id;
key_t shm_key = ftok ("./", 5151);
key_t sem_key = ftok ("./", 5152);

shm_id = shmget (shm_key, 1028, IPC_CREAT | 0644);
char * shm_addr = shmat (shm_id, NULL, 0);

sem_id = semget (sem_key, 2, IPC_CREAT | 0644);
if (sem_id == -1) {
sem_id = semget (sem_key, 2, 0644);
} else {
sem_init (sem_id, 0, 0); // sem[0]: for read
sem_init (sem_id, 1, 1); // sem[1]: for write
}

while (1) {
sem_p (sem_id, 1);
fgets (shm_addr, 1028, stdin);
sem_v (sem_id, 0);
}

return 0;
}

这样两个进程就可同步的对同一片共享内存进行读写了

信号量编程应用:生产者-消费者模型

编程实例

生产者消费者模型

  • 若有若干个缓冲区,生产者不断往里填数据,消费者不断的从里面取数据
  • 如何使两者不产生冲突呢?
    • 缓冲区只有若干个,且有固定大小,而生产者和消费者则有多个进程
    • 生产者往缓冲区填数据前要判断缓冲区是否满了,满了就会等,直到有空间
    • 消费者从缓冲区拿数据之前要判断缓冲区是否为空,空了就会等,直到缓冲区内有数据为止
    • 在某个时刻,缓冲区只允许一个操作者进行读或写操作

生产消费者模型

  • 2个生产者,每1S、5S往缓冲区写一次数据

  • 3个消费者,每2S、2S、5S往缓冲区读一次数据

  • 2个写进程分别对写信号量做P操作、对读信号量做V操作

  • 3个读进程分别对写信号量做V操作、对读信号量做P操作

producer

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>

void mem_write (int* addr, int data) {
int index;
index = addr[0];
index ++;
addr[index] = data;
addr[0] = index;
}
union semun {
int val;
struct semid_ds * buf;
unsigned short int* array;
struct seminfo * __buf;
};

int sem_id;

void sem_init (int semid, int nsignum, int sem_value)
{
union semun sem_union;
sem_union.val = sem_value;
if (semctl (semid, nsignum, SETVAL, sem_union) == -1)
{
perror ("semctl");
exit (EXIT_FAILURE);
}
}
void sem_p (int semid, int nsignum)
{
struct sembuf sops;
sops.sem_num = nsignum;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1)
{
perror ("semop");
exit (EXIT_FAILURE);
}
}
void sem_v (int semid, int nsignum)
{
struct sembuf sops;
sops.sem_num = nsignum;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1)
{
perror ("semop");
exit (EXIT_FAILURE);
}
}

void sem_print (int sem_id, int nsignum)
{
int sem_value;
sem_value = semctl (sem_id, nsignum, GETVAL);
printf ("sem[%d] = %d\n", nsignum, sem_value);
}

int main () {
int shm_id;
key_t shm_key = ftok ("./", 5161);
key_t sem_key = ftok ("./", 5162);

shm_id = shmget (shm_key, 1028, IPC_CREAT | 0644);
char *shm_addr = shmat (shm_id, NULL, 0);
memset (shm_addr, 0, 128);

sem_id = semget (sem_key, 2, IPC_CREAT | 0644);
if (sem_id == -1) {
perror ("semget");
exit (EXIT_FAILURE);
} else {
sem_init (sem_id, 0, 0);
sem_init (sem_id, 1, 5);
}

int ret_from_fork;
if ((ret_from_fork = fork()) == -1) {
perror ("fork");
exit (EXIT_FAILURE);
} else if (ret_from_fork == 0) {
int child_data = 1;
while (1) {
sleep (1);
sem_p (sem_id, 1);
printf ("child data: %d\n", child_data);
mem_write ((int *) shm_addr, child_data);
child_data = child_data + 2;
sem_v (sem_id, 0);
}
} else {
int parent_data = 2;
while (1) {
sleep (1);
sem_p (sem_id, 1);
printf ("parent data: %d\n", parent_data);
mem_write ((int *) shm_addr, parent_data);
parent_data = parent_data + 2;
sem_v (sem_id, 0);
}
}

return 0;
}

consumer

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>

int mem_read (int* addr) {
int index;
int data;
index = addr[0];
data = addr[index];
index --;
addr[0] = index;
return data;
}

union semun {
int val;
struct semid_ds * buf;
unsigned short int* array;
struct seminfo * __buf;
};

int sem_id;

void sem_init (int semid, int nsignum, int sem_value)
{
union semun sem_union;
sem_union.val = sem_value;
if (semctl (semid, nsignum, SETVAL, sem_union) == -1)
{
perror ("semctl");
exit (EXIT_FAILURE);
}
}
void sem_p (int semid, int nsignum)
{
struct sembuf sops;
sops.sem_num = nsignum;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1)
{
perror ("semop");
exit (EXIT_FAILURE);
}
}
void sem_v (int semid, int nsignum)
{
struct sembuf sops;
sops.sem_num = nsignum;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
if (semop (sem_id, &sops, 1) == -1)
{
perror ("semop");
exit (EXIT_FAILURE);
}
}

void sem_print (int sem_id, int nsignum)
{
int sem_value;
sem_value = semctl (sem_id, nsignum, GETVAL);
printf ("sem[%d] = %d\n", nsignum, sem_value);
}

int main () {
int shm_id;
key_t shm_key = ftok ("./", 5161);
key_t sem_key = ftok ("./", 5162);

shm_id = shmget (shm_key, 1028, IPC_CREAT | 0644);
char *shm_addr = shmat (shm_id, NULL, 0);
memset (shm_addr, 0, 128);

sem_id = semget (sem_key, 2, IPC_CREAT | 0644);
if (sem_id == -1) {
perror ("semget");
exit (EXIT_FAILURE);
} else {
sem_init (sem_id, 0, 0);
sem_init (sem_id, 1, 5);
}
for (int i = 0; i < 2; i ++) {
int ret_from_fork;
if ((ret_from_fork = fork()) == -1) {
perror ("fork");
exit (EXIT_FAILURE);
} else if (ret_from_fork == 0) {
while (1) {
sleep (2);
sem_p (sem_id, 0);
printf ("pid: %d data: %d\n", getpid(), mem_read((int *)shm_addr));
sem_v (sem_id, 1);
}
}
}
while (1) {
sleep (5);
sem_p (sem_id, 0);
printf ("pid: %d data : %d\n", getpid(), mem_read((int *)shm_addr));
sem_v (sem_id, 1);
}

return 0;
}

小结

System V 信号量的通信特点

  • 信号量是通过标识符而不是常用的文件描述符来引用的

  • 使用键而不是文件名来标识信号量

  • 创建和初始化信号量需要使用单独的系统调用

  • 内核不会维护引用一个信号量集的进程数量。很多操作需要开发者自己控制

  • 信号量的操作存在诸多限制

POSIX IPC

POSIX IPC简介

POSIX IPC对象编程接口

POSIX 与 system V IPC

POSIX IPC

  • POSIX接口更简单:使用类似于文件I/O的open、close、unlink等接口

  • POSIX使用名字代替键来标识IPC对象

  • 对IPC 对象引用计数,简化了对 IPC 对象的删除

    • 跟文件类似,删除操作也仅仅是删除了IPC对象的名字
    • 只有当IPC对象的引用计数变成0之后才真正销毁IPC对象

System V IPC

  • System V IPC 可移植性更好:几乎所有的UNIX系统都支持system V,POSIX在UNIX系统中只是一个可选组件,有些UNIX系统并不支持

  • Linux系统一般都会支持system V

  • Linux 2.6开始陆续支持POSIX…

POSIX 编程注意事项

  • 使用POSIX 消息队列和共享内存时,需要实时库librt链接,编译时需指定$ -lrt

  • 使用POSIX 信号量时,需要和线程库libpthread链接起来,编译时需指定$ -lpthread

POSIX消息队列上:API编程实例

相关API

• mq_open: 创建或打开一个消息队列

• mq_send: 向消息队列写入一条消息

• mq_receive:从消息队列中读取一条消息

• mq_close: 关闭进程打开的消息队列

• mq_unlink:删除一个消息队列

• mq_setattr:设置消息队列一些额外的属性

• mq_getattr:获取消息队列一些额外的属性

• mq_nofity: 异步通知

创建和打开IPC对象

函数原型:

  • mqd_t mq_open (const char *name, int oflag);

  • mqd_t mq_open (const char *name, int oflag, mode_t mode, struct mq_attr *attr);

函数功能:使用指定名字创建或打开一个对象,返回该对象的句柄

函数参数:

  • name:用来标识要创建或打开的对象

  • Oflag:O_CREAT/O_EXCL /O_RDONLY /O_WRONLY /O_RDWR /O_NONBLOCK

  • Mode:位掩码,权限设置

  • Attr:设置消息队列的属性,若为NULL,使用默认属性。Linux3.5以后版本也可通过/proc查看设置

函数返回值

  • 成功:返回消息队列的IPC对象描述符

  • 失败:返回-1,并设置errno

关闭POSIX消息队列

  • 函数原型:int mq_close(mqd_t mqdes);

  • 函数功能:通过描述符关闭消息队列

  • TIPS:

    • POSIX 消息队列在进程终止或执行exec()时会自动被关闭

删除一个POSIX 消息队列

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

  • 函数功能:

    • 删除通过 name 标识的消息队列
    • 在所有进程使用完该队列之后销毁该队列。
    • 若打开该队列的所有进程已经关闭该队列,立即删除

向 POSIX 消息队列写入消息

  • 函数原型:int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);

  • 函数功能:将msg_ptr指向的缓冲区中的消息添加到描述符mqdes所引 用的消息队列中

  • 函数参数:

    • mqdes: 消息队列描述符

    • msg_ptr:指向存放消息的缓冲区指针

    • msg_len:消息的长度[10,8192]

    • msg_prio:消息对队列中按优先级排列,设置为0表示无需优先级

从 POSIX 消息队列读取消息

  • ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);

  • 函数功能:

    • 如果msg_prio设置为NULL,则从mqdes引用的消息队列中删除一条优先级最高、存放时间最长的消息
    • 将删除的消息保存在msg_ptr指针指向的缓冲区
  • 函数参数:

    • mqdes: 消息队列描述符

    • msg_ptr:指向存放消息的缓冲区指针

    • msg_len:msg_ptr所指向的缓冲区长度,要大于消息队列的mq_msgsize

    • msg_prio:如不为空,接收到的消息的优先级会被复制到指针指向处

  • 函数返回值

    • 成功:返回接收的消息的字节数

    • 失败:-1,并设置errno

编程实例

父子进程通过消息队列进行通信
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
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>

#define handle_error(msg) \
do {perror (msg); exit (EXIT_FAILURE); } while (0);

int main () {
mqd_t mq_id;
if ((mq_id = mq_open ("/posix_msg_queue", O_RDWR | O_CREAT, 0644, NULL)) == -1)
handle_error ("mq_open");

struct mq_attr mq_attribute;
if (mq_getattr (mq_id, &mq_attribute) == -1)
handle_error ("mq_getattr");
printf ("mq_flags: %ld\n", mq_attribute.mq_flags);
printf ("mq_maxmsg: %ld\n", mq_attribute.mq_maxmsg);
printf ("mq_msgsize: %ld\n", mq_attribute.mq_msgsize);
printf ("mq_curmsgs: %ld\n", mq_attribute.mq_curmsgs);

int ret_from_fork;
ret_from_fork = fork ();
if (ret_from_fork == 0) {
char msg_buf[mq_attribute.mq_msgsize];
memset (msg_buf, 0, mq_attribute.mq_msgsize);
int count = 10;
while (count --) {
if (mq_receive (mq_id, msg_buf, mq_attribute.mq_msgsize, NULL) == -1)
handle_error ("mq_receive");
printf ("child process receive msg: %s\n", msg_buf);
sleep (1);
}
} else if (ret_from_fork > 0) {
int count = 10;
while (count --) {
if (mq_send (mq_id, "hello world", sizeof ("hello world"), 1) == -1)
handle_error ("msg_send");
printf ("parent process: send msg to mqueue success\n");
sleep (1);
}
} else
handle_error ("fork");

mq_close (mq_id);
sleep (5);

if (mq_unlink ("/posix_msg_queue") == -1)
handle_error ("mq_unlink");

return 0;
}
两个不相关的进程通过消息队列进行通信

mq_send.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
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>

int main () {
mqd_t mq_id;
if ((mq_id = mq_open ("/posix_msg_queue", O_WRONLY | O_CREAT, 0644, NULL)) == -1) {
perror ("mq_open");
exit (EXIT_FAILURE);
}
int count = 10;
while (count --) {
if (mq_send (mq_id, "hello world", sizeof ("hello world"), 1) == -1) {
perror ("mq_receive");
exit (EXIT_FAILURE);
}
printf ("msg send success .......... \n");
sleep (1);
}
mq_close (mq_id);

return 0;
}

mq_rcv.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 <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>


int main () {
mqd_t mq_id;
if ((mq_id = mq_open ("/posix_msg_queue", O_RDONLY | O_CREAT, 0644, NULL)) == -1) {
perror ("mq_open");
exit (EXIT_FAILURE);
}
struct mq_attr mq_attribute;
if (mq_getattr (mq_id, &mq_attribute) == -1) {
perror ("mq_getattr");
exit (EXIT_FAILURE);
}
char msg_buf[mq_attribute.mq_msgsize];
memset (msg_buf, 0, mq_attribute.mq_msgsize);

int count = 10;
while (count --) {
if (mq_receive (mq_id, msg_buf, mq_attribute.mq_msgsize, NULL) == -1) {
perror ("mq_receive");
exit (EXIT_FAILURE);
}
printf ("%s\n", msg_buf);
sleep (1);
}
mq_close (mq_id);

if (mq_unlink ("/posix_msg_queue") == -1) {
perror ("mq_unlink");
exit (EXIT_FAILURE);
}


return 0;
}

POSIX消息队列中:异步通知

异步通知API介绍

异步通知API介绍

  • 函数原型:int mq_notify(mqd_t mqdes, const struct sigevent * sevp);

  • 函数功能:

    • 当空的消息队列到来消息时给进程发送一个通知
    • 当执行完相关处理,通知机制结束,可以重新调用mq_notify注册
  • 函数参数:

    • mqdes:消息队列的ID
    • sevp:通知方式设置

关键结构体:sigevent

  • sigev_notify

    • SIGEV_NONE:有通知时什么也不做
    • SIGEV_SIGNAL:给进程发送一个信号来通知进程
    • SIGEV_THREAD/ SIGEV_THREAD_ID
  • sigev_signo:要发送的信号

1
2
3
4
5
6
7
8
struct sigevent {
int sigev_notify; /* Notification method */
int sigev_signo; /* Notification signal */
union sigval sigev_value; /* Data passed with notification */
void (*sigev_notify_function) (union sigval);/* Function used for thread notification (SIGEV_THREAD) */
void *sigev_notify_attributes; /* Attributes for notification thread (SIGEV_THREAD) */
pid_t sigev_notify_thread_id; /* ID of thread to signal (SIGEV_THREAD_ID) */
};

编程实例

mq_notify.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
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

mqd_t mq_id;
char buff[8192];
struct sigevent sigev;

static void signal_handler (int signo) {
ssize_t receive_len;
mq_notify (mq_id, &sigev);
memset (buff, 0, sizeof (buff));
receive_len = mq_receive (mq_id, buff, 8192, NULL);
if (receive_len == -1) {
perror ("mq_receive");
exit (EXIT_FAILURE);
}
printf ("read: %ld bytes: %s\n", (long) receive_len, buff);

return;
}

int main (void) {
mq_id = mq_open ("/notify_mqueue", O_RDONLY | O_CREAT, 0644, NULL);
if (mq_id == -1) {
perror ("mq_open");
exit (EXIT_FAILURE);
}
signal (SIGUSR1, signal_handler);
sigev.sigev_notify = SIGEV_SIGNAL;
sigev.sigev_signo = SIGUSR1;
mq_notify (mq_id, &sigev);

int count = 0;
while (1) {
printf ("while loop %d\n", count ++);
sleep (1);
}
mq_close (mq_id);


return 0;
}
mq_send.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
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>


int main (void) {
mqd_t mq_id;
if ((mq_id = mq_open ("/notify_mqueue", O_WRONLY | O_CREAT, 0644, NULL)) == -1) {
perror ("mq_open");
exit (EXIT_FAILURE);
}
while (1) {
char s[100];
memset (s, 0, sizeof (s));
printf ("please input:");
scanf ("%s", s);
if (mq_send (mq_id, s, strlen(s), 1) == -1) {
perror ("msg_receive");
exit (EXIT_FAILURE);
}
printf ("msg send success....str:%s len = %ld\n", s, strlen(s));
}

}

POSIX消息队列下:内核实现

POSIX消息队列内核实现

  • 相关数据结构:/usr/include/linux/mqueue.h、/ipc/mqueue.c

  • 消息队列是消息的链表,存储在内核中,由消息队列标识符标识。

  • 标识符成为消息队列的ID,程序通过这个句柄可以操作消息队列

  • 消息属性:

    • 一个无符号整数优先级
    • 消息的数据长度(可以为0)
    • 消息的数据本身

system V消息队列内核实现

  • 相关数据结构:msgid_ds 、/usr/include/linux/msg.h、/ipc/msg.c

  • 消息队列是消息的链表,存储在内核中,由消息队列标识符标识。

  • 标识符成为消息队列的ID,程序通过这个句柄可以操作消息队列

  • 消息属性:

    • 一个长整数类型(System V)

    • 消息的数据长度(可以为0)

    • 消息的数据本身

内核中的POSIX消息队列描述符

  • 用来标识打开的消息队列,类似于文件描述符,用来标识打开的文件

  • 是一个进程级句柄,在内核中实现类似于文件描述符

查看设置消息队列的属性

  • 通过proc文件系统

    • $ cat /proc/sys/fs/mqueue/queues_max
    • $ cat /proc/sys/fs/mqueue/msg_max
    • $ cat /proc/sys/fs/mqueue/msgsize_max
  • 通过POSIX系统调用接口

  • mq_setattr:设置mq_flags

  • mq_open:设置mq_maxmsg、mq_msgsize

POSIX消息队列在Linux上实现的特性

  • 提供了mqueue类型的虚拟文件系统

  • 使用另一种I/O模型操作消息队列、获取消息队列的相关信息

    • 可通过挂载、ls、rm命令来列出和删除消息队列
    • $ mkdir /dev/mqueue
    • $ mount -t mqueue none /dev/queue
    • $ cat /dev/mqueue/my_mqueue
    • $ hexdump /dev/mqueue/my_mqueue

POSIX与 system V消息队列

区别和联系

  • POSXI消息队列通过设置优先级,总是返回优先级最高的最早消息

  • System V 消息队列可以通过消息类型返回指定优先级的任意消息

  • POSIX消息队列可以实现异步事件通知

    • 当有一个消息放置到某个空消息队列中时,这种通知有两种方式可以选择:产生一个信号,或者创建一个线程来执行一个指定的函数
    • msgrcv函数接受信息时,若队列为空会阻塞,若设置了NOBLOCK标志,则会不停地调用msgrcv轮询是否有消息到来,非常消耗CPU资源

小结

POSIX消息队列的优势

  • 允许一个进程能够在一条消息进入之前的空队列时异步地通过信号或线程实例化来接受通知
  • 在Linux上可以使用poll、select、epoll 来监控POSIX消息队列
  • POSIX可移植性很差
  • POSIX消息队列严格按照优先级排序,而system V可以根据类型来选择消息,灵活性更强

POSIX 信号量

相关的API

• sem_t *sem_open (const char *name, int oflag);

• sem_t *sem_open (const char *name, int oflag,mode_t mode, unsigned

int value);

• int sem_close (sem_t *sem);

• int sem_post (sem_t *sem);

• int sem_wait (sem_t *sem);

• int sem_trywait (sem_t *sem);

• int sem_timedwait (sem_t *sem, const struct timespec *abs_timeout);

• int sem_unlink (const char *name);

• int sem_getvalue (sem_t *sem, int *sval);

使用指南

  • 包含头文件:#include <semaphore.h>

  • 编译时要指定:-lpthread

  • Pthread:

    • POSIX threads,操作线程的API标准
    • 适用于 Unix、Linux、Mac OS

编程实例

sem_demo.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
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <semaphore.h>
#include <errno.h>
#include <unistd.h>

int main () {
unsigned int sem_value = 4;
sem_t * sem = sem_open ("posix_sem", O_RDWR | O_CREAT | O_EXCL, 0777, sem_value);
if (sem == SEM_FAILED) {
perror ("sem_open");
exit (EXIT_FAILURE);
}
if (sem_getvalue (sem, &sem_value) != -1)
printf ("the sem values: %d\n", sem_value);
sem_wait (sem);
sem_wait (sem);
sem_wait (sem);
sem_wait (sem);
// sem_wait (sem); // 加上这一行会阻塞
// sem_try_wait (); // 不会阻塞
if (sem_getvalue (sem, &sem_value) != -1)
printf ("the sem value: %d\n", sem_value);
sem_post (sem);
sem_post (sem);
sem_post (sem);
sem_post (sem);
sem_post (sem);
sem_post (sem);
if (sem_getvalue (sem, &sem_value) != -1)
printf ("the sem value: %d\n", sem_value);

if (sem_close (sem) != -1) {
printf ("sem close posix_sem success!");
}

printf ("wait for sem_unlink, 10s\n");
sleep (10);

if (sem_unlink ("posix_sem") != -1)
printf ("sem_unlink posix_sem success!\n");


return 0;
}
sem_sync.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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <pthread.h>

#define SEM_NAME "/posix_sem_operation"

int main () {
int i = 0, j = 0;
int ret_fork;
int sem_val = 0;
sem_t *sem;
sem = sem_open (SEM_NAME, O_CREAT, 0666, 1);

ret_fork = fork ();
if (ret_fork == -1) {
perror ("fork");
sem_close (sem);
sem_unlink (SEM_NAME);
exit (EXIT_FAILURE);
}

if (ret_fork == 0) {
while (i ++ < 10) {
sem_wait (sem);
sem_getvalue (sem, &sem_val);
printf ("child process: sem value = %d\n", sem_val);
sleep (1);
}
exit (EXIT_FAILURE);
} else if (ret_fork > 0) {
while (j ++ < 10) {
sem_post (sem);
sem_getvalue (sem, &sem_val);
printf ("parent process: sem value = %d\n", sem_val);
sleep (2);
}
}

sem_close (sem);
sem_unlink (SEM_NAME);

return 0;
}
sem_post.c和sem_wait.c

P操作: sem_wait()

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 <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

int main (void) {
char *name = "/posix_sem";
unsigned int sem_value = 4;
sem_t *sem = sem_open (name, O_RDWR | O_CREAT, 0777, sem_value);
if (sem == SEM_FAILED) {
perror ("sem_open");
exit (EXIT_FAILURE);
}
printf ("sem open %s success\n", name);

while (1) {
if (sem_wait (sem) == -1) {
perror ("sem_wait");
exit (EXIT_FAILURE);
}
if (sem_getvalue (sem, &sem_value) != -1)
printf ("wait process: sem value = %d\n", sem_value);
sleep (1);
}

sleep (10);
sem_close (sem);
if (sem_unlink (name) != -1) {
printf ("sem_unlink %s success\n", name);
}

return 0;
}

V操作:sem_post()

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 <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>


int main (void) {
char *name = "/posix_sem";
unsigned int sem_value = 4;
sem_t *sem = sem_open (name, O_RDWR | O_CREAT, 0777, sem_value);
if (sem == SEM_FAILED) {
perror ("sem_open");
exit (EXIT_FAILURE);
}
printf ("sem_open %s success\n", name);

while (1) {
if (sem_post (sem) == -1) {
perror ("sem_post");
return -1;
}
if (sem_getvalue (sem, &sem_value) != -1) {
printf ("post process: sem value = %d\n", sem_value);
}
sleep (5);
}
sleep (10);

if (sem_unlink (name) != -1) {
printf ("sem_unlink %s success\n", name);
}

return 0;
}

POSIX共享内存

相关的API函数

• int shm_open (const char *name, int oflag, mode_t mode);

• int shm_unlink (const char *name);

• int ftruncate (int fd, off_t length);

• int fstat (int fd, struct stat *buf);

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

• int munmap (void *addr, size_t length);

编程实例

shm_write.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
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>


#define SHM_NAME "/shm"

int main (void) {
int shm_fd;
shm_fd = shm_open (SHM_NAME, O_RDWR | O_CREAT, 0666);
if (shm_fd < 0) {
perror ("shm_open");
return -1;
}
ftruncate (shm_fd, 8192);
struct stat filestat;
fstat (shm_fd, &filestat);
printf ("st_size: %ld\n", filestat.st_size);

char *shm_ptr;
shm_ptr = (char*) mmap (NULL, filestat.st_size, \
PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
close (shm_fd);

char buf[] = "Hello World";
memmove (shm_ptr, buf, sizeof (buf));
printf ("pid %d: %s\n", getpid(), shm_ptr);
munmap (shm_ptr, filestat.st_size);

return 0;
}
shm_read.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
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>


#define SHM_NAME "/shm"


int main (void) {
int shm_fd;
shm_fd = shm_open (SHM_NAME, O_RDWR | O_CREAT, 0);
if (shm_fd < 0) {
perror ("shm_open");
return -1;
}
ftruncate (shm_fd, 8192);
struct stat filestat;
fstat (shm_fd, &filestat);
printf ("st_size: %ld\n", filestat.st_size);
char *shm_ptr;
shm_ptr = (char*) mmap (NULL, filestat.st_size, \
PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
close (shm_fd);
printf ("pid %d: %s\n", getpid(), shm_ptr);
munmap (shm_ptr, filestat.st_size);
shm_unlink (SHM_NAME);

return 0;
}
与信号量结合使用

read_shm_sem.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 <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>

#define SHM_NAME "/memmap"
#define SEM_NAME "/memmap_sem"


int main (void) {
int shm_fd = shm_open (SHM_NAME, O_RDWR | O_CREAT, 0666);
sem_t *sem = sem_open (SEM_NAME, O_CREAT, 0666, 0);
if (shm_fd < 0 || sem == SEM_FAILED) {
perror ("open");
return -1;
}
ftruncate (shm_fd, 8192);
struct stat filestat;
fstat (shm_fd, &filestat);
printf ("st_size: %ld\n", filestat.st_size);
char *shm_ptr;
shm_ptr = (char*) mmap (NULL, filestat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shm_ptr == (void*) -1) {
perror ("mmap");
exit (-1);
}
close (shm_fd);
sem_wait (sem);
printf ("pid %d %s\n", getpid(), shm_ptr);
sem_close (sem);
munmap (shm_ptr, 8192);
shm_unlink (SHM_NAME);
sem_unlink (SEM_NAME);

return 0;
}

write_shm_sem.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
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>

#define SHM_NAME "/memmap"
#define SEM_NAME "/memmap_sem"


int main (void) {
int shm_fd = shm_open (SHM_NAME, O_RDWR | O_CREAT, 0666);
sem_t *sem = sem_open (SEM_NAME, O_CREAT, 0666, 0);
if (shm_fd < 0 || sem == SEM_FAILED) {
perror ("open");
return -1;
}
ftruncate (shm_fd, 8192);
struct stat filestat;
fstat (shm_fd, &filestat);
printf ("st_size: %ld\n", filestat.st_size);
char *shm_ptr = (char*) mmap (NULL, filestat.st_size, \
PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
close (shm_fd);

char msg[] = "hello world";
memmove (shm_ptr, msg, sizeof (msg));
printf ("pid %d: %s\n", getpid(), shm_ptr);

sem_post (sem);
sem_close (sem);
munmap (shm_ptr, 8192);

return 0;
}

文件锁

共享内存

优点

  • 在所有的IPC通信中效率最高

缺点

  • 所有共享内存带来的问题:同步

  • 解决方法

    • System V 信号量
    • POSIX 信号量
    • 文件锁:
      • 专门为文件设计的同步技术
      • 适用于操作共享文件映射

什么是文件锁

  • 英文名:file lock,在同一时刻只允许一个进程对文件进行访问

  • 建议性锁:advisory locking,又称协同锁

    • 内核只提供加减锁以及检测是否加锁,不提供锁的控制与协调工作

    • 需要多进程相互检测确认的加锁机制

    • A进程对一个操作的文件加了锁

    • B进程同样可以对该文件进行读写操作

    • 只有当B进程也对该文件加锁,文件锁才能起到同步作用

    • Linux一般使用建议锁,而Windows一般使用强制性锁

  • 强制性锁:mandatory locking

    • 进程对文件进行I/O操作是,内核内部会检测该文件是否被加锁
    • A进程对一个操作的文件加了锁
    • 当B进程对该文件进行I/O操作时,内核若检测该文件加了强制锁,B进程的操作则会失败

系统调用:flock

  • 函数原型:int flock (int fd, int operation);

  • 函数功能:给整个文件添加或解除一个建议锁

  • 函数参数:operation

    • LOCK_SH:共享锁
    • LOCK_EX:独占锁、排他锁
    • LOCK_UN:移除锁
  • TIPS

    • flock只提供加锁、解锁机制,不提供锁检查
    • 需要用户自己检测,达到多进程同步操作
    • 用户若不自己检测,同样可以对一个已经加锁的文件进行读写操作
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 <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <fcntl.h>
#include <errno.h>

int main (void) {
int fd, i;
char filename[] = "data.log";
extern int errno;
fd = open (filename, O_WRONLY | O_CREAT, 0666);
if (fd != -1) {
printf ("open file %s success\n", filename);
printf ("pls input a num to lock the file.\n");
scanf ("%d", &i);
printf ("try to lock the file...\n");
if (flock (fd, LOCK_EX) == 0)
printf ("lock file success\n");
else
printf ("lock file failed!\n");
write (fd, "hello", 5);
printf ("input a sum to Unlock the file.\n");
scanf ("%d", &i);
if (flock (fd, LOCK_UN) == 0)
printf ("file unlock success\n");
else
printf ("file unlock failed!\n");
while (1);
} else {
perror ("open");
exit (EXIT_FAILURE);
}

return 0;
}

系统调用:fcntl

  • 函数原型:int fcntl (int fd, int cmd, … /* arg */ );

  • 函数功能:给文件(部分文件)进行加锁、解锁操作

  • 函数参数:cmd

    • F_SETLK:非阻塞式申请锁
    • F_SETLKW:阻塞式申请锁
    • F_GETLK:获取锁的相关信息
  • 记录锁

    • 读锁F_RDLCK,写锁F_WRLCK,释放锁F_UNLCK

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 <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

int main (int argc, char *argv[]) {
if (argc > 1) {
int fd = open (argv[1], O_WRONLY | O_CREAT, 0666);
if (fd == -1) {
perror ("open");
exit (-1);
}
static struct flock lock;
lock.l_type = F_WRLCK;
lock.l_start = 0;
lock.l_whence = SEEK_SET;
lock.l_len = 0;
lock.l_pid = getpid ();

printf ("trying lock %s ...\n", argv[1]);
int ret = fcntl (fd, F_SETLKW, &lock);
if (ret == 0) {
printf ("lock %s success\n", argv[1]);
while (1);
}
}

return 0;
}

系统调用:lockf

  • 函数原型:int lockf (int fd, int cmd, off_t len);

  • 函数功能:

    • 可以更细粒度地对文件进行加锁、解锁操作
    • 库函数lockf是对系统调用fcntl的封装
  • 函数参数:operation

    • F_LOCK:对文件某一区域添加独占锁
    • F_TLOCK:非阻塞式申请锁
    • F_ULOCK:对文件某一区域解锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main () {
int fd, ret;
int pid;
fd = open ("tmp.txt", O_RDWR | O_CREAT, 0666);
ret = flock (fd, LOCK_EX);
printf ("flock return ret: %d\n", ret);
ret = lockf (fd, F_LOCK, 0);
printf ("lockf return ret: %d\n", ret);

sleep (30);

return 0;
}

信号机制:signal

信号(signal)

  • 又叫:软中断信号,是一种异步通信的IPC

  • 类似于硬件中断,可以将一个事件以信号形式通知给进程

  • 给一个指定进程发送一个信号

    • 信号只是告诉进程发生了什么事,并不传递数据
    • 进程表的表项中有一个软中断信号域,有信号发给该进程,对应位置位
    • 进程根据接收信号类型作相应的处理

信号的来源

  • 来自shell终端用户输入的各种信号:ctrl + C/D

  • 来自其它进程或者进程本身发送的信号

  • 来自系统内部的信号

    • 硬件异常:如SIGBUS表示总线错误、SIGSEGV表示段错误
    • 终端相关的信号
    • 软件事件相关的信号

一个进程对信号的处理方式

  • 缺省行为

    • 忽略信号:如SIGIGN、SIGCHLD
      • SIGKILL/SIGSTOP比较特殊,不能忽略,所有进程都要在OS管控之下
    • 终止进程:SIGTERM、SIGINT、SIGHUP
    • 终止进程并内核转储:SIGBUS、SIGABRT、SIGQUIT
  • 捕获信号并执行信号注册的handler

    • 通过signal系统调用可以改变信号的处理行为,即注册新的handler
    • 当有信号到来时,信号的处理类似于中断程序
    • 暂停当前进程正在执行的代码、跳到注册的回调函数handler执行
    • 函数返回,回到当前进程捕获信号的地方继续执行
    • 若该信号没有注册回调函数,采用默认操作:忽略或终止进程

信号相关API

  • typedef void (*sighandler_t)(int);

  • sighandler_t signal (int signum, sighandler_t handler);

  • int kill (pid_t pid, int sig);

    • 通过signal注册信号处理函数
    • 进程之间通过kill发送软中断信号
    • 内核也可以因内部异常等事件给进程发信号

系统调用:signal

  • 函数原型:sighandler_t signal (int signum, sighandler_t handler);

  • 函数功能:注册一个信号处理函数

  • 函数参数

    • signum:信号值,定义在:asm/signal.h 头文件中,很多信号跟体系相关
    • handler:信号对应的处理函数
    • Linux支持的信号列表

signal.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
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

void handler (int signo) {
switch (signo) {
case 1:
printf ("get a SIGHUP signal: %d\n", signo);
break;
case 2:
printf ("get a SIGINT signal: %d\n", signo);
break;
case 3:
printf ("get a SIGQUIT signal: %d\n", signo);
break;
}
}

int main (void) {
signal (SIGHUP, handler);
signal (SIGINT, handler);
signal (SIGQUIT, handler);

while (1);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <signal.h>
#include <unistd.h>
#include <stdio.h>


int main () {
while (1) {
kill (3486424, 2);
sleep (2);
}

return 0;
}

系统调用:kill

• 函数原型:int kill (pid_t pid, int sig);

• 函数功能:给指定进程发送一个信号

系统调用:pause

• 函数原型:int pause (void);

• 函数功能:将当前进程挂起睡眠,等待某一个信号,直到信号到来,恢复运行

• 返回值:该函数总是返回-1

定时发送信号

系统调用:alarm

• 函数原型:unsigned int alarm(unsigned int seconds);

• 函数功能:给当前进程在指定的seconds秒后发送一次SIGALRM信号

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

void signal_handler (int unused) {
printf ("catch a signal %d \n", unused);
}

int main (void) {
signal (SIGALRM, signal_handler);
alarm (5);
pause ();

printf ("main exit!\n");
return 0;
}

系统调用:setitimer

  • int getitimer (int which, struct itimerval *curr_value);

  • int setitimer (int which, const struct itimerval *new_value, struct itimerval *old_value);

  • 函数功能:获取定时器状态、设置定时器,周期发送信号

  • 函数参数:which,指定三个内部定时器中的一个

    • ITIMER_REAL:按实际时间计时,计时到达给进程发送SIGALRM信号
    • ITIMER_VIRTUAL:当进程执行时才计时,到期后发送SIGVTALRM信号
    • ITIMER_PROF:当进程执行或系统为该进程执行动作时都计时,如统计进程在用户态和内核态所花的时间,到期后发送SIGPROF信号给进程
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
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>


void signal_handler (int signum)
{
switch (signum)
{
case SIGALRM:
printf ("catch a signal: SIGALRM\n");
break;
case SIGVTALRM:
printf ("catch a signal: SIGVTALRM\n");
break;
}
return;
}

int main (void)
{
struct itimerval value,old_value, value2;
printf ("process id: %d\n", getpid());
signal (SIGALRM, signal_handler);
signal (SIGVTALRM, signal_handler);

value.it_value.tv_sec = 5;
value.it_value.tv_usec = 0;
value.it_interval.tv_sec = 1;
value.it_interval.tv_usec = 0;
setitimer (ITIMER_REAL, &value, &old_value);

value2.it_value.tv_sec = 10;
value2.it_value.tv_usec = 0;
value2.it_interval.tv_sec = 1;
value2.it_interval.tv_usec = 0;
setitimer (ITIMER_VIRTUAL, &value2, &old_value);
while (1)
{
// for(;;);
sleep (5);
}
return 0;
}

编写安全的信号处理函数

信号的本质

  • 是一种软中断,中断有优先级,信号也有优先级
  • 信号处理函数类似于中断处理函数
  • 信号也可以随时打断当前正在运行的进程,去执行信号处理函数

编程要点

  • 重入:可能在任何时刻、任意地点打断当前进程的执行

  • 尽量不要在处理函数中修改全局数据

  • 尽量使用可重入函数,被打断的进程可能正在调用不可重入函数

  • 难点:很难写出一个安全地、可重入的信号处理程序

1
2
3
4
5
6
int sum ;
int add (int count){
static int sum = 0;
for (i from 0 to count)
sum = sum + i;
}

可重入与线程安全

  • 可重入函数一定是线程安全的

  • 不可重入函数通过加锁访问全局变量,也是线程安全的,但仍是不可重入的

  • 如果一个函数对于信号处理来说是可重入的,则称其为异步信号安全函数,可重入函数跟信号安全函数可以看做等价的

  • 线程安全的函数,不一定是异步信号安全的

可重入函数安全列表

信号底层API:sigaction

标准信号及其不可靠性

  • 标准信号

    • 1~31号信号,也叫不可靠信号,继承UNIX信号,采用位图管理
    • 如果同时来相同的信号来不及处理,内核会丢弃掉
  • 实时信号

    • 32~64号信号,是可靠的,采用队列管理
    • 来一次,处理一次,转发一次

内核对信号的处理

  • A进程向B进程发送一个信号,内核会首先收到该信号,然后发给B进程,在发送给B进程之前,内核负责管理这些信号

  • 对于不可靠信号,内核采用位图标记,给该信号分配sigqueue结构体,挂入链表之中,并将位图中的对应位置一;此时若有相同的信号发来,因为对应位已经置一,因此内核会丢弃该信号

  • 对于可靠信号,内核采用队列管理:给该信号分配一个sigqueue结构体,并挂入到链表队列之中

  • 队列中信号的个数也是有限制的,超过默认值,可靠信号也会丢失,也就变得不可靠了。

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 <signal.h>
#include <unistd.h>
#include <stdio.h>


void handler (int signo)
{
switch (signo)
{
case SIGHUP:
printf ("get a SIGHUP signal: %d\n", signo);
break;
case SIGINT:
printf ("get a SIGINT signal: %d\n", signo);
break;
case SIGQUIT:
printf ("get a SIGQUIT signal: %d\n", signo);
break;
case SIGUSR1:
printf ("get a SIGUSR1 signal: %d\n", signo);
break;
default:
printf ("unknow signal\n");
}
for (int i = 0; i < 10000; i++) //delay
{
for (int j = 0; j < 1000; j++)
;
}
printf ("handler exit with signo: %d\n", signo);
}


int main (void)
{
signal (SIGHUP, handler);
signal (SIGINT, handler);
signal (SIGQUIT, handler);
signal (SIGUSR1, handler);

while (1);
return 0;
}
//test 1:
// run: kill -HUP 22380
// run: kill -HUP 22380
// run: kill -HUP 22380
// run: kill -HUP 22380
// run: kill -HUP 22380
//test 2:
// run: kill -HUP 22380
// run: kill -INT 22380
// run: kill -QUIT 22380
// run: kill -USR1 22380
// run: kill -HUP 22380

信号底层注册函数

  • 函数原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

  • 函数功能:给信号设置新的注册函数act,同时保留原有的信号处理函数在oldact

    • 执行某些信号时屏蔽某些信号,直接给sa_mask赋值即可

    • 处理带参数的信号

    • 一次注册,长期有效

1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
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
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

void signal_handler (int signum)
{
printf ("signal_handler\n");
switch (signum)
{
case SIGHUP:
printf ("get signal: SIGHUP\n");
sleep (20);
break;
case SIGINT:
printf ("get signal: SIGINT\n");
break;
case SIGQUIT:
printf ("get signal: SIGQUIT\n");
break;
case SIGUSR1:
printf ("get signal: SIGUSR1\n");
break;
default:
printf ("undefined signal\n");
}
}
void signal_sigaction (int signum, siginfo_t *parm, void *parm2)
{
printf ("signal_sigaction\n");
switch (signum)
{
case SIGHUP:
printf ("get signal: SIGHUP\n");
break;
case SIGINT:
printf ("get signal: SIGINT\n");
break;
case SIGQUIT:
printf ("get signal: SIGQUIT\n");
break;
case SIGUSR1:
printf ("get signal: SIGUSR1\n");
}
printf ("received data: %d\n", parm->si_value);
printf ("sending signal process pid : %d\n", parm->si_pid);
}


int main (void)
{
struct sigaction act, old_act;
act.sa_sigaction = signal_sigaction;
act.sa_handler = signal_handler;
sigemptyset (&act.sa_mask);
sigaddset (&act.sa_mask, SIGUSR1);
act.sa_flags = 0;
//sa_flags must be set,or it will cause core dump
//set 0 may cause signal losing
//act.sa_flags = SA_RESETHAND | SA_NODEFER;
//SA_RESETHAND: restore signal action to DEF
//SA_SIGINFO: use sa_sigaction as signal handler
//SA_NODEFER: umask sa_mask

sigaction (SIGHUP, &act, &old_act);
// sigaction (SIGINT, &act, &old_act);
// sigaction (SIGQUIT, &act, &old_act);
// sigaction (SIGUSR1, &act, &old_act);
while (1);
return 0;
}

新的信号发送函数

  • 函数原型:int sigqueue (pid_t pid, int sig, const union sigval value);

  • 函数功能:

    • 用法与kill类似
    • 与kill不同之处:kill可以将 pid 设置为指定负值,向整个进程发送信号
    • 可以给指定进程传递一个int型数据
    • sigaction 和 sigqueue 是一对 CP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int main (int argc, char *argv[]) {
union sigval val;
val.sival_int = 10;
pid_t pid = atoi (argv[1]);
if (sigqueue (pid, SIGHUP, val) == -1) {
perror ("sigqueue");
exit (EXIT_FAILURE);
}
printf ("current pid: %d\n", getpid());

return 0;
}

小结

  • 一旦给信号安装了handler,它就一直有效

  • 信号的handler存在并发访问、可重入问题

  • 在信号的handler运行期间,可能会阻塞掉当前本身相同的信号

  • 在handler运行期间,当前信号的多次提交可能被丢弃,只保留一次

  • 除了本身信号是被阻塞的,可以通过设置,阻塞设定的一些信号

  • signal是标准C定义的函数,而sigaction是POSIX接口函数

  • Signal是对sigaction的封装

  • 不同的架构、操作系统对信号的value、default action 可能不一样

  • 特殊的两个信号:SIGKILL和SIGSTOP

    • 不能被忽略
    • 不能安装signal handler、不能被捕捉
    • 不能被阻塞

Linux新增API

signalfd

信号通信机制优缺点

• 在软件层次上是对中断机制的一种模拟

• 信号是进程间通信中唯一的“异步通信机制”

• 带来的弊端:数据的并发访问、可重入问题

• 解决方案:将信号抽象为文件,将信号转化为I/O文件操作

signalfd

• 将信号抽象为一个文件描述符

• 将信号的异步处理转换为文件的I/O操作

• 当有信号发生时,可以对其read

• 每次read都会阻塞、直到signalfd指定的信号到来

• 也可以将信号的监听放到select、poll、epoll等监听队列中

  • 函数原型:int signalfd(int fd, const sigset_t *mask, int flags);

  • 函数功能:创建一个可以对信号进行I/O访问的文件描述符

  • 函数参数:

    • mask:进程想通过文件描述符接收的信号集
    • flags:
      • SFD_NONBLOCK:
      • SFD_CLOEXEC:
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
#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define handle_error(msg) \
do {perror (msg); exit (EXIT_FAILURE); } while(0)

void print_signal (struct signalfd_siginfo *siginfo) {
printf ("read signal: %d\n", siginfo->ssi_signo);
printf ("sending process PID: %d\n", siginfo->ssi_pid);
}

int main (void) {
sigset_t mask;
int signal_fd;
struct signalfd_siginfo siginfo;
ssize_t read_len;

sigemptyset (&mask);
sigaddset (&mask, SIGINT);
sigaddset (&mask, SIGQUIT);

if (sigprocmask (SIG_BLOCK, &mask, NULL) == -1)
handle_error ("sigpromask");
signal_fd = signalfd (-1, &mask, 0);
if (signal_fd == -1)
handle_error ("signalfd");
while (1) {
read_len = read (signal_fd, &siginfo, sizeof (struct signalfd_siginfo));
if (read_len != sizeof (struct signalfd_siginfo))
handle_error ("read");
if (siginfo.ssi_signo == SIGINT)
print_signal (&siginfo);
else if (siginfo.ssi_signo == SIGQUIT)
print_signal (&siginfo);
else if (siginfo.ssi_signo == SIGHUP) // 这里永远不会执行
print_signal (&siginfo);
else
printf ("Read unexpect signal!\n");
printf ("for loop\n");
}

return 0;
}

timerfd

timerfd

  • 函数功能:通过文件I/O方式去获取定时器的通知事件

  • 相关API:

    • int timerfd_create(int clockid, int flags);
    • int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
    • int timerfd_gettime(int fd, sturct itimerspec *curr_value);
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <time.h>
#include <sys/time.h>
#include <sys/timerfd.h>

#define handle_error(msg) \
do{perror(msg);exit(EXIT_FAILURE);}while(0)

void print_time ()
{
struct timeval tv;
gettimeofday (&tv, NULL);
printf ("current time: %ld.%ld\n", tv.tv_sec, tv.tv_usec);
}

int main (int argc, char *argv[])
{
struct timespec now;
if (clock_gettime (CLOCK_REALTIME, &now) == -1)
handle_error ("clock_gettime");

struct itimerspec new_value;
new_value.it_value.tv_sec = now.tv_sec + 10; // 10s later,timer will timeout
new_value.it_value.tv_nsec = now.tv_nsec;
new_value.it_interval.tv_sec = 3; //timer intelval
new_value.it_interval.tv_nsec = 0;

int fd = timerfd_create (CLOCK_REALTIME, 0);
if (fd == -1)
handle_error ("timerfd_creat");
if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1)
handle_error ("timerfd_settime");

print_time ();
printf ("timer started\n");

for (uint64_t tot_exp = 0; tot_exp < 20;) //timer run times: 20
{
uint64_t exp;
ssize_t s = read (fd, &exp, sizeof(uint64_t));
if (s != sizeof(uint64_t))
handle_error ("read");
tot_exp += exp;
print_time ();
printf ("read: %llu, total: %llu\n", exp, tot_exp);
}

return 0;
}

eventfd

eventfd API

  • int eventfd(unsigned int initval, int flags);

    • 用来创建一个用于事件通知的eventfd对象
    • 返回值为一个int型fd,用来引用打开的对象
    • 打开的对象在内核中是一个64位的无符号整型计数器,初始化为initval
    • 计数器数据类型:typedef uint64_t eventfd_t
  • flags:

    • EFD_CLOEXEC:类似于open的flags,fork子进程时不继承
    • EFD_NONBLOCK:一般会设置成非阻塞
    • EFD_SEMAPHORE:信号量语义的read,每次读操作,计时器值减1
  • 内核实现:fs/eventfd.c

eventfd操作API

  • read/eventfd_read

    • 读操作将64位的计数器置0
    • 如果有EFD_SEMAPHORE标记,计数器值减1
  • 如果计数器值为零,继续读的话,可能会阻塞或非阻塞

  • write/eventfd_write:

    • 设置计数器的值
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 <sys/eventfd.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define handle_error(msg) \
do {perror (msg); exit (EXIT_FAILURE); } while (0)


int main (int argc, char *argv[]) {
int fd_event, j;
uint64_t u;
ssize_t s;

if (argc < 2) {
fprintf (stderr, "Usage: %s <num>...\n", argv[0]);
exit (EXIT_FAILURE);
}
fd_event = eventfd (0, 0);
if (fd_event == -1)
handle_error ("eventfd");

switch (fork()) {
case 0:
sleep (1);
for (j = 1; j < argc; j ++) {
printf ("Child writing %s to fd_event\n", argv[j]);
u = strtoull (argv[j], NULL, 0);
s = write (fd_event, &u, sizeof (uint64_t));
if (s != sizeof (uint64_t))
handle_error ("error");
}
printf ("Child completed write loop!\n");
exit (EXIT_SUCCESS);
default:
sleep (2);
printf ("Parent about to read!\n");
s = read (fd_event, &u, sizeof (uint64_t));
if (s != sizeof (uint64_t))
handle_error ("read");
printf ("Parent read %llu (0x%llx) from fd_event!\n", (unsigned long long) u, (unsigned long long) u);
exit (EXIT_SUCCESS);
case -1:
handle_error ("fork");

}


return 0;
}

D-BUS总线简介及小结

什么是D-BUS?

• 针对桌面环境,用于本地进程间通信的一种IPC机制

• 主要用于同一桌面会话中,不同桌面应用程序之间的通信

• 同时支持桌面会话与操作系统之间的通信

• 系统总线:system bus,用于内核和应用进程之间的通信和消息传递

• 会话总线:session bus,用于桌面(GNOME/KDE等)用户进程之间的通信。D-BUS一般由一个系统总线和几个会话总线构成

• 总线权限:只有Linux内核、Linux桌面环境和权限较高的应用进程才有权限向系统总线写入消息,安全性高

D-BUS组成

  • Libdbus

    • 点对点的通信支持库,提供C语言API
    • 不同进程可引用库的API进行通信
  • dbus daemon

    • D-BUS服务进程,基于libdbus,作用类似于总线

    • 不同进程可以通过API连接它,发送和接收消息

    • 支持一对一、一对多的通信

  • 基于libdbus的封装库或框架:

    • Libdbus-glib、libdbus-qt
    • 通过dbus binding,支持多种语言:C/C++、Java/Python、Perl、Ruby…
    • 只要应用程序兼容dbus通信协议,可以支持任何语言

对象

  • 每个使用D-BUS的进程都包括一组对象,消息发送到或者发送自某一个对象,这些对象由对象路径唯一标识

  • D-BUS本质上是一个对等(p2p)协议:每个消息都有一个源和目的,这些地址被称为对象路径

  • 每个对象支持一个或多个接口,一个接口是多个方法和信号的集合

在PC桌面环境中的应用

  • 给Linux桌面环境(GNOME、KDE等)提供的服务标准化

  • Android中的Dbus

  • Qt中的Dbus

  • 越多越多的GUI开始支持/兼容Dbus…

  • 例:Ubuntu16.04桌面环境

    • Systemd—lightdm—upstart
      • Dbus-daemon
      • Dcanf-service
      • Gnome-session-b
      • Gnome-terminal-bash-su

各种工具的比较

  • 无名管道:只能用于亲缘进程通信

  • 有名管道:可用于任意两进程间通信,但只能传输流数据、缓冲区大小受限

  • 消息队列:可以传输有格式字节流,但是效率低:系统调用产生的用户空间、内核空间转换的开销

  • 共享内存:通信效率最高最快,解决了进程间通信运行效率低等开销问题,但是可能会带来同步问题

  • 信号量:用来不同进程、线程之间的同步,与共享内存结合使用

  • 文件锁:可以对整个文件、或者文件的一部分区域进行加锁

  • 信号:唯一的异步通信、但是存在一系列的问题

  • Linux特有API:将异步通信操作转换为I/O操作

  • Dbus:桌面进程之间的通信

  • 套接字:适用于不同机器进程间的通信,目前应用最广泛的

多线程编程

多线程编程的概念

关于多线程…

• 有了进程,为什么还要多线程?

• 多线程编程有哪些优点?

• 多线程编程主要用在什么地方?

• SMP、NUMA、MPP

• 多核、4核8线程

进程、线程、协程

• 提高程序运行效率

• 模块细分,防止程序阻塞

• 高并发、多核、服务器

• 线程池、协程

多线程学习准备工作

Pthread线程库

• 线程的实现: windows、Linux

• Pthread库: POSIX标准中的thread API

• Glibc 与 LinuxThread

• Glibc 与 NPTL

• $ getconf GNU_LIBPTHREAD_VERSION

在Linux中通常在用户空间实现对线程的支持

Linux和Windows的API

使用pthread库

  • 安装man手册

    • $ apt install glibc-doc manpages-posix-dev
  • 程序的编译

    • $ gcc main.c -lpthread
    • /usr/lib/libpthread.a
  • pthread常用API

    • pthread_create

    • pthread_exit

    • pthread_cancel

    • pthread_join

    • pthread_detach

    • pthread_mutex_lock

    • pthread_cond_init

    • pthread_cond_signal

    • pthread_cond_wait

    • pthread_rwlock_rdlock

创建一个新线程:pthread_create

API接口说明

  • 函数原型:

    1
    int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
  • 函数功能:创建一个新线程

  • 参数说明:

    • thread:指向线程ID的指针
    • attr: 线程的属性
    • start_routine:线程执行实体入口
    • arg: 传递给线程的参数
    • typedef unsigned long int pthread_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
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>

void *thread_start (void * arg) {
int arg_data = *((int *) arg);
for (int i = 0; i < 10; i ++) {
for (int j = 0; j < 50000; j ++);
printf ("Receive arg %d = %d\n", arg_data, arg_data);
}
}

int main (void) {
pthread_t tid;
int arg1 = 1;
int arg2 = 2;
int ret = pthread_create (&tid, NULL, thread_start, (void *) & arg1);
if (ret) {
perror ("pthread_create");
exit (EXIT_FAILURE);
}
thread_start ((void*) &arg2);

return 0;
}

线程的终止

终止线程的三种方式

  • 从start_routine正常return

  • 显式调用 pthread_exit

    • ​ 函数原型: void pthread_exit(void *retval);
    • 返回值通过参数retval传递
  • 线程被 pthread_cancel取消

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int sum = 0;

void *thread_start (void *arg) {
int i;
int len = *(int*) arg;
for (i = 0; i < len; i ++) {
sum += i;
sleep (1);
printf ("sub thread: sum = %d\n", sum);
}
printf ("sub thread: exit !\n");
pthread_exit (NULL);
}

int main (void) {
pthread_t tid;
int arg = 10;
pthread_create (&tid, NULL, thread_start, (void *) &arg);
sleep (5);
pthread_cancel (tid);
printf ("main thread sum = %d\n", sum);
printf ("main thread: exit\n");

return 0;
}

线程pthread_exit与exit的区别

  • 线程调用pthread_exit,只会结束当前线程,不影响进程的运行
  • 一个进程中的任何一个线程调用exit,将会结束整个进程

等待线程的终止

线程分两种

  • Joinable

    • PTHREAD_CREATE_JOINABLE
    • 可通过 pthread_join 等待线程终止
    • 调用pthread_join 的线程会阻塞
    • 一个Joinable线程结束时,资源不会自动释放给系统(堆栈、exit状态等)
    • 当线程终止时, pthread_join 会回收该线程的资源,然后返回
    • 若无pthread_join参与“擦屁股”工作,该线程将变为僵尸线程
  • Unjoinable

    • PTHREAD_CREATE_DETACHED
    • 可通过 pthread_detach 分离一个线程
    • 当线程终止时,资源会自动释放给系统
    • 主线程不会阻塞

API接口

pthread_join()

  • 函数原型:
1
int pthread_join (pthread_t thread, void **retval);
  • 函数功能:阻塞掉当前线程,等待指定线程终止

  • 函数终止: void pthread_exit (void *retval);

  • 参数:

    • thread:线程的ID
    • retval:从pthread_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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int sum = 0;

void *thread_start (void *arg) {
int i;
int len = *(int *) arg;
for (int i = 0; i < len; i ++) {
sum += 1;
printf ("sub thread: sum = %d\n", sum);
sleep (1);
}
printf ("sub thread: exit !\n");
pthread_exit (NULL);
}

int main () {
pthread_t tid;
int arg = 10;
pthread_create (&tid, NULL, thread_start, (void *) &arg);
pthread_join (tid, NULL);

printf ("sum = %d\n", sum);
printf ("main thread: exit\n");
return 0;
}

pthread_detach()

  • 函数原型:
1
int pthread_detach (pthread_t thread);
  • 函数功能:将指定线程与当前线程分离

  • 参数说明:指定要分离的线程的ID

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>
#include <pthread.h>

int sum = 0;

void *thread_start (void *arg) {
pthread_detach (pthread_self ());
int i;
int len = *(int *) arg;
for (int i = 0; i < len; i ++) {
sum += 1;
printf ("sub thread: sum = %d\n", sum);
sleep (1);
}
printf ("sub thread: exit !\n");
pthread_exit (NULL);
}

int main () {
pthread_t tid;
int arg = 10;
pthread_create (&tid, NULL, thread_start, (void*) &arg);
// pthread_detach (tid);
printf ("sum = %d\n", sum);
printf ("main thread: exit\n");

pthread_exit (NULL);
return 0;
}

线程属性

默认属性

  • 调度参数:

  • 线程栈地址:

  • 线程栈大小:8M

  • 栈末尾警戒缓冲区大小:PAGESIZE

  • 线程的分离状态:joinable、detached

  • 继承性:PTHREAD_INHERIT_SCHED、PTHREAD_EXPLICIT_SCHED

  • 作用域:PTHREAD_SCOPE_PROCESS、PTHREAD_SCOPE_SYSTEM

  • 调度策略:SCHED_FIFO、SCHED_RR、SCHED_OTHER

相关API函数

• int pthread_attr_init (pthread_attr_t *attr);

• int pthread_attr_destroy (pthread_attr_t *attr);

• int pthread_attr_setstacksize (pthread_attr_t *attr, size_t stacksize);

• int pthread_attr_getstacksize (const pthread_attr_t *attr, size_t *stacksize);

• int pthread_attr_setstackaddr (pthread_attr_t *attr, void *stackaddr);

• int pthread_attr_getstackaddr (const pthread_attr_t *attr, void **stackaddr);

• int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate);

• int pthread_attr_getdetachstate (const pthread_attr_t *attr, int *detachstate);

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

int sum = 0;

void * thread_start (void *arg)
{
int len = *(int*)arg;
for (int i = 0; i < len; i++)
{
sum += i;
printf ("sub thead: sum = %d\n", sum);
}
printf ("sub thread: exit\n");
pthread_exit (NULL);
}

int main (void)
{
int status;
pthread_t tid;
int arg = 10;

pthread_attr_t attr;
status = pthread_attr_init (&attr);
if (status != 0)
{
perror ("pthread_attr_init");
exit (EXIT_FAILURE);
}

// set/get thread stack size
int stacksize = 0;
pthread_attr_getstacksize (&attr, &stacksize);
printf ("stacksize = %d kb\n", stacksize/1024);
stacksize = 2 * 1024 * 1024;
pthread_attr_setstacksize (&attr, stacksize);
stacksize = 0;
pthread_attr_getstacksize (&attr, &stacksize);
printf ("stacksize = %d kb\n", stacksize/1024);

//get/set thread detach state
int detachstate;
pthread_attr_getdetachstate (&attr, &detachstate);
printf ("thread detach state: %d\n", detachstate);
pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
// pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_JOINABLE);
pthread_attr_getdetachstate (&attr, &detachstate);
printf ("thread detach state: %d\n", detachstate);


pthread_create (&tid, &attr, thread_start, (void *) &arg);
pthread_attr_destroy (&attr);

// pthread_join (tid, NULL);
printf ("main thread: sum = %d\n", sum);
printf ("main thread: exit\n");
pthread_exit (NULL);
return 0;
}

线程调度与运行

线程分类

  • 核心级线程
    • 由内核调度,有利于并发使用多核处理器资源
  • 用户级线程
    • 由用户层调度,减少上下文切换开销
  • 线程模型
    • 一对一模型
    • 多对一模型
    • 多对多模型

一对一模型

• 用户线程通过LWP关联内核线程

• 线程调度由内核完成

• SMP、并发使用CPU资源

• 线程间同步由用户层完成

• Linux、Windows家族(XP之前)

多对一模型

• 多个用户线程与一个内核线程关联

• 线程管理由用户完成、CPU仍以进程为调度单位

• 单处理器

• Solaris线程库:Green thread

多对多模型

  • 内核线程为CPU调度单元

  • 用户线程管理

Linux下的线程

一对一线程模型

  • 一个轻量进程(LWP)对应一个线程,

  • 每个LWP都与一个内核线程关联

  • 内核线程,通过LWP绑定,调度用户线程

  • 内核线程被阻塞,LWP也阻塞,用户线程也阻塞

  • 调度由内核完成

    • SCHED_OTHER:分时调度策略
    • SCHED_FIFO:实时调度策略:FIFO
    • SCHED_RR:实时调度策略:时间片轮转
  • 创建线程、同步等API由用户线程库完成

    • Linuxthreads:线程PID、信号处理存在不足

    • NPTL(Native POSIX Thread Library)

LWP与普通用户进程比较

  • LWP只有一个最小的执行上下文和调度程序需要的统计信息

  • 用户进程有独立地址空间,LWP与父进程共享地址空间

  • LWP可以像内核线程一样,全局范围内竞争处理器资源

  • LWP调度可以跟用户进程、内核线程一样调度

  • 每一个用户进程可能有一个或多个LWP

  • 通过clone,各进程共享地址空间和资源

    • CLONE_VM、 CLONE_FS
    • CLONE_FILES、CLONE_SIGHAND
  • $ top -H -p <pid>

  • 查看某个指定PID进程下的线程运行

线程安全

进程的虚拟地址空间

多进程下的资源访问

资源划分

  • 共享的资源

    • 代码段、数据段、地址空间

    • 打开的文件、信号处理程序

  • 独占的资源

    • 程序计数器:PC
    • 寄存器
    • 栈空间
      • 不同体系不同分配方式
      • 用户线程和管理线程栈是分离的
  • 进程与线程资源

    • 一室一厅一卫
    • 三室一厅一卫

线程安全

对共享资源的安全访问

  • 临界区与临界资源

  • 关中断

  • 锁、条件变量、读写锁

函数引用

  • 可重入函数

  • 线程安全函数

线程同步

互斥锁

竞争访问

  • 全局变量

  • 缓冲区

相关API函数

• int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

• int pthread_mutex_lock(pthread_mutex_t *mutex);

• int pthread_mutex_trylock(pthread_mutex_t *mutex);

• int pthread_mutex_unlock(pthread_mutex_t *mutex);

• int pthread_mutex_destroy(pthread_mutex_t *mutex);

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int cake_count = 0;
pthread_mutex_t mutex;

static void *produce_thread (void *arg) {
cake_count = *(int *)arg;
while (1) {
pthread_mutex_lock (&mutex);
cake_count += 1;
printf ("produce thread: cake_count = %d\n", cake_count);
pthread_mutex_unlock (&mutex);
sleep (3);
}
printf ("sub thread1: exit\n");
pthread_exit (NULL);
}

static void *consume_thread (void *arg) {
int consume_unit = *(int *) arg;
while (1) {
pthread_mutex_lock (&mutex);
if (cake_count == 0) {
pthread_mutex_unlock (&mutex);
continue;
}
cake_count -= consume_unit;
printf ("consume thread consume 1: cake_count = %d\n", cake_count);
pthread_mutex_unlock (&mutex);
sleep (1);
}
printf ("sub thread2 exit!\n");
pthread_exit (NULL);
}

int main (void) {
pthread_t tid1, tid2;

pthread_mutex_init (&mutex, NULL);

int arg1 = 10;
int arg2 = 1;
pthread_create (&tid1, NULL, produce_thread, (void *) &arg1);
pthread_create (&tid2, NULL, consume_thread, (void *) &arg2);
pthread_join (tid1, NULL);
pthread_join (tid2, NULL);

pthread_mutex_destroy (&mutex);
printf ("main thread: exit\n");

return 0;
}

条件变量

基本概念

互斥锁缺陷:

  • 不断加锁解锁、查询满足条件,开销很大

  • 加锁开销:用户态-内核态-用户态,阻塞在内核态

  • 解锁开销:用户态-内核态-用户态,唤醒等待线程

条件变量

  • 互斥锁(mutex)搭配使用,允许线程阻塞,等待条件满足的信号

优势:

  • 将互斥锁和条件变量绑定

  • 省去了不断加锁解锁的开销

  • 可以使用广播(broadcast)唤醒所有绑定到该条件变量的线程

相关API函数

• pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

• int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *cond_attr);

• int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);

• int pthread_cond_signal (pthread_cond_t *cond);

• int pthread_cond_broadcast (pthread_cond_t *cond);

• int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex,

const struct timespec *abstime);

• int pthread_cond_destroy (pthread_cond_t *cond);

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int current_count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

static void *produce_thread (void *arg) {
int count_init = * (int *) arg;
current_count = count_init;
while (1) {
pthread_mutex_lock (&mutex);
current_count ++;
printf ("produce thread: current_count = %d\n", current_count);
if (current_count % 10 == 0)
pthread_cond_signal (&cond);
pthread_mutex_unlock (&mutex);
sleep (1);
}
printf ("produce thread: exit\n");
pthread_exit (NULL);
}

static void *consume_thread (void *arg) {
int consume_unit = *(int *) arg;
while (1) {
pthread_mutex_lock (&mutex);
pthread_cond_wait (&cond, &mutex); // 释放锁,阻塞,等待条件满足时的信号
current_count -= 10;
printf ("consume thread has consume: %d\n", consume_unit);
pthread_mutex_unlock (&mutex);
sleep (1);
}
printf ("consume thread: exit\n");
pthread_exit (NULL);
}

int main () {
pthread_t tid1, tid2;
pthread_mutex_init (&mutex, NULL);
int arg1 = 5;
int arg2 = 10;
pthread_create (&tid1, NULL, produce_thread, (void *) &arg1);
pthread_create (&tid2, NULL, consume_thread, (void *) &arg2);
pthread_join (tid1, NULL);
pthread_join (tid2, NULL);

pthread_mutex_destroy (&mutex);
pthread_cond_destroy (&cond);

printf ("main thread exit!\n");
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int current_count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

static void *produce_thread (void *arg) {
int count_init = * (int *) arg;
current_count = count_init;
while (1) {
pthread_mutex_lock (&mutex);
current_count ++;
printf ("produce thread: current_count = %d\n", current_count);
pthread_mutex_unlock (&mutex);
if (current_count % 20 == 0)
pthread_cond_broadcast (&cond);
sleep (1);
}
printf ("produce thread: exit\n");
pthread_exit (NULL);
}

static void *consume_thread (void *arg) {
int consume_unit = *(int *) arg;
while (1) {
pthread_mutex_lock (&mutex);
pthread_cond_wait (&cond, &mutex); // 释放锁,阻塞,等待条件满足时的信号
current_count -= 10;
printf ("consume thread1 has consume: %d\n", consume_unit);
pthread_mutex_unlock (&mutex);
sleep (1);
}
printf ("consume thread: exit\n");
pthread_exit (NULL);
}
static void *consume_thread2 (void *arg) {
int consume_unit = *(int *) arg;
while (1) {
pthread_mutex_lock (&mutex);
pthread_cond_wait (&cond, &mutex); // 释放锁,阻塞,等待条件满足时的信号
current_count -= 10;
printf ("consume thread2 has consume: %d\n", consume_unit);
pthread_mutex_unlock (&mutex);
sleep (1);
}
printf ("consume thread: exit\n");
pthread_exit (NULL);
}



int main () {
pthread_t tid1, tid2, tid3;
pthread_mutex_init (&mutex, NULL);
int arg1 = 5;
int arg2 = 10;
int arg3 = 10;
pthread_create (&tid1, NULL, produce_thread, (void *) &arg1);
pthread_create (&tid2, NULL, consume_thread, (void *) &arg2);
pthread_create (&tid3, NULL, consume_thread2, (void *) &arg2);
pthread_join (tid1, NULL);
pthread_join (tid2, NULL);
pthread_join (tid3, NULL);

pthread_mutex_destroy (&mutex);
pthread_cond_destroy (&cond);

printf ("main thread exit!\n");
return 0;
}

读写锁

基本概念

  • 互斥锁:同一时刻只允许一个线程读或写

  • 读写锁

    • 允许多个读线程同时读
    • 只允许一个线程写,写的时候会阻塞其它线程(包括读线程)
    • 写优先级高于读

相关API函数

• pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

• pthread_rwlock_init (pthread_rwlock_t *restrict rwlock,

const pthread_rwlockattr_t *restrict attr);

• int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);

• int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);

• int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);

• int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);

• int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);

• int pthread_rwlock_destroy (pthread_rwlock_t *rwlock)

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int cake_count = 0;
pthread_rwlock_t rwlock;

void *write_thread (void *arg) {
static int id = 0;
int i = ++id;
int produce = *(int *) arg;
while (1) {
pthread_rwlock_wrlock (&rwlock);
cake_count += produce;
printf ("produce thread %d: cake_count = %d\n", i, cake_count);
pthread_rwlock_unlock (&rwlock);
sleep (1);
}
printf ("produce thread: exit!\n");
pthread_exit (NULL);
}

void *read_thread (void *arg) {
static int id = 0;
int i = ++id;
int consume_unit = *(int *) arg;
while (1) {
pthread_rwlock_rdlock (&rwlock);
printf ("consume thread %d cake_count = %d\n", i, cake_count);
pthread_rwlock_unlock (&rwlock);
sleep (1);
}
printf ("consume thread %d: exit\n", i);
pthread_exit (NULL);
}

int main () {
pthread_t produce_tid1, produce_tid2;
pthread_t consume_tid1, consume_tid2, consume_tid3;
int produce1 = 10;
int produce2 = 20;
int consume1 = 5;
int consume2 = 10;
int consume3 = 15;

pthread_rwlock_init (&rwlock, NULL);
pthread_create (&produce_tid1, NULL, write_thread, (void *) &produce1);
pthread_create (&produce_tid2, NULL, write_thread, (void *) &produce2);
pthread_create (&consume_tid1, NULL, read_thread, (void *) &consume1);
pthread_create (&consume_tid2, NULL, read_thread, (void *) &consume2);
pthread_create (&consume_tid3, NULL, read_thread, (void *) &consume3);

pthread_join (produce_tid1, NULL);
pthread_join (produce_tid2, NULL);
pthread_join (consume_tid1, NULL);
pthread_join (consume_tid2, NULL);
pthread_join (consume_tid3, NULL);

pthread_rwlock_destroy (&rwlock);
printf ("main thread: exit!\n");

return 0;
}

线程池

线程的开销

• 系统调用的开销:线程的创建、销毁

• 上下文切换、互斥锁等加锁解锁

线程池原理

• 预先在池中创建一些线程

• 无任务时,线程阻塞在池中

• 有任务时,将任务分配到指定的线程执行

• 池中线程的数目甚至可根据任务多少动态删减

实现原理

管理线程

  • 创建并管理线程

  • 任务分配运行

工作线程

  • 线程池中实际执行任务的线程

    • 任务接口
  • 每个任务的实现接口

超线程技术

基本概念

共享打印机

  • 电脑—打印机

  • 电脑1—打印机

  • 电脑2—打印机

超线程技术

  • Hyper-Threading,简称HT

  • 使用特殊指令将一个物理处理器视为2个逻辑处理器

  • 每个逻辑处理器都可以分配一个线程运行

  • 最大限度地提升CPU资源利用率

实现原理

CPU微架构资源划分

  • 复制的资源:每个逻辑CPU都有一套完整的体系结构状态

  • 划分的资源:重定序(re-order)缓冲、load/store缓冲、队列等

    多任务模式下这些资源划分给2个逻辑CPU使用

    单任务模式下这些资源划分给1个逻辑CPU使用

  • 共享的资源:执行计算单元(加、减)、cache、总线

主板的支持

  • 如I7的X58芯片组

  • 主板BIOS支持超线程

  • 操作系统的支持:Windows XP以后、Linux2.4以后

  • 应用层支持:NPTL库

实现原理

  • 交替工作

  • 共享单元

    • 执行单元
    • 缓存
    • 总线
  • 应用场所

    • 服务器
    • 工作站

协程

从进程到线程

协程的概念

基本概念

  • 线程的开销

    • 上下文切换开销
    • 互斥锁的开销
  • 协程

    • 对共享资源的访问由程序本身维护控制

    • 不需要锁,无调度成本,执行效率高

    • 适合彼此熟系的程序组件:合作式多任务、管道

    • 进程 + 线程 -> 进程 + 协程

协程的实现

  • Python:yield/send、async/await(python3.5以后)

  • Lua:Lua5.0开始使协程

  • Go:支持线程,后端并发

  • C语言:协程库

进程、线程与协程

  • 进程、线程是系统级,而协程是语言级

  • 进程:资源分配的基本单位

  • 线程:程序执行的基本单位

  • 切换成本:进程 > 线程 > 协程

  • 安全性:进程 > 线程 > 协程

  • 协程缺陷:无法利用多核CPU,做到真正的并发

求大佬赏个饭