Linux系统编程

Linux系统编程

一、静态和动态

在linux中有两种有静态和动态的方式

1.静态链接和动态链接

链接分为两种:静态链接、动态链接

1)静态链接

静态链接:由链接器在链接时将库的内容加入到可执行程序中。

优点:

  • 对运行环境的依赖性较小,具有较好的兼容性

缺点:

  • 生成的程序比较大,需要更多的系统资源,在装入内存时会消耗更多的时间
  • 库函数有了更新,必须重新编译应用程序

2)动态链接

动态链接:连接器在链接时仅仅建立与所需库函数的之间的链接关系,在程序运行时才将所需资源调入可执行程序。

优点:

  • 在需要的时候才会调用对应的资源函数
  • 简化程序的升级;有着较小的程序体积
  • 实现进程之间的志愿共享(避免重复拷贝)

缺点:

  • 依赖动态库,不能独立运行
  • 动态库依赖版本问题严重

3)静态、动态编译对比

在Linux中的gcc是默认使用动态链接来生成代码。

如果想使用静态链接来生成代码,则需要使用下面的指令

1
gcc -static

剩下的和动态链接一样

2、静态库和动态库介绍

所谓”程序库“,简单说,就是包含了数据和执行码的文件。其不能单独执行,可以作为出现都一部分来完成某些功能

库的存在可以使得程序模块化,可以加快程序的再编译,可以实现代码重用,可以使得程序便于升级

程序库可分 静态库共享库

3.静态库制作和使用

静态库可以认为是一些目标代码的集合,是在可执行程序运行前就已经加入到执行代码中,成为执行程序的一部分。

按照习惯,一般以 .a 作为文件后缀名。静态库的命名一般分为三个部分:

  • 前缀:lib
  • 库名称:自己定义即可
  • 后缀: .a

所以最终的静态库的名字应该为:libxxx.a

3.1 静态库制作

步骤1:将C源文件生成对应的.o文件

步骤2:使用打包工具ar将准备好的.o文件打包为.a文件 libtest.a

在使用ar工具是需要添加参数:rcs

  • r更新
  • c创建
  • s建立索引

3.2 静态库使用

静态库制作完成后,需要将.a文件和头文件一起发布给用户。

假设测试文件为test.c,静态库文件为libtest.a头文件为head.h

编译命令:

1
gcc test.c -L. -I. -l test -o test

参数说明:

  • -L:表示要连接的库所在目录
  • -l(小写L):指定链接时需要的库,去除前缀和后缀

在执行程序的时候,程序1使用到这个静态库,就会在内存中开辟出这块空间来存放静态库,程序2执行的时候也会和程序1的过程一样,会产生大量的空间,导致程序会很大,如果执行多次那空间就会非常大

4.动态库制作和使用

共享库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入、不同的应用程序如果调用系统的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。

动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布带来的麻烦。用户只需要更新动态库即可,增量更新。

按照习惯,一般以 .so 作为文件后缀名。共享库的命名一般分为三个部分:

  • 前缀:lib
  • 库名称
  • 后缀:.so

所以最终的动态库的名字应该为:libxxx.so

4.1 动态库制作

步骤一:生成目标文件,此时要加编译选项:-fpic

参数:-fpic创建与地址无关的编译程序,是为了能够在多个应用程序间共享。

步骤二:生成共享库,此时要加链接器选项:-shared(指定生成动态链接库)

1
gcc -shared *.o -o libxxx.so

步骤三:通过nm命令查看对应函数

1
nm libtest.so | grep add

4.2 动态库使用

在使用的时候也是需要对其进行编译

编译完成后运行,会发现报错了

原因是因为在链接的时候需要用到连接器才可以使用

  • 当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统动态载入器。
  • 对于elf格式的可执行程序,是由ld-linux.so*来完成的,它先后搜索elf文件的 DT_RPATH段-环境变量LD_LIBRARY_PATH-/etc/ld.so.cache文件列表-/lib/,/usr/lib目录找到库文件将其载入内存

4.3 如何让系统找到动态库

  • 将动态库放到 /lib 目录中即可

  • 临时设置LD_LIBRARY_PATH:

    1
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径
  • 永久设置,把 export LD_LIBRARY_PATH=&LD_LIBRARY_PATH:库路径 ,设置到 ~/.bashrc/etc/profile 文件中

    export LD_LIBRARY_PATH=&LD_LIBRARY_PATH:库路径 添加到最后一行即可

    执行下面代码让文件生效

    1
    ~/.bashre
  • 将其添加到 /etc/ld.so.conf 文件中

    编辑 /etc/ld.so.conf 文件,加入库文件所在目录的路径

    运行 sudo ldconfig -v ,该命令会重建 /etc/ld.so.cache 文件

  • 使用符号链接,但一定要使用绝对1路径

    1
    sudo ln -s /home/deng/test/6share_test/libtest.so /lib/libtest.so

5.GDB调试器

5.1 GDB简介

GNU工具集中的调试器是GDB,该程序是一个交互式工具,工作在字符模式。除GDB外,Linux下比较有名的调试器还有xxgdb,ddd,kgdb,ups

GDB主要帮助你完成下面四个方面的功能:

1.启动程序,可以按照你的自定义的要求随心所欲的运行程序。

2.可以被调试的程序在你所指定的调置的断点处停止。

3.当程序被停住时,可以检查此时你的程序中所发生的事

4.动态的改变你的程序的执行环境

5.2 生成调试信息

一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的-g参数可以做到这一点

1
2
gcc -g hello.c -o hello
g++ -g hello.cpp -o hello

如果没有-g,你将看不到程序的函数名、变量名,所替代的全是运行时的内存地址

5.3 启动GDB

  • 启动GDB:gdb program

    program 也是你的执行文件,一般在当前目录下。

  • 设置运行参数

    set args 可指定运行时参数。(如:set args 10 20 30 40 50)

    show args 命令可以查看设置好的运行参数。

  • 启动程序

    run:程序开始执行,如果有断点,停在第一个断点处

    start:程序向下执行一行

5.4 显示源代码

用list命令来打印程序的源代码。默认打印10行。

  • list linenum:打印第linenm行的上下文内容
  • list function:显示函数名为function的函数的源程序
  • list:显示当前行后面的源程序
  • list -:显示当前行前面的源程序

一般是打印当前行的上5行和下5行,如果显示的函数是上2行下8行,默认10行,当然,你也可以定制显示的范围,使用下面命令可以设置一次显示源程序的行数

  • set listsize count:设置一次显示源代码的行数
  • show listsize:查看当前listsize的设置。

5.5 断点操作

5.5.1 简单断点

break设置断点,可以简写为b

  • b 10 设置断点,在源文件第10行
  • b func 设置断点,在func函数入口处
5.5.2 多文件设置断点

如果有名称空间,可以使用namespace::class::function或者function(type, type) 格式来指定函数名。

  • break filename:linenum –在源文件filename的linenum行处停住
  • break filename:function – 在源文件filename的function函数的入口处停住
  • break class::function 或 function(type, type) – 在类class的function函数入口处停住
  • break namespace::class::function – 在名称空间为namespace的类class的function函数的入口处停住
5.5.3 查询所有断点
  • info b
  • info break
  • i break
  • i b
5.5.4 条件断点

一般来说,为断点设置一个条件,我们使用if关键词,后面跟其断点条件。

设置一个条件断点:

b test.c:8 if Value == 5

5.5.5 维护断点

1)delete [range…] 输出指定的断点,其简写命令为d。

  • 如果不指定断点号,则表示删除所有的断点。range表示断点号的范围*如:3-7)。
  • 比删除更好的一种方法是disable停止点,disable了的停止点,GDB不会删除,当你还需要时,enable即可,就好像回收站一样

2)disable [range…]使指定断点无效,简写命令是dis。

​ 如果什么都不指定,表示disable所有的停止点无效

3)enable [range…] 使无效断定失效,缩写命令是ena。

​ 如果什么都不指定,表示enable所有的停止点

5.5.6 调试代码
  • run 运行程序,可简写为r
  • next 单步跟踪,函数调用当作一条简单语句执行,可简写为n
  • step 单步跟踪,函数调用进入被调用函数体内,可简写为s
  • finish 退出进入的函数
  • until 在一个循环体内单步跟踪时,这个命令可以运行直到退出循环体,可简写为u。
  • continue 继续运行程序,停在下一个断点的位置,可简写为c
  • quit 退出gdb,可简写为q
5.5.7 数据查看

1)查看运行时数据

​ print 打印变量、字符串、表达式等的值,可简写为p

​ p count 打印count的值

5.5.8 自动显示

可以设置一些自动显示的变量,当程序停住时,或是在你单步跟踪时,这些变量会自动显示。相关的GDB命令是display。

  • display 变量名
  • info display – 查看设置的自动显示的信息。
  • undisplay num (info display时显示的编号)
  • delete display dnums… – 删除自动显示,dnums意为所设置好了的自动显示的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围内的编号,可以用减号表示(如:2-5)
  • disable display dnums…
  • enable display dnums…
  • disable和enalbe不删除自动显示的设置,而只是让其失效和恢复
5.5.9 查看修改变量

1)ptype width – 查看变量width的类型

​ type = double

2)p width – 打印变量width的值

你可以使用set var命令告诉GDB,width不是你GDB的参数,而是程序的变量名,如:

​ set var width=47 //将变量var值设置为47

在吧改变程序变量取值时,最好都使用set var格式的GDB命令

二、自动化编译

1.Makefile

1.1 Makefile简介

一个工程中的源文件不记其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至进行更复杂的功能操作,因为makefile就像一个shell脚本一样,其中也可以执行操作系统的命令。

Makefile代码来的好处就是–“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令。

make主要解决两个问题:

1)大量代码的关系维护

大项目中源代码比较多,手工维护、编译时间长而且命令复杂,难以记忆及维护

把代码维护命令及编译命令写在makefile文件中,然后再用make工具解析此文件自动执行相应命令,可实现代码的合理编译

2)减少重复编译时间

在改动其中一个文件的时候,能判断哪些文件被修改过,可以只对该文件进行重新编译,然后重新链接所有目标文件,节省编译时间

Makefile文件命名规则

makefile和Makefile都可以

make工具安装

sudo apt install make

1.2 Makefile语法规则

一条规则:

目标:依赖文件列表

命令列表

Makefile基本规则三要素:

1)目标:

  • 通常是要产生的文件名称,目标可以是可执行文件或其他obj文件,也可以是一个动作的名称

2)依赖文件:

  • 用来输入从而产生目标的文件
  • 一个目标通常有几个依赖文件(可以没有)

3)命令:

  • make执行的动作,一个规则可以含几个命令(可以没有)
  • 有多个命令时,每个命令占一行

1.3 make命令格式

make是一个命令工具,它解释Makefile中的指令(应该说是规则)。

make命令格式:

make [-f file][options][targets]

1.[-f file]:

  • make默认在工作目录中寻找名为makefile、makefile、Makefile的文件作为makefile输入文件
  • -f 可以指定以上名字以外的文件作为makefile输入文件

2.[options]

  • -v:显示make工具的版本信息
  • -w:在处理makefile之前和之后显示工作路径
  • -C dir:读取makefile之前改变工作路径至dir目录
  • -n:只打印要执行的命令但不执行
  • -s:执行但不显示执行的命令

3.[targets]:

  • 若使用make命令时没有指定目标,则make工具默认会实现makefile文件内的第一个目标,然后退出
  • 指定make工具要实现的目标,目标可以是一个或多个(多个目标间用空格隔开)。

1.4 Makefile实例

比如要编译之前写的Add.c Sub.c Div.c Mul.c

在Makefile文件中的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
test:Add.o Sub.o Div.o Mul.o test.o
gcc Add.o Sub.o Div.o Mul.o test.o -o test
Add.o:Add.c
gcc -c Add.c -o Add.o
Sub.o:Sub.c
gcc -c Sub.c -o Sub.o
Div.o:Div.c
gcc -c Div.c -o Div.o
Mul.o:Mul.c
gcc -c Mul.c -o Mul.o
test.o:test.c
gcc -c test.c -o test.o

这种写法后,在如果又修改其他文件话只需要重新编译刚写的文件

1.5 Makefile中的变量

在Makefile中使用变量有点类似于C语言中的宏定义,使用该变量相当于内存替换,使用变量可以使Makefile易于维护,修改内容变得简单变量定义及使用。

1.5.1 自定义变量

1)定义变量方法:

​ 变量名 = 变量值

2)引用变量:

​ $(变量名)或${变量名}

3)makefile的变量名:

  • makefile量名可以以数字开头
  • 变量是大小写敏感的
  • 变量一般都在makefile的头部定义
  • 变量几乎可以在makefile的任何地方使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
OBJS = Add.o Sub.o Div.o Mul.o test.o
test:${OBJS}
gcc ${OBJS} -o test
Add.o:Add.c
gcc -c Add.c -o Add.o
Sub.o:Sub.c
gcc -c Sub.c -o Sub.o
Div.o:Div.c
gcc -c Div.c -o Div.o
Mul.o:Mul.c
gcc -c Mul.c -o Mul.o
test.o:test.c
gcc -c test.c -o test.o

除了使用用户自定义变量,makefile中也提供了一些变量(变量名大写)供用户直接使用,我们可以直接对其进行赋值。

CC=gcc #arm-linux-gcc

CPPFLAGS: C预处理器的选择 如:-l

CFLAGS:C编译器的选择 -Wall -g -c

LDFLAGS:链接器选择 -L -l

1.5.2 自动变量
  • $@:表示规则中的目标
  • $<:表示规则中的第一个依赖
  • $^:表示所有依赖
  • $?:所有目标依赖中被修改过的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
OBJS = Add.o Sub.o Div.o Mul.o test.o
OBJS = Add.o Sub.o Div.o Mul.o test.o
TARGET=test
test:${OBJS}
gcc $^ -o ${TARGET}
Add.o:Add.c
gcc -c $< -o $@
Sub.o:Sub.c
gcc -c $< -o $@
Div.o:Div.c
gcc -c $< -o $@
Mul.o:Mul.c
gcc -c $< -o $@
test.o:test.c
gcc -c $< -o $@
clear:
rm -rf ${OBJS} ${TARGET}

1.5.3 规则模式

模式规则示例:

%.o:%.c

$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o &@

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
OBJS = Add.o Sub.o Div.o Mul.o test.o
TARGET=test
test:${OBJS}
gcc $^ -o ${TARGET}
%.o:%.c
gcc -c $^ -o $@
#Add.o:Add.c
# gcc -c $< -o $@
#Sub.o:Sub.c
# gcc -c $< -o $@
#Div.o:Div.c
# gcc -c $< -o $@
#Mul.o:Mul.c
# gcc -c $< -o $@
#test.o:test.c
# gcc -c $< -o $@
clear:
rm -rf ${OBJS} ${TARGET}

1.6 Makefile中的函数

makefile中的函数有很多,在这里给大家介绍两个最常用的。

1.wildcard - 查找指定目录下的指定类型的文件

​ src = $(wildcard *.c) //找到当前目录下所有后缀为.c的文件,赋值 给src

2.patsubst - 匹配替换

obj = $(patsubst %.c, %.o, $(src)) //把src变量里所有后缀为.c的文件替换成.o

在makefile中所有的函数都有返回值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#获取当前目录下所有的.c文件
SRC = $(wildcard ./*.c)

#将SRC中所有出现的.c的替换成.o
OBJS = $(patsubst %.c, %.o, $(SRC))

TARGET=test
test:${OBJS}
gcc $^ -o ${TARGET}
%.o:%.c
gcc -c $^ -o $@
clear:
rm -rf ${OBJS} ${TARGET}

1.7 Makefile中的伪目标

clean用途:清除编译生成的中间文件.o文件和最终目标文件

make clean如果当前目录下有同名clean文件,则不执行clean对应的命令,解决方案:

  • 伪目标声明:.PHONY:clean

    声明目标为伪目标之后,makefile将不会该判断目标是否存在或者该目标是否需要更新

clean命令中的特殊符号:

  • - 此条命令出错,make也会继续执行后续的命令。如:“-rm main.o”
  • “@”不显示命令本身,只显示结果。如:“@echo clean done”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#获取当前目录下所有的.c文件
SRC = $(wildcard ./*.c)

#将SRC中所有出现的.c的替换成.o
OBJS = $(patsubst %.c, %.o, $(SRC))

TARGET=test
test:${OBJS}
@gcc $^ -o ${TARGET}
%.o:%.c
@gcc -c $^ -o $@
.PHONY:clean
clear:
-rm -rf ${OBJS} ${TARGET}

1.8 makefile工作原理

1)若想生成目标,检查规则中的依赖条件是否存在,如果不出来,则寻找是否有规则用来生成该依赖文件

2)检查规则中的目标是否需要更新,必须先检查它的所有依赖,依赖中有任一个被更新,则目标必须更新

总结:

  • 分析各个目标和依赖之间的关系
  • 根据依赖关系自底向上执行命令
  • 根据修改事件比目标新,确定更新
  • 如果目标不依赖任何条件,则执行对应命令,以示更新

三、系统文件调用

1.系统文件调用简介和实现

1.1 什么是系统调用

操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过区组“特殊“接口来获得操作系统内核提供的服务,比如说用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获取系统时间或设置定时器等。

从逻辑上来说,系统调用可以看成是一个内核与用户空间程序交互的接口——它好比是一个中间人,把用户进程的请求传达给内核,待内核把请求处理完后再将处理结果送回给用户空间。

1.2 系统调用的实现

系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。运行在内核态的进程可以毫无限制的访问各种资源,而在用户态的用户进程的各种操作都有着权限,比如不能随意的访问内存、不能开闭中断以及切换运行特权。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?

答案是软件中断。软件中断和我们常说的中断(硬件中断)不同之处在于,它是通过软件指令触发而非外设引发的中断,也就是说,又是编程人员开发出的一种异常(该异常为正常的异常)。操作系统一般是通过软件中断从用户态切换到内核态。

1.3 系统调用和库函数的区别

Linux下对文件操作方式有两种方法:系统调用(system call)库函数调用(Library functions)

库函数由两类函数组成:

1)不需要调用系统调用

不需要切换到内核空间即可完成函数全部功能,并且将结果反馈给应用程序,如strcpy、bzero 等字符串操作函数

2)需要调用系统调用

需要切换到内核空间,这类函数通过封装系统调用去实现相应功能,如printf、fread等

系统调用是需要时间的,程序中频繁的使用系统调用会降低程序的运行效率。当运行内核代码时,CPU工作在内核态,在系统调用发生前需要保存用户态和内存环境,然后转入内核态工作。系统调用结束后,又要切换回用户态。这种环境的切换会消耗许多时间。

1.4 C库中IO函数工作流程

库函数访问文件的时候需要根据需要,设置不同类型的缓冲区,从而减少了直接调用IO系统调用的次数,提高了访问效率

这个过程类似于快递员给某个区域(内核空间)送快递一样,快递员有两种方法送:

1)来一件快递马上送到目的地,来一件送一件,这样导致来回走比较频繁(系统调用)

2)等快递攒着差不多后(缓冲区),才一次性送到目的地(库函数调用)

2.错误处理函数

errno是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。

当Linux C api函数发生异常时,一般会将errno全局变量赋一个整数值,不同的值表示不同的意义,可以通过查看该值推测出出错原因。

测试代码:

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

int main(){
FILE* fp = fopen("txt", "r");
if (fp == NULL){
printf("%d\n", errno);//打印错误码
printf("%s\n", strerror(errno)); //把errno的数字转换成相应的文字
}
return 0;
}

查看错误号:

/usr/include/asm-generic/errno-base.h

/usr/include/asm-generic/errno.h

3.虚拟地址空间

每个进程都会分配虚拟地址,在32位机器上,该地址空间为4G

在进程里平时说的指针变量,保存的就是虚拟地址。当应用程序使用虚拟地址访问内存时,处理器(CPU)会将其转化成物理地址(MMU)。

MMU:将虚拟的地址转化为物理地址。

这样做的好处在于:

  • 进程隔离,更好的保护系统安全运行
  • 屏蔽物理差异带来的麻烦,方便操作系统和编译器安排进程地址

4.文件描述符

在Linux的世界里,一切设备皆为文件。我们可以调用系统中I/O的函数(I:input,输入;O:output,输出),对文件进行相应的操作(open()、close()、write()、read()等)。

打开现存文件或新建文件时,系统(内核)会返回一个文件描述符,文件描述符用来指定已经打开的文件。这个文件描述符相当于这个文件已经打开文件的符号,文件描述符是非负整数,是文件的标识,操作这个文件描述符相当于操作这个描述符所指定的文件。

程序运行起来后(每个进程)都有一张文件描述符的表,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符0、1、2记录在表中。程序运行起来后这三个文件描述符是默认打开的。

1
2
3
#define STDIN_FILENO 0 //标准输入的文件描述符
#define STDOUT_FILENO 1 //标准输出的文件描述符
#define STDERR_FILENO 2 //标准错误的文件描述

在程序运行起来后打开其它文件时,系统会返回文件描述符表中最小可用的文件描述符,并将此文件描述符记录在表中

最大打开的文件个数

Linux中一个进程最多只能打开 NR_OPEN_DEFAULT (即1024)个文件,故当文件不再使用时应及时调用close()函数关闭文件。

  • 查看当前系统允许打开最大文件个数:

    cat/proc/sys/fs/file-max

  • 当前默认设置最大打开文件个数1024

    ulimit -a

  • 修改默认设置最大打开文件个数4096

    ulimit -n 4096

5.常用文件IO函数

5.1 open函数

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

int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);

功能:
打开文件,如果文件不存在则可以选择创建。
参数:
pathname:文件的路径及文件名
flags:打开文件的行为标志,必选项 O_RDONLY O_WRONLY O_RDWR
mode:这个参数,只有在文件不存在时有效,指新建文件时指定文件的权限
返回值:
成功:返回打开的文件描述符
失败:-1

flags详细说明

必选项:

取值 含义
O_RDONLY 以只读的方式打开
O_WRONLY 以只写的方式打开
O_RDWR 以可读、可写的方式打开

可选项,和必选项按位或起来

取值 含义
O_CREAT 文件不存在则创建文件,使用此选项时需要使用mode说明文件的权限
O_EXCL 如果同时指定了O_CREAT,则文件已经存在,则会出错
O_TRUNC 如果文件存在,则清空文件内容
O_APPEND 写文件时,数据添加到文件末尾
O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O

mode补充说明

1)文件最终权限:mode & ~umask

2)shell进程的umask掩码可以用umask命令查看

  • umask:查看掩码(补码)
  • umask mode:设置掩码,mode为八进制数
  • umask -S:查看各组用户的默认操作权限
取值 八进制 含义
S_IRWXU 00700 文件拿使用者的读、写、可执行权限
S_IRUSR 00400 文件所有者的读权限
S_IWUSR 00200 文件所有者的写权限
S_IXUSR 00100 文件所有者的可执行权限
S_IRWXG 00070 文件所有这同组用户的读、写、可执行权限
S_IRGRP 00040 文件所有者同用户组的读权限
S_IWGRP 00020 文件所有者同组用户的写权限
S_IXGRP 00010 文件所有者同组的可执行权限
S_IRWXO 00007 其它用户的读、写、可执行权限
S_IROTH 00004 其它用户的读权限
S_IWOTH 00002 其它用户的写权限
S_IXOTH 00001 其它用户的可执行权限

5.2 close函数

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

int close(int fd);
功能:
关闭已打开的文件
参数:
fd:文件描述符,open()的返回值
返回值:
成功:0
失败:-1,并设置errno

需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所有即使用用户程序不调用close,在终止时内核也会自动关闭它打开的文件。

但对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,负责随着打开的文件越多,会占用大量文件描述符和系统资源

5.3 write函数

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
size_t write(int fd, const void* buf, size_t count);
功能:
把指定数目的数据写到文件(fd)
参数:
fd:文件描述符
buf:数据首地址
count:写入数据的长度(字节)
返回值:
成功:实际写入数据的字节个数
失败:-1

5.4 read函数

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
size_t read(int fd, void* buf, size_t count);
功能:
把指定数目的数据到内存(缓冲区)
参数:
fd:文件描述符
buf:内存首地址
count:读取的字节个数
返回值:
成功:实际读取的字节个数
失败:-1
阻塞和非阻塞的概念

读常规文件是不会阻塞,不管读多少字节,read一定会在有限的事件内返回。

从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多少事件也是不确定的,如果一直没有数据到达就一直阻塞在那里。

同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

【注意】阻塞与非阻塞是对于文件而言的,而不是指read、write等的属性。

5.5 lseek函数

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

off_t lseek(int fd, off_t offset, int whence);
功能:
改变文件的偏移量
参数:
fd:文件描述符
offset:根据whence来移动的位移量(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果移动的字节数超过了文件末尾,再次写入时将增大文件尺寸。

whence:其取值如下:
SEEK_SET: 从文件开头移动offset个字节数
SEEK_CUR: 从当前位置移动offset个字节数
SEEK_END: 从文件末尾移动offset个字节数
返回值:
若lseek成功执行,则返回新的偏移量
如果失败,返回-1

四、文件操作相关函数

1.stat函数

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

int stat(const char* path, struct stat* buf);
int lstat(const char* pathname, struct stat* buf);
功能:
获取文件状态信息
stat和lstat的区别:
当文件是一个符号链接时,lstat返回的是该符号链接本身的信息;
而stat返回的是该链接指向的文件的信息。
参数:
path:文件名
buf:保存文件信息的结构体
返回值:
成功:0
失败:-1

struct stat结构体说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存储的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid;//组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件字节数(文件大小)
blksize_t st_blksize; //块大小(文件系统I/O缓冲区大小)
blkcnt_t st_blocks; //块数

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 */

#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};

1.1 stat获得文件类型(第一个版本)

之前说stat中有一个获取文件类型和存储权限中的st_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
40
41
42
43
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int show_file_type(struct stat* s){
switch(s->st_mode & S_IFMT){
case S_IFREG:
printf("普通文件\n");
break;
case S_IFDIR:
printf("目录\n");
break;
case S_IFCHR:
printf("字符设备\n");
break;
case S_IFBLK:
printf("块文件\n");
break;
case S_IFSOCK:
printf("套接字\n");
break;
case S_IFIFO:
printf("管道\n");
break;
case S_IFLNK:
printf("符号链接\n");
break;
default:
printf("未知的文件类型\n");
}
return 0;
}

int main(int argc, char** argv){
int ret = -1;
struct stat s;

//容错判断
if (argc != 2){
printf("usage: ./a.out filename\n");

上面的很多内容都可以在官方手册上看到

比如

1
s->st_mode & S_IFMT

是官方手册上规定的写法

运行时只需要执行

1
./a.out filename

这里采用的是默认的编译

如果你使用了自定义文件名的话,只需要把 a.out 改成你自定义的文件名就可以了。

1.2 第二种写法

在官方手册上还提供了第二种写法,比第一种要简单

直接写代码

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

int show_file_type(struct stat* s){
if (S_ISREG(s->st_mode)){
printf("普通文件\n");
}
else if(S_ISDIR(s->st_mode)){
printf("目录\n");
}
else if (S_ISCHR(s->st_mode)){
printf("字符设备\n");
}
else if(S_ISBLK(s->st_mode)){
printf("块文件\n");
}
else if(S_ISSOCK(s->st_mode)){
printf("套接字\n");
}
else if(S_ISFIFO(s->st_mode)){
printf("管道\n");
}
else if(S_ISLNK(s->st_mode)){
printf("符号链接\n");
}
else{
printf("未知文件类型\n");
}
return 0;
}

int main(int argc, char** argv){
int ret = -1;
struct stat s;

//容错判断
if (argc != 2){
printf("./a.out filename\n");
return 1;
}

ret = stat(argv[1], &s);
if (ret == -1){
perror("stat");
return 1;
}
show_file_type(&s);
return 0;
}

1.3 获得文件权限

在stat中st_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
40
41
42
43
44
45
46
47
 1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <unistd.h>
7
8 int show_file_type(struct stat* s){
9 if (s->st_mode & S_IRUSR){
10 printf("r");
11 }
12 else{
13 printf("-");
14 }
15 if (s->st_mode & S_IWUSR){
16 printf("w");
17 }
18 else{
19 printf("-");
20 }
21 if (s->st_mode & S_IXUSR){
22 printf("x");
23 }
24 else{
25 printf("-");
26 }
27 printf("\n");
28 return 0;
29 }
30
31 int main(int argc, char** argv){
32 int ret = -1;
33 struct stat s;
34
35 if(argc != 2){
36 printf("./file_3 filename\n");
37 return 0;
38 }
39
40 ret = stat(argv[1], &s);
41 if (ret == -1){
42 perror("stat");
43 return 1;
44 }
45 show_file_type(&s);
46 return 0;
47 }

2.access函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>
int access(const char* pathname, int mode);
功能:测试指定文件是否具有某种属性
参数:
pathname:文件名
mode:文件权限,4种权限
R_OK: 是否有读写权限
W_OK: 是否有写权限
X_OK: 是否有执行权限
F_OK: 测试文件是否存在
返回值:
0:有某种权限,或文件存在
-1:没有,或文件不存在

3.chmod函数

1
2
3
4
5
6
7
8
9
#include <sys/stat.h>
int chmod(const char* pathname, mode_t mode);
功能:修改文件权限
参数:
filename:文件名
mode:权限(8进制数)
返回值:
成功:0
失败:-1

4.chown函数

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
int chown(const char* pathname, uid_t owner, git_t group);
功能:
修改文件所有者和所属组
参数:
pathname:文件或目录名
owner:文件所有者id,通过查看 /etc/passwd 得到所有者id
group:文件所属组id,通过 /etc/group 得到用户组id
返回值:
成功:0
失败:-1

5.truncate函数

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>
#include <sys/types.h>
int truncate(const char* path, off_t length);
功能:修改文件大小
参数:
path:文件名字
length:指定的文件大小
a)比用来小,删除后边的部分
b)比原来大,向后拓展
返回值:
成功:0
失败:-1

6.link函数

1
2
3
4
5
6
7
8
9
#include <unistd.h>
int link(const char* oldpath, const char* newpath);
功能:创建一个硬链接
参数:
oldpath:源文件名字
newpath:硬链接名字
返回值:
成功:0
失败:-1

7.symlink函数

1
2
3
4
5
6
7
8
9
#include <unistd.h>
int symlink(const char* target, const char* linkpath);
功能:创建一个软链接
参数:
target:源文件名字
linkpath:软链接名字
返回值:
成功:0
失败:-1

8.readlink函数

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
ssize_t readlink(const char* pathname, char* buf, size_t bufsize);
功能:读软连接对应的文件名,不是读内容(该函数只能读软链接文件)
参数:
pathname:软连接名
buf:存放软链接对呀的文件名
bufsize:缓冲区大小(第二个参数存放的最大字节数)
返回值:
成功:>0,读到buf中的字符个数
失败:-1

9.unlink函数

1
2
3
4
5
6
7
8
#include <unistd.h>
int unlink(const char* pathname);
功能:删除一个文件(软硬链接文件)
参数:
pathname:删除的文件名字
返回值:
成功:0
失败:-1

10.rename函数

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int rename(const char* oldpath, const char* newpath);
功能:把oldpath的文件名改成newpath
参数:
oldpath:旧文件名
newpath:新文件名
返回值:
成功:0
失败:-1

五、文件描述符复制

1.概述

dup() 和dup2() 是两个非常有用的系统调用,都是用来复制一个文件的描述符,使新的文件描述符也标识旧的文件描述符所标识的文件。

对于dup() dup2() 也一样,通过原来的文件描述符复制出一个新的文件描述符,这样的话,原来的我加你描述符和新的文件描述符都指向同一个文件,我们操作这两个文件描述符的任何一个,都能操作它所对应的文件。

2.dup函数

1
2
3
4
5
6
7
8
9
#include <unistd.h>
int dup(int oldfd);
功能:
通过 oldfd 复制出一个新的文件描述符,新的文件描述符表中最小可用的文件描述符,最终 oldfd 和新的文件描述符都指向同一个文件
参数:
oldfd:需要复制的文件描述符 oldfd
返回值:
成功:新文件描述符
失败:-1

3.dup2函数

1
2
3
4
5
6
7
8
9
#include <unistd.h>
int dup2(int oldfd, int newfd);
功能:
通过 oldfd 复制出一个新的文件描述符 newfd,如果成功,newfd 和函数返回值是同一个返回值,最终 oldfd 和新的文件描述符 newfd 都指向同一个文件。
oldfd:需要复制的文件描述符
newfd:新的文件描述符,这个描述符可以人为指定一个合法数字(0-1023),如果指定的数字已经被占用(和某个文件有关联),此函数会自动关闭close()断开这个数字和某个文件的关联,再来使用这个合法数字。
返回值:
成功:返回 newfd
失败:-1

4.案例分析

5.fcntl函数

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
功能:改变已打开的文件性质,fcntl针对描述符提供控制。
参数:
fd:操作的文件操作符
cmd:操作方式
arg:针对cmd的值,fcntl能够接受第三个参数int arg
返回值:
成功:返回某个其他值
失败:-1

fcntl函数有5种功能:

1)复制一个现有的描述符(cmd = F_DUPFD)

2)获得 / 设置文件描述符标记(cmd=F_GETFD或F_SETFD)

3)获得 / 设置文件状态标记(cmd=F_GETFL或F_SETFL)

4)获得 / 设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)

5)获得 / 设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

示例

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

//fcntl复制文件描述符功能
int main(void){
int fd = -1;
int newfd = -1;
int ret = -1;
//打开文件
fd = open("txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1){
perror("open");
return 1;
}
//文件描述符
//第三个参数0 表示返回第一个最小的可用的文件描述符,并且大于或等于0
newfd = fcntl(fd, F_DUPFD, 0);
if (newfd == -1){
perror("fcntl");
return 1;
}
//写文件
write(fd, "123456789", 9);
write(newfd, "ABCDEFG", 7);
//关闭文件
close(fd);
close(newfd);
}

5.1 获得文件状态表示和设置

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

//fcntl复制文件描述符功能
int main(void){
int fd = -1;
int newfd = -1;
int ret = -1;
//打开文件
fd = open("txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1){
perror("open");
return 1;
}

//获取文件状态标记
ret = fcntl(fd, F_GETFL);
if (ret == -1){
perror("fcntl");
return 1;
}

if (ret & O_APPEND){
printf("before append\n");
}
else{
printf("before not append\n");
}

//设置文件状态标记
ret = ret | O_APPEND;
ret = fcntl(fd, F_SETFL, ret);
if (ret == -1){
perror("fcntl");
return 1;
}

//获取文件状态标记
ret = fcntl(fd, F_GETFL);
if (ret == -1){
perror("fcntl");
return 1;
}

if (ret & O_APPEND){
printf("before append\n");
}
else{
printf("before not append\n");
}

//关闭文件
close(fd);
}

6.目录习惯操作

6.1 getcwd函数

1
2
3
4
5
6
7
8
9
#include <unistd.h>
char* getcwd(char* buf, sie_t size);
功能:获取当前进程的工作目录
参数:
buf:缓冲区,存储当前的工作目录
size:缓冲区大小
返回值:
成功:buf中保存当前进程工作目录位置
失败:NULL

6.2 chdir函数

1
2
3
4
5
6
7
8
#include <unistd.h>
int chdir(const char* path);
功能:修改当前进程(应用程序)的路径
参数:
path:切换的路径
返回值:
成功:0
失败:-1

6.3 opendir函数

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <dirent.h>
DIR* opendir(const char* name);
功能:打开一个目录
参数:
name:目录名
返回值:
成功:返回指向该目录结构体指针
失败:NULL

6.4 closedir函数

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR* dirp);
功能:关闭目录
参数:
dirp:opendir返回的指针
返回值:
成功:0
失败:-1

6.5 readdir函数

1
2
3
4
5
6
7
8
#include <dirent.h>
struct dirent* readdir(DIR* dirp);
功能:读取目录
参数:
dirp:opendir的返回值
返回值:
成功:目录结构体指针
失败:NULL

相关结构体说明:

1
2
3
4
5
6
7
struct dirent{
ino_t d_ino; //此目录进入点的inode
off_t d_off; //目录文件开头至目录进入点的位移
signed short int d_reclen; //d_name的长度,不包含NULL字符
unsigned char d_type; //d_type所指的文件类型
char d_name[256]; //文件名
};

d_type文件类型说明

取值 说明
DT_BLK 块设备
DT_CHR 字符设备
DT_DIR 目录
DT_LNK 软链接
DT_FIFO 管道
DT_REG 普通文件
DT_SOCK 套接字
DT_UNKNOWN 未知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>

//目录打开和关闭
int main(){
DIR* dir = NULL;
struct dirent* d = NULL;
//打开目录
dir = opendir("/home/pi/student/student03");
if (dir == NULL){
perror("dir");
return 1;
}
printf("打开目录成功...\n");
//循读取目录中的内容
while(1){
d = readdir(dir);
if (d == NULL){
break;
}
printf("file_type:%hu\tfile_name:%s\n", d->d_type, d->d_name);
}
//关闭目录
closedir(dir);
return 0;
}

六、进程

1.进程和程序

程序是存放在存储介质上的一个可执行文件,而进程是程序执行的过程。进程的状态是变化的,其包括进程的创建、调度和消亡。程序是静态的,进程是动态的。

在linux系统中,操作系统是通过进程去完成一个一个的任务,进程是管理事务的基本单元。

进程拥有自己独立的处理环境(如:当前需要用到哪些环境变量,程序运行的目录在哪是,当前是哪个用户在运行此程序等)和系统资源(如:处理器CPU占用率、存储器、I/O设备、数据、程序)。

2.单道、多道程序设计

2.1 单道程序设计

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

2.2 多道程序设计

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

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

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

1s = 1000ms

1ms = 1000um

1um = 1000ns

1s = 1000000000ns

3.并行和并发

**并行(parallel):**指同一时刻,有多条指令在多个处理器上同时执行

**并发(concurrency):**指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使得进程快速交替的执行

4.MMU

MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中原来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。

5.进程控制块PCB

进程运行时,内核为进程每个进程分配一个PCB(进程控制块),维护相关信息,Linux内核的进程控制块是task_struct结构体。

其内部成员有很多,掌握以下部分即可

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

6.进程的状态

进程状态反应进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。

在三态模型中,进程状态分为三个基本状态,即运行态、就绪态、阻塞态。

在五态模型中,进程分为新建态、终止态、运行态、就绪态、阻塞态。

查看进程的方法

1
ps aux

6.1 ps

进程是一个具有一定独立功能的程序,它是操作系统动态执行的基本单元。

ps命令可以查看进程的详细情况,常用选项(选项可以不加”-“)如下:

选项 含义
-a 显示终端上的所有进程,包括其它用户的进程
-u 显示进程的详细状态
-x 显示没有控制终端的进程
-w 显示加宽,以便显示更多的信息
-r 只显示正在运行的进程

6.2 top

top命令用来动态显示运行中的进程。top命令能够在运行后,在指定的时间间隔更新显示信息。可以在top命令时加上-d来指定显示信息更新的时间间隔。

在top命令执行后,可以按下按键得到对显示的结果进行排序:

按键 含义
M 根据内存使用量来排序
P 根据CPU占有率来排序
T 根据进程运行时间的长短来排序
U 可以根据后面输入的用户名筛选进程
K 可以根据后面输入的PID来杀死进程
q 退出
h 获得帮助

6.3 kill

kill命令指定进程号的进程,需要配合ps使用

使用格式:

1
kill [-signal] pid

信号值从0到15,其中9为绝对终止,可以处理一般信号无法终止的进程。

有些进程没办法直接杀死,需要添加 -9 参数强制杀死

6.4 killall

通过名字杀死进程

7.进程号和相关函数

每个进程都由一个进程号标识,其类型为pid_t(整形),进程号的范围:0~32767.进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用。

进程号(PID):

标识进程的一个非负整形数

父进程号(PPID):

任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。如,A进程创建了B进程,A的进程号就是B进程的父进程号。

进程组号(PGID):

进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。这个过程有点类似于QQ群,各个进程相当于各个好友,把各个好友都拉入这个QQ群里,主要是方便管理,特别是通知某些事时,只要在群里吼一声,所有人都收到,简单粗暴。但是,这个进程组号和QQ群号是有区别的,默认的情况下,当前的进程号会当作当前的进程号。

7.1 getpid函数

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:
获取本进程号(PID)
参数:

返回值:
本进程号

7.2 getppid函数

1
2
3
4
5
6
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
功能:获取调用此函数的进程的父进程号
参数:无
返回值:调用此函数的进程的父进程号(PPID)

7.3 getpgid函数

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

pid_t getpgid(pid_t pid);
功能:
获取进程组号(PGID)
参数:
查询进程的进程号
返回值:
进程组号(PGID)

7.4 示例

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>

//获取进程号 父进程号 进程组号
int main(){
pid_t pid = -1;
//获取当前进程的进程号
pid = getpid();
printf("进程号:%d\n", pid);

//获得当前进程的父进程号
pid = getppid();
printf("父进程号:%d\n", pid);

//获得当前进程组号
pid = getpgid(getpid());
printf("进程组号:%d\n", pid);
return 0;
}

8.进程的创建

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程呢个,形成进程树结构模型。

1
2
3
4
5
6
7
8
9
10
11
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
功能:用于从一个已经存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:无
返回值:
成功:子进程中返回0,父进程中返回子进程ID。pid_t为整形
失败:返回-1
失败的两个原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。

举例

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

int main(){
fork();
printf("hello,word\n");
return 0;
}

输出结果:

1
2
hello,word
hello,word

9.父子进程关系

使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间:包括进程上下文(进程执行活动全过程的静态描述)、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。

子进程所独有的只有它的进程号,计时器等(只有小量信息)。因此,使用fork()函数的代价是很大的。

简单来说,一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

实际上,更准确来说,Linux的fork()使用是通过写时拷贝(copy-on-write)实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只要在需要写入的社会才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。

注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件文件偏移指针。

10.区分父子进程

子进程是父进程的一个复制品,可以简单认为父子进程的代码一样。父进程做了什么事,子进程也做什么事,如果我们需要实现多任务的要求该如何实现呢?答案是通过fork()的返回值。

fork()函数被调用一次,但返回两次。两次返回的区别是:子进程的返回值是0,而父进程的返回值则是子进程的ID。

测试程序:

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

int main(){
int child_id;
child_id = fork();
if (child_id < 0){
printf("子进程创建失败\n");
}
else if (child_id > 0){
printf("id: %d pid: %d\n", getpid(), getppid());
}
else{
printf("id: %d child_id: %d\n", getpid(), child_id);
}
return 0;
}

11.父子进程地址空间

父进程的值是在单独的空间,子进程的值是在单独空间,两者修改不会互相影响。

如果是在堆区开辟的空间也不会相互受到影响,但是记住一定要释放一下开辟出来的空间,比如下面的代码

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

int main(){
int child_id;
int* num = malloc(4);
*num = 200;
child_id = fork();
if (child_id < 0){
printf("子进程创建失败\n");
}
else if (child_id == 0){
sleep(1);
printf("子进程:%d\n", *num);
free(num);
num = NULL;
}
else{
printf("父进程前:%d\n", *num);
(*num)++;
printf("父进程后:%d\n", *num);
free(num);
num = NULL;
}
return 0;
}

如果不两次释放会导致内存溢出

12.GDB调试多进程

使用GDB调试的时候,GDB只能跟踪一个进程。可以在fork函数调用之前,通过指令设置GDB调试工具跟踪父进程或者各种子进程。默认跟踪父进程。

  • set follow-fork-mode child 设置GDB在fork之后跟踪子进程。
  • set follow-fork-mode parent 设置跟踪父进程(默认)

注意,一定要在fork函数调用之前设置才有效

13.进程退出函数

相关函数

1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>
void exit(int status);

#include <unistd.h>
void _exit(int status);
功能:
结束调用此函数的进程。
参数:
status:返回值给父进程的参数(低8位有效),至于这个参数是多少根据需求来写。
返回值:

exit() 和 _exit() 函数功能和用法是一样的,无非是所包含的头文件不一样,exit() 属于标准库函数, _exit() 属于系统调用函数。

14.等待进程退出函数

14.1 概述

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要指进程控制块PCB的信息(包括进程号、退出状态、允许时间等)。

父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除这个进程。

wait() 和 waitpid() 函数的功能一样,区别在于, wait() 函数会阻塞, waitpid() 可以设置不阻塞, waitpid() 还可以指定等待哪个子进程结束。

注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

14.2 wait函数

函数说明:

1
2
3
4
5
6
7
8
9
10
11
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);
功能:
等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:
status:进程结束时的状态信息
返回值:
成功:已经结束子进程的进程号
失败:-1

调用 wait() 函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号才被唤醒(相当于继续往下执行)。

若调用进程没有子进程,该函数立即返回;若它的子进程已经结束,该函数同样会立即返回,并且会回收那个早已结束进程的资源。

所以,wait()函数的主要功能为回收已经结束子进程的资源。

如果参数status的值不是NULL,wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。

这个退出信息在一个int中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。

宏函数可以分为如下三组:

1)WIFEXITED(status)

​ 为非0 进程正常结束

WEXITSTATUS(status)

​ 如果宏为真,使用此宏 获取进程退出状态(exit的参数)

2)WIFSIGNALED(status)

​ 为非0 进程异常终止

WTERMSIG(status)

​ 如上宏为真,使用此宏 取得进程终止的那个信号的编号

3)WIFSTOPPED(status)

​ 为非0 进程处于暂停状态

WSTOPSIG(status)

​ 如上宏为真,使用此宏 取得使进程暂停的那个信号的编号

WIFCONTINUED(status)

​ 为真 进程暂停后已经继续运行

14.3 waitpid函数

函数说明:

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

pid_d waitpid(pid_t pid, int* status, int options);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数:
pid:参数 pid 的值有以下几种类型:
pid > 0 等待进程 ID 等于 pid 的子进程
pid = 0 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
pid = -1 等待任意子进程,此时 waitpid 和 wait 作用一样。
pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。

status:进程退出时的状态信息。和 wait() 用法一样。

options:options 提供了一些额外的选项来控制 waitpid()。
0:同 wait(),阻塞父进程,等待子进程退出。
WNOHANG:没有任何已经结束的子进程,则立刻返回
WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予理会子进程的结束状态。(由于涉及到一些调试方面的知识,加之极少用到)
返回值:
waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:
1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0
3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;

15.孤儿进程

父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程(Orphan Process)。

每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。

因此孤儿进程并不会有什么危害。

16.僵尸进程

进程终止,父进程尚未回收子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

这样就会导致一个问题,如果进程不调用wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有进程号而导致系统不能产生新的进程,这就是僵尸进程的危害,应当避免。

17.进程替换

概述

在Windows平台下,我们可以通过双击运行可执行程序成为一个进程;而在Linux平台,我们可以通过 ./ 运行,让一个可执行程序成为一个进程。

但是,如果我们本来就运行着一个程序(进程),我们如何在这个进程内部启动一个外部程序,由内核将这个程序读入内存,使其执行起来成为一个进程呢?这里我们通过 exec 函数族实现。

exec 函数族,顾名思义,就是一簇函数,在Linux中,并不存在 exec() 函数,exec 指的是一组函数,一共有6个:

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
extern char** environ;

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

int execve(const char* filename, char* const argv[], char* const envp[]);

其中只有 execve() 是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

exec函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。

进程调用一种exec函数时,该进程完全由新程序交替,而新程序则从其main函数开始执行。因为调用exec并吧创建新进程,所以前后的进程 ID (当然还有父进程号、进程组号、当前工作目录……)并未改变。exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段(进程替换)。

示例:

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

int main(){
printf("hello\n");
execlp("ls", "ls", "-l", "/home", NULL);
printf("hello\n");
return 0;
}

七、进程间通讯

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。

但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信(IPC:Inter Processes Communication)。

进程间通讯的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

Linux 操作系统支持的主要进程间通信的通信机制:

1.无名管道

1.1 概述

管道也叫无名管道,它是UNIX系统IPC(进程间通信)的最古老形式,所有的UNIX系统都支持这种通信机制。

管道有如下特点:

1)半双工,数据在同一时刻只能在一个方向上流动。

2)数据只能从管道的一端写入,从另一端读出。

3)写入管道中的数据遵循先入后出的规则。

4)管道所传送的数据是无格式的,这要求管道的读出方必须事先约定好数据的格式,如多少字节算一个消息等。

5)管道不是普通的文件,不属于某个文件系统,其只存放于内存中。

6)管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。

7)从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。

8)管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

对于管道特点的理解,我们可以类比实现生活中管子,管子的一端塞东西,管子的另一端取东西。

管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。

1.2 pipe函数

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

int pipe(int pipefd[2]);
功能:创建无名管道。

参数:
pipefd:为 int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。

当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O的函数都可以用来操作管道(lseek() 除外)。

返回值:
成功:0
失败:-1

例子:父子进程通过无名管道进行通讯

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

#define SIZE 64

//父子进程通讯
int main(){
char read_string[SIZE];
int pd[2];
int ret = -1, child_id = -1;
int read_bool = -1, write_bool = -1;
//创建无名管道
ret = pipe(pd);
if(ret == -1){
perror("ret");
return 1;
}

//创建子进程
child_id = fork();
if (child_id == -1){
perror("fork");
return 1;
}

//子进程 读内容
if (child_id == 0){
//禁用写端
close(pd[1]);

//读取管道中的内容
read_bool = read(pd[0], read_string, SIZE);
if (read_bool < 0){
perror("read");
return 1;
}

printf("child read: %s\n", read_string);
//禁用读端
close(pd[0]);
exit(0);
}

//父进程 写内容
//关闭读管道
close(pd[0]);

write_bool = write(pd[1], "ABCDEFGHIJK", SIZE);
if(write_bool < 0){
perror("write");
}

printf("parent process write len: %d\n", write_bool);

//关闭写管道
close(pd[1]);
return 0;
}

需要注意一下,创建匿名管道的时候需要在创建进程前创建,如果按照下面的代码创建匿名管道:

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

int main(){
int fd[2], ret, i;
char buf[20];
pid_t pid;
pid = fork();
ret = pipe(fd);
if (ret == -1){
perror("pipe");
return -1;
}
if (pid == -1){
perror("fork");
return -2;
}
else if (pid == 0){
close(fd[1]);
ret = read(fd[0], buf, 20);
printf("%d %s\n", ret, buf);
close(fd[0]);
}
else {
close(fd[0]);
ret = write(fd[1], "abcd", 4);
if (ret == -1){
perror("write");
return -3;
}
close(fd[1]);
}
return 0;
}

这样会导致子进程读不出父进程向管道传入的数据,原因是因为在创建好子进程后,子进程后父进程分别创建了一个管道,子进程使用的管道是子进程自己创建的管道,而父进程使用的管道是父进程创建的管道,父进程向管道中传入内容,其实是向它自己创建的管道中传入内容,而子进程读取管道中的内容是读取自己管道中的内容,所以子进程读不出来内容。

1.3 管道的读写特点

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

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

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

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

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

总结:

读管道:

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

写管道:

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

1.4 设置为非阻塞的方法

设置方法:

1
2
3
4
5
6
//获取原来的flags
int flage = fcntl(fd[0], F_GETFL);
//设置新的flags
flag |= O_NONBLOCK;
//flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);

结论:如果写端没有关闭,读端设置为非阻塞,直接返回-1。

1.5 查看管道缓冲区命令

可以使用ulimit-a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小

1.6 查看管道缓冲区函数

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

long fpathconf(int fd, int name);
功能:该函数可以通过name参数查看不同的属性值
参数:
fd:文件描述符
name:
_PC_PIPE_BUF,查看管道缓冲区大小
_PC_NAME_MAX,文件名字字节数的上限
返回值:
成功:根据name返回的值的意义也不同。
失败:-1

示例

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

int main(){
int pd[2], ret = -1;
//创建无名管道
ret = pipe(pd);
if (ret == -1){
perror("pipe");
}
//查看管道缓冲区函数
printf("%ld\n", fpathconf(pd[0], _PC_PIPE_BUF));
printf("%ld\n", fpathconf(pd[0], _PC_NAME_MAX));
return 0;
}

2.有名管道

2.1 概述

无名管道由于没有名字,只能用于亲缘关系的进程间通讯。为了克服这个缺点,提出了明名管道(FIFO),也叫有民管道、FIFO文件。

命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通讯,因此,通过FIFO不相关的进程也能交换数据。

命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:

1)FIFO在文件系统中作为一个特殊的文件而存在,但FIFO中的内容却存放在内存中。

2)当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。

3)FIFO有名字,不相关的进程可以通过打开命名管道进行通讯。

2.2 通过命令创建有名管道

2.3 通过函数创建有名管道

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char* pathname, mode_t mode);
功能:
命名管道的创建。
参数:
pathname : 普通的路径名,也就是创建后 FIFO 的名字。
mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。(0666
返回值:
成功: 0 状态码
失败:如果文件已经存在,则会出错且返回-1

2.4 有名管道读写操作

一旦使用mkfifo创建一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。

FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始到处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

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
/*write.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#define SIZE 128

int main(){
int fp = -1, ret = -1, i = 14;
char buf[SIZE];
//打开有名管道
fp = open("fifo", O_WRONLY);
if (fp == -1){
perror("open");
return 1;
}

//写内容
while(1){
memset(buf, 0, SIZE);
sprintf(buf, "hello itcast:%d", i);
ret = write(fp, buf, strlen(buf));
if (ret <= 0 ){
perror("write");
break;
}
printf("write fifo : %d\n", ret);
sleep(1);
}

//关闭管道
close(fp);

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
/*read.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define SIZE 128

int main(){
int fd =-1, ret = -1;
char buf[SIZE];
//打开文件
fd = open("fifo", O_RDONLY);
if (fd == -1){
perror("open");
return 1;
}
printf("read....\n");

//读取文件
while(1){
ret = read(fd, buf, SIZE);
if(ret <= 0){
perror("read");
break;
}
printf("%s\n", buf);
}

//关闭文件
close(fd);
return 0;
}

2.5 有名管道注意事项

1)一个为只读而打开一个管道的进程会阻塞直到另一个进程为只写打开该管道

2)一个为只写而打开一个管道的进程会阻塞直到另一个进程为只读打开管道

读管道:

  • 管道中有数据,read返回实际读到的字节数。
  • 管道中无数据:
    • 管道写端被全部关闭,read返回0(相当于读到文件结尾)
    • 写端没有全部被关闭,read阻塞等待

写管道:

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

3.消息队列

在上面说了一下管道,学习完管道后发现管道是一端发送什么,另一端就只能接收什么,而且只能传递一种类型,但在实际的项目中,有些时候需要选择性的接收另一端传递过来的数据,为了解决这个问题,Linux中出现了一个叫做消息队列的东西。

3.1 什么是消息队列

消息队列其实是一个在内核地址空间中的内部链表,每一个链表的内容是一个数据块,数据块中能存放的内容有很多的类型,这些可以自己去定义,而且还能选择性的进行接收数据。

3.2 通过命令查看消息队列

在Linux中可以使用

1
ipcs -q

来查看一下当前系统中的消息队列的个数和每个消息队列的大小

3.3 创建消息队列

可以使用msgget函数创建出一个消息队列,函数的原型如下:

1
2
3
4
5
6
7
8
9
10
#include <sys/msg.h>
int msgget(key_t key, int msgfig);
/*
功能:创建接口队列
参数:
key:消息队列中的键值,通常使用ftok()函数进行创建
msgfig:权限标志
IPC_CREATE:如果key值所对应的消息队列不存在,那么就创建一个
IPC_EXCL:
*/

4.共享存储映射

4.1 概述

存储映射I/O(Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。

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

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式,因为进程可以直接读写内存,而不需要任何数据的拷贝。

4.2 存储映射函数

(1)mmap函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sys/mman.h>

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:
一个文件或者其它对象映射进内存
参数:
addr : 指定映射的起始地址,通常设为NULL,由系统指定
length : 映射到内存的文件长度
prot : 映射区的保护方式,最常用的:
a) 读:PROT_READ
b) 写:PROT_WRITE
c) 读写:PROT_READ | PROT_WRITE
flags: 映射区的特性,可以是
a) MAP_SHARED : 写入映射区的数据会复制回文件,且允许其它映射该文件的进程共享。
b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write),对此区域所做的修改不会写回原文件。
fd: 由open返回的文件描述符,代表要映射的文件。
offset: 以文件开始处的偏移量,必须是4k的整数倍,通常为0,表示从文件头开始映射
返回值:
成功:返回创建的映射区首地址
失败:MAP_FAILED宏

关于mmap函数的使用总结:

1)第一个参数写成NULL

2)第二个参数要映射的文件大小 >0

3)第三个参数:PROT_READ、PROT_WRITE

4)第四个参数:MAP_SHARED 或者 MAP_PRIVATE

5)第五个参数:打开的文件对应的文件描述符

6)第六个参数:4k的整数倍,通常为0

(2)munmap函数
1
2
3
4
5
6
7
8
9
10
11
#include <sys/mman.h>

int munmap(void* addr, size_t length);
功能:
释放内存映射区
参数:
addr: 使用mmap函数创建的映射区的首地址
length: 映射区的大小
返回值:
成功:0
失败:-1

4.3 注意事项

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

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

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

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

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

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

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

4.4 共享映射的方式操作文件

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

int main(){
int fd = -1;
void* addr = NULL;
int ret = -1;

//打开文件
fd = open("txt", O_RDWR);
if (fd == -1){
perror("open");
return 1;
}
printf("读取文件成功...\n");

//创建映射
addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED){
perror("mmap");
return 1;
}

printf("创建映射成功\n");

//关闭文件描述符
close(fd);
printf("关闭文件描述符成功\n");

//向映射区写内容
memcpy(addr, "123456789", 9);

//释放内存区
ret = munmap(addr, 1024);
if (ret == -1){
perror("munmap");
return 1;
}
printf("释放内存成功\n");

return 0;
}

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
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 <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>

int main(){
int fd = -1;
void* mp = NULL;
pid_t child_id = -1;
//打开文件
fd = open("txt", O_RDWR);
if (fd == -1){
perror("open");
return 1;
}

//创建映射
mp = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mp == MAP_FAILED){
perror("mmap");
return 1;
}

//关闭文件描述符
close(fd);

//创建进程
child_id = fork();
if (child_id == -1){
perror("fork");
return 1;
}
else if(child_id == 0){
//子进程
memcpy(mp, "abcdefg", 7);
}
else{
wait(NULL);
printf("mp:%s\n", (char*)mp);
}

//释放内存映射
munmap(mp, 1024);

return 0;
}

4.6 不同进程使用存储映射进行进程间通讯

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

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>

int main(){
int fd = -1;
void* addr = NULL;
int ret = -1;
//打开文件
fd = open("txt", O_RDWR);
if (fd == -1){
perror("open");
return 1;
}
printf("打开文件成功\n");

//添加映射
addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED){
perror("mmap");
return 1;
}
printf("内存映射添加成功\n");

//关闭文件描述符
close(fd);
//写文件
memcpy(addr, "qwertyuiop", 10);

//释放映射内存
ret = munmap(addr, 1024);
if (ret == -1){
perror("munmap");
return 1;
}
printf("释放映射内存成功\n");
}

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

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>

int main(){
int fd = -1;
void* addr = NULL;
int ret = -1;
//打开文件
fd = open("txt", O_RDWR);
if (fd == -1){
perror("open");
return 1;
}
printf("打开文件成功\n");

//添加映射
addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED){
perror("mmap");
return 1;
}
printf("内存映射添加成功\n");

//关闭文件描述符
close(fd);
//读文件
printf("%s\n", (void*)addr);

//释放映射内存
ret = munmap(addr, 1024);
if (ret == -1){
perror("munmap");
return 1;
}
printf("释放映射内存成功\n");
}

4.7 匿名映射实现父子进程通讯

通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程通讯间也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。

通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。可以直接使用匿名映射来代替。

其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定

使用MAO_ANONYMOUS(或MAP_ANON)

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

  • 4“随意举例,该位置表示映射区大小,可依实际需要填写”。
  • MAP_ANONYMOUS 和 MAP_ANON 这两个宏是Linux操作系统特有的宏。推荐使用 MAP_ANONYMOUS

程序实例:

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

int main(){
int pd = -1;
void* arrd = NULL;
arrd = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (arrd == MAP_FAILED){
perror("mmap");
return 1;
}
//创建进程
pd = fork();
if (pd == -1){
perror("fork");
return 1;
}
//向文件中写内容
if (pd == 0){
memcpy(arrd, "hello,word", 10);
}
//读文件中的内容
else{
printf("arrd:%s\n", (char*)arrd);
wait(NULL);
}
//关闭映射
munmap(arrd, 4096);
return 0;
}

八、信号

1.信号的概述

信号是Linux进程通讯的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

信号的特点

  • 简单
  • 不能携带大量信息
  • 满足某个特设条件才发送

信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。

一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:

注意:这里信号的产生,注册,注销时信号的内部机制,而不是信号的函数实现。

2.信号的编号

1)信号编号:

Unix早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley 和 AT&T都对信号模型做了修改,增加了可靠信号机制。但彼此不兼容。POSIX.1对可靠信号例程进行了标准化。

Linux 可使用命令:kill -l,查看相应的信号

不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。

2)Linux常规信号一览表

编号 信号 对应事件 默认动作
1 SIGHUP 用户推出shell时,由该shell启动的所有进程将收到这个信号 终止进程
2 SIGINT 当用户按下了**<Ctrl+C>**组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 终止进程
3 SIGQUIT 用户按下了**<ctrl+\>**组合键时产生了该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 终止进程
4 SIGILL CPU检测到某进程执行了非法指令 终止进程并产生core文件
5 SIGTRAP 该信号由断点指令或其他trap指令产生 终止进程并产生core文件
6 SIGABRT 调用abort函数时产生该信号 终止进程并产生core文件
7 SIGBUS 非法访问内存地址,包括内存对齐出错 终止进程并产生core文件
8 SIGFPE 在发生致命的运算时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 终止进程并产生core文件
9 SIGKILL 无条件终止进程。本信号不能被忽略,处理和阻塞 终止进程,可以杀死任何进程
10 SIGUSE1 用户定义的信号。即程序员可以在程序中定义并使用该信号 终止进程
11 SIGSEGV 指示进程进行了无效内存访问(段错误) 终止进程并产生core文件
12 SIGUSR2 另外一个用户自定义信号,程序员可以在程序中定义并使用该型号 终止进程
13 SIGPIPE Broken pipe向一个没有读端的管道写数据 终止进程
14 SIGALRM 定时器超时,超时的时间由系统调用alarm设置 终止进程
15 SIGTERM 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令kill时,缺省产生这个信号 终止进程
16 SIGSTKFLT Linux早期版本出现的信号,现仍保留向后兼容 终止进程
17 SIGCHLD 子进程结束时,父进程会收到这个信号 忽略这个信号
18 SIGCONT 如果进程已停止,则使其继续运行 继续/忽略
19 SIGSTOP 停止进程的执行。信号不能被忽略,处理和阻塞 为终止进程
20 SIGTSTP 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 暂停进程
21 SIGTTIN 后台进程读终端控制台 暂停进程
22 SIGTTOU 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 暂停进程
23 SIGURG 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 忽略该信号
24 SIGXCPU 进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程 终止进程
25 SIGXFSZ 超过文件的最大长度设置 终止进程
26 SIGVTLRM 虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用s’j 终止进程
27 SGIPROF 类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间 终止进程
28 SIGWINCH 窗口变化大小时发出 忽略该信号
29 SIGIO 此信号向进程指示发出了一个异步IO事件 忽略该信号
30 SIGPWR 关机 终止进程
31 SIGSYS 无效的系统调用 终止进程并产生core文件
34~64 SIGRTMIN~ Linux的实时信号,它们没有固定的含义(可以自定义) 终止进程

3.信号四要素

每个信号必备4要素,分别是:

1)编号 2)名称 3)事件 4)默认处理动作

可通过 man 7 signal 查看帮助文档获取:

我这个是 arm 架构的,和教程上的不一样。

在标准信号中,有一些信号是有三个“Value”,第一个值通常对alpha和sparc架构有效,中间值针对x86、arm和其他框架,最后一个应用于mips架构。一个’-‘表示在对应架构上尚未定义该信号。

不同的操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内,也出现在Linux中,而有的信号出现在FreeBSD或Mac OS中却没有出现在Linux下。这里我们只研究Linux系统中的信号。

Action为默认动作:

  • Term:终止进程
  • lgn:忽略信号(默认即使对该种信号忽略操作)
  • Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
  • Stop:停止(暂停)进程
  • Cont:继续运行进程

注意通过 man 7 signal 命令查看帮助文档,其中可看到:The signals SIGKILL and SIGSTOP cannot be caught,blocked,or ignored。

这里特别强调了 SIGKILL 和 SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。

另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!!

4.信号的状态

1)产生

a)当用户按某些终端键时,将产生信号。

​ 终端上按“ctrl+c”组合键通常产生中断信号 SIGINT

​ 终端上按“ctrl+\”键通常产生中断信号 SIGQUIT

​ 终端上按“ctrl+z”键通常产生中断信号 SIGSTOP等。

b)硬件异常将产生信号。

除数为0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。

c)软件异常将产生信号。

当检测到某种软件条件已发生(如:定时器alarm),并将其通知有关进程时,产生信号。

d)调用系统函数(如:kill、raise、abort)将发送信号。

注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。

e)运行 kill / killall命令将发送信号。

此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。

2)未决状态:没有被处理

3)递达状态:信号被处理了

5.阻塞信号集和未决信号集

信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。

Linux内核的进程控制块PCB是一个结构体,task_struct,除了包含进程id、状态、工作目录、用户id、组id、文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

5.1 阻塞信号集(信号屏蔽字)

将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,该信号的处理将推后(处理发送在解除屏蔽后)。

5.2 未决信号集

信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位反转回为0.这一时刻往往非常短暂。

信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称为未决信号集。在屏蔽解除前,信号一直处于未决状态。

6.信号产生函数

6.1 kill函数

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

int kill(pid_t pid, int sig);
功能:给指定进程发送指定信息(不一定杀死)

参数:
pid : 取值有4种情况:
pid > 0:将信号传送给进程 ID 为pid的进程
pid = 0:将信号传送给当前进程所在进程组中的所有进程。
pid = -1:将信号传送给系统内所有的进程。
pid < -1:将信号传送给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。
sig:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l 进行查看。不推荐直接使用数字,应该使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
返回值:
成功: 0
失败: -1

super用户(root)可以发送信号给容易用户,普通用户是不能向系统用户发送信号的。

kill -9(root用户的pid)是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。

普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户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
31
32
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

int main(){
int pd = -1;
//创建进程
pd = fork();
if (pd == -1){
perror("fork");
return 1;
}
else if (pd == 0){
//子进程
while(1){
printf("child is run\n");
sleep(1);
}
}
else{
//父进程
sleep(5);
printf("child is not good\n");
kill(pd, SIGTERM);
printf("ok\n");
}
return 0;
}

6.2 raise函数

1
2
3
4
5
6
7
8
9
#include <signal.h>

int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
参数:
sig:信号编号
返回值:
成功:0
失败:非0

程序示例:

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 <string.h>

#include <unistd.h>
#include <signal.h>

int main(){
int i = 1;
while(1){
printf("run:%d\n", i++);
sleep(1);
if (i == 4){
raise(SIGALRM);
}
}
return 0;
}

6.3 abort函数

1
2
3
4
5
6
#include <stdlib.h>

void abort(void);
功能:给自己发送异常终止信号 SIGABRT,并产生core文件,等价于 kill(getpid(), SIGABRT);
参数:无
返回值:无

6.4 alarm函数(闹钟)

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

unsigned int alarm(unsigned int seconds);
功能:
设置定时器(闹钟)。在指定的seconds后,内核会给当前进程发送 SIGALRM 信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。
取消定时器alarm(0),返回旧闹钟余下秒数。
参数:
seconds: 指定的时间,以秒为单位
返回值:
返回0或剩余的秒数

定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸….无论进程处于何种状态,alarm都计时。

测试程序:

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

#include <unistd.h>

int main(){
int ret;
alarm(5);
printf("正在运行程序\n");
sleep(2);
ret = alarm(10);
printf("之前的闹钟无效了,之前还剩 %d 秒\n", ret);
printf("请按任意键退出...");
getchar();
return 0;
}

6.5 setitimer函数(定时器)

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

int setitimer(int which, const struct itimerval* new_value, struct itimerval* old_value);
功能:
设置定时器(闹钟)。可代替alarm函数。精度微秒us,可以实现周期定时。
参数:
which: 指定定时方法
a) 自然定时: ITIMER_REAL → SUGALR计算自然时间
b) 虚拟空间计时(用户空间): ITIMER_VIRTUAL → SIGPROF计算进程占用cpu的时间
c) 运行时计时(用户 + 内核): ITIMER_PROF → SIGPROF计算占用cpu及执行系统调用的时间
new_value:
struct itimerval{
struct timerval it_interval; //闹钟触发周期
struct timerval it_value; //闹钟触发时间
};
struct timeval{
long tv_sec; //秒
long tv_user; //毫秒
};
itimerval.it_vale: 设定一次执行function所延迟的秒数
itimerval.it_interval: 设定以后每几秒执行function
old_value: 存放旧的timeout值,一般指定为NULL
返回值:
成功: 0
失败: -1

程序示例:

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 <string.h>
#include <sys/time.h>

int main(){
struct itimerval iti;
int ret = -1;
//第一次触发时间
iti.it_value.tv_sec = 3;
iti.it_value.tv_usec = 0;
//触发周期
iti.it_interval.tv_sec = 2;
iti.it_interval.tv_usec = 0;
ret = setitimer(ITIMER_REAL, &iti, NULL);
if (ret == -1){
perror("setitmer");
return 1;
}
printf("按任意键退出...\n");
getchar();
return 0;
}

7.信号集

7.1 信号集概述

在PCB中有两个非常重要的信号集。一个称为“阻塞信号集”,另一个称为“未决信号集”。

这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合,借助信号集操作函数对PCB中的这两个信号集进行修改。

7.2 自定义信号集函数

为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在Linux系统中引入了信号集(信号的集合)。

这个信号集有点类似于我们的QQ群,一个个的信号相当于QQ群里的一个个好友。

信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。既然是一个集合,就需要对集合进行添加/删除等操作。

相关函数说明如下:

1
2
3
4
5
6
#include <signal.h>
int sigemptyset(sigset_t* set); //将set集合置空
int sigfillset(sigset* set); //将所有信号加入set集合
int sigaddset(sigset* set, int signo); //将signo信号加入到set集合
int sigdelset(sigset_t* set, int signo); //从set集合中移除signo信号
int sigismember(const sigset_t* set, int signo); //判断信号是否存在

除sigismember外,其余操作函数中的set均为传出参数。sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。

示例程序:

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

//打印信号集
void print_sigset(sigset_t* s){
int i = 0;
for (i = 0; i < 32; i++){
if (sigismember(s, i)){
printf("1");
}
else{
printf("0");
}
}
printf("\n");
}

int main(){
sigset_t set;
//清空信号集
sigemptyset(&set);
print_sigset(&set);

//将所有信号加入set集中
sigfillset(&set);
print_sigset(&set);

//将signo信号移除
sigdelset(&set, SIGHUP);
print_sigset(&set);

//将signo信号重新加入到set集合中
sigaddset(&set, SIGHUP);
print_sigset(&set);
return 0;
}

7.3 sigprocmask函数

信号阻塞也称信号屏蔽集、信号掩码。每个进程都有应该阻塞集,创建进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。

所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将阻塞的信号从信号阻塞集中删除,且对应的信号在阻塞时发生了,进程将会收到相应的信号。

我们可以通过 sigprocmask() 修改当前的信号掩码来改变信号的阻塞情况。

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

int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
功能:
检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。

参数:
how:信号阻塞集合的修改方法,有3种情况:
SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。相当于 mask = mask|set
SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。相当于 mask = mask & ~ set
SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集,相当于 mask = set
set:要操作的信号集地址。
setNULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。
oldset:保存原来信号阻塞地址
返回值:
成功:0
失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how不合法。

7.4 sigpending函数

1
2
3
4
5
6
7
8
9
#include <signal.h>

int sigpending(sigset_t* set);
功能:读取当前进程的未决信号集
参数:
set:未决信号集
返回值:
成功:0
失败:-1

8.信号捕捉

8.1 信号处理方式

一个进程收到一个信号的时候,可以用如下方式进行处理:

1)执行系统默认动作

对于大多数信号来说,系统默认动作是用来终止该进程。

2)忽略此信号(丢弃)

接收到此信号后没有任何动作。

3)执行自定义信号处理函数(捕捉)

用用户定义的信号处理函数处理该信号。

【注意】:SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。

内核实现信号捕捉过程:

8.2 signal函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:
注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。
参数:
signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill -l进行相应查看。
handler:取值有 3 种情况:
SIG_IGN:忽略该信号
SIG_DFL:执行系统默认动作
信号处理函数名:自定义信号处理函数,如:func
回调函数的定义如下:
void func(int signo){
//signo 为触发的信号,为signal() 第一个参数的值
}
返回值:
成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
失败:返回 SIG_ERR

该函数由ANSI定义,因为Unix和Linux版本的原因不推荐使用这个函数,取而代之的是sigaction函数

8.3 sigaction函数

1
2
3
4
5
6
7
8
9
10
11
12
#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
功能:
检查或修改指定信号的设置(或同时执行这两种操作)。
参数:
signum:要操作的信号。
act:要设置的对信号的新处理方式(传入参数)。
oldact:原来对信号的处理方式(传入参数)。
如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。
返回值:
成功:0
失败:-1

struct sigaction结构体:

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)sa_handler、sa_sigaction:信号处理函数指针,如signal()里的函数指针用法一样,应根据情况给 sa_sigaction、sa_handler 两者之一赋值,其取值如下:

a)SIG_IGN:忽略该信号

b)SIG_DFL:执行系统默认动作

c)处理函数名:自定义信号处理函数

2)sa_mask:信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号。

3)sa_flags:用于指定信号处理的行为,通常设置为0,表使用默认属性。它可以是一下值的”按位或“组合:

  • SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)
  • SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到SIGCHLD信号。
  • SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵尸进程。
  • SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
  • SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
  • SA_SIGINFO:使用sa_sigaction成员而不是sa_handler作为信号处理函数。

信号处理函数:

1
2
3
4
5
void (*sa_sigaction)(int signum, siginfo_t* info, void* context);
参数说明:
signum:信号的编号。
info:记录信号发送进程信号的结构体。
context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文。

示例代码:

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

void fun1(int signum, siginfo_t* info, void* context){
printf("捕捉到信号:%d\n", signum);
}

int main(){
int ret = -1;
struct sigaction act;
//使用新的信号处理函数指针
act.sa_sigaction = fun1;
//标志指定使用新的信号处理函数指针
act.sa_flags = SA_SIGINFO;
//信号注册
ret = sigaction(SIGINT, &act, NULL);
if (-1 == ret){
perror("sigaction");
return 1;
}
printf("按回车退出...\n");
while(1){
getchar();
}
return 0;
}

9.不可重入、可重入

如果一个函数不幸被设计成为这样:那么不同任务调用这个函数时可能修改其它任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数。

满足下列条件的函数是不可重入(不安全)的:

  • 函数体内使用了静态的数据结构
  • 函数体内调用了malloc() 或者 free() 函数(谨慎使用堆)
  • 函数体内调用了标准 I/O 函数

相反,肯定有一个安全的函数,这个安全的函数又叫可重入函数。那么什么是可重入函数呢?所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。

保证函数的可重入性的方法:

  • 在写函数时尽量使用局部变量(例如寄存器、栈中的变量)
  • 对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数

注意:信号处理函数是可重入函数

10.SIGCHLE信号

10.1 SIGCHLD信号产生的条件

1)子进程终止时

2)子进程接收到SIGSTOP信号停止时

3)子进程处在停止态,接受到SIGCONT后唤醒时

10.2 如何避免僵尸进程

1)最简单的方式,父进程通过wait()和waitpid()等待子进程结束,但是,这会导致父进程挂起。

2)如果父进程要处理的事情很多,不能够挂起,通过 signal() 函数人为处理信号 SIGCHLD,只要子进程退出自动调用指定好的回调函数,因为子进程结束后,父进程会收到该信号 SIGCHLD,可以在其回调函数里调用wait() 或 waitpid() 回收

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

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

void fun1(int signum, siginfo_t* info, void* context){
wait(NULL);
printf("子进程结束了\n");
}

int main(){
pid_t pid = -1;
int ret = -1, i;
struct sigaction act;
//使用新的信号处理函数指针
act.sa_sigaction = fun1;
//标志指定使用新的信号处理函数指针
act.sa_flags = SA_SIGINFO;
//信号注册
ret = sigaction(SIGCHLD, &act, NULL);
//创建子进程
pid = fork();
if (pid == -1){
perror("fork");
return 1;
}
else if (pid == 0){
for (i = 0; i < 10; i++){
printf("%d\n", i);
sleep(1);
}
}
else{
sleep(30);
}
return 0;
}

3)如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN) 通知内核,自己对子进程的结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核回收,并不再给父进程发送信号。

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

void fun1(int signo){
printf("子进程结束了\n");
}

int main(){
pid_t pid = -1;
int i;
//信号注册
signal(SIGCHLD, SIG_IGN);
//创建子进程
pid = fork();
if (pid == -1){
perror("fork");
return 1;
}
else if (pid == 0){
for (i = 0; i < 10; i++){
printf("%d\n", i);
sleep(1);
}
}
else{
sleep(30);
}
return 0;
}

九、守护进程-线程

1.终端的概念

在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),进程中,控制终端是保存在PCB中的信息,而fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。

默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是用户的键盘输入,进程往标准输出或标准错误输出也写到显示器上。

信号中还讲过,在控制终端输入有些特殊的控制按键可以给前台进程发信号,例如Ctrl+C表示SIGINT,Ctrl+\表示SIGQUIT。

函数说明:

1
2
3
4
5
6
7
8
#include <unistd.h>
char* ttyname(int fd);
功能:由文件描述符查出对应的文件名
参数:
fd:文件描述符
返回值:
成功:终端名
失败:NULL

2.进程组概念

2.1 进程组概述

进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。

每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。

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

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

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

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

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

2.2 相关函数说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
pid_t getpgrp(void);
功能:获取当前进程组的进程ID;
参数:无;
返回值:总是返回调用者的进程组ID;

pid_t getpgid(pid_t pid);
功能:获取指定进程的进程组ID;
参数:
pid:进程号,如果pid = 0,那么该函数作用和getpgrp一样
返回值:
成功:进程组ID
失败:-1

int setpgid(pid_t pid, pid_t pgid);
功能:
改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
参数:
将参1对应的进程,加入参2对应的进程组中
返回值:
成功:0
失败:-1

3.会话

3.1 会话概述

会话是一个或多个进程组的集合。

  • 一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;
  • 建立与控制终端连接的会话首进程被称为控制进程
  • 一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组
  • 如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组
  • 如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。

3.2 创建会话注意事项

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

2)该调用进程是组长进程,则出错返回

3)该进程成为一个新进程组的组长进程

4)需要root权限(ubuntu不需要)

5)新会话丢弃原有的控制终端,该会话没有控制终端

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

3.3 API函数介绍

getsid函数

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

pid_t getsid(pid_t pid);
功能: 获取进程所属的会话ID
参数:
pid: 进程号,pid为0表示查看当前进程session ID
返回值:
返回值:返回调用进程的会话ID
失败:-1

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

setsid函数

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

pid_t setsid(void);
功能:
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。调用了setsid函数的进程,既是新的会长,也是新的组长。
参数: 无
返回值:
成功:返回调用进程的会话ID
失败:-1

4.守护进程

4.1 守护进程介绍

守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立控制终端并且周期性地执行某种任务或等待处理某些发生的时间。一般采用以d结尾的名字。

守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离终端是为了避免进程被任何终端产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在Linux中,每一个系统用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个字段,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器,web服务器,http等。

4.2 守护进程模型

1)创建子进程,父进程退出(必须)

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

2)在子进程中创建新会话(必须)

  • setsid()函数
  • 使子进程完全独立出来,脱离控制

3)改变当前目录为根目录(不是必须)

  • chdir()函数
  • 防止占用可卸载的文件系统
  • 也可以换成其它路径

4)重设文件权限掩码(不是必须)

  • umask() 函数
  • 防止继承的文件创建屏蔽字拒绝某些权限
  • 增加守护进程灵活性

5)关闭文件描述符(不是必须)

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

6)开始执行守护进程核心工作(必须)

  • 守护进程退出处理程序模型

示例代码:

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

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

int main(){
int ret = -1;
pid_t pid = -1;
//1.创建子进程
pid = fork();
if (pid == -1){
perror("fork");
return 1;
}
else if(pid > 0){
exit(0);
}

//2.在子进程中创建新会话
pid = setsid();
if (pid == -1){
perror("setsid");
return 1;
}

//3.改变当前工作目录
ret = chdir("/");
if (ret == -1){
perror("chdir");
return 1;
}

//4.重设文件权限掩码
umask(0);

//5.关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

//6.开始执行进程核心工作
//要求每隔一秒种向txt文件插入时间
while(1){
system("date >> /home/pi/time.txt");
sleep(1);
}
return 0;
}

5.线程简介

5.1 线程概念

在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么,只是维护引用程序所需的各种资源,而线程则是真正的执行实体。

所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。

为了让进程完成一定的工作,进程必须至少包含一个线程。

进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统,操作系统会以进程为单位,分配系统资源,所以我们也说:进程是CPU分配资源的最小单位。

线程存在与进程当中(进程可以认为是线程的容器),是操作系统调度执行的最小单位。说通俗点,线程就是干活的。

进程是具有一定独立功能的程序关于某个数据集合上的一次运动活动,进程是系统进程资源分配和调度的一个独立单位。

线程是进程的一个实体,是CPU调度和分配的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其它的线程共享进程所拥有的全部资源。

如果说进程是一个资源管家,负责从主人哪里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某个进程。

进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆,栈,静态存储区。

进程是操作系统分配资源的最小单位

线程是操作系统调度的最小单位

5.2 线程函数列表安装

命令:

sudo apt-get install manpages-posix-posix-dev

【说明】manpages-posix-posix-dev 包含POSIX的header files 和calls的用法

查看:

man -k pthread

5.3 NPTL

当linux最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调用度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享系统的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程同步原语方面都存在问题。另外,这个线程模式也不符合POSIX的要求。

要改进LinuxThreads,非常明显我们需要内核的支持,并且需要重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括IBM的开发人员的团队开展了NGPT项目。同时,Red Hat 的一些开发人员开展了NPTL项目。NGPT在2003年中期被放弃了,把这个领域完全留给了NPTL。

NPTL或称为Native POSIX Thread Library,是Linux线程的一个新实现,它克服了LinuxThreads的缺陷,同时也符合POSIX的需求。与LinuxThreads相比,它在性能和稳定性方面都提供了重大的改进。

查看当前pthread库版本:getconf GNU_LIBPTHREAD_VERSION

5.4 线程的特点

类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进制机制实现出了线程的概念。

因此在这类系统中,进程和线程关系密切:

1)线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone。

2)从内核里看进程和线程是一样的,都有各自不同的PCB。

3)进程可以蜕变成线程

4)在Linux下,线程是最小的执行单位;进程是最小的分配资源单位

查看指定进程的LWP号:

ps -Lf pid

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone。

  • 如果复制对方的地址空间,那么就产出一个“线程”。
  • 如果共享对方的地址空间,就会产生一个“线程”

Linux内核是不区分进程和线程的,只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

5.5 线程共享资源

1)文件描述符表

2)每种信号的处理方式

3)当前工作目录

4)用户ID和组ID

内存地址空间(.test/.data/.bss/heap/共享库)

5.6 线程非共享资源

1)线程id

2)处理器现场和栈指针(内核栈)

3)独立的栈空间(用户空间栈)

4)errno变量

5)信号屏蔽字

6)调用优先级

5.7 线程的优缺点

优点:

  • 提高程序并发性
  • 开销小
  • 数据通讯、共享数据方便

缺点:

  • 库函数,不稳定
  • 调试、编写困难、gdb不支持
  • 对信号支持不好

优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

6.线程常用操作

6.1 线程号

就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。

进程号用 pid_t 数据类型表示,是一个非负整数。线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整型表示。

有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。

pthread_self函数:

1
2
3
4
5
6
7
8
9
#include <pthread.h>

pthread_t pthread_self(void);
功能:
获取线程号。
参数:

返回值:
调用线程的线程ID。

因为pthread_self这个函数是一个第三方库,所以在编译的时候需要链接一下外部的动态库

1
-pthread

示例代码:

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

#include <pthread.h>

int main(){
pthread_t pt;
pt = pthread_self();
printf("%lu\n", pt);
return 0;
}

pthread_equal函数:

1
2
3
4
5
6
7
8
int pthread_equal(pthread_t t1, pthread_t t2);
功能:
判断线程号t1和t2是否相等。为了方便移植,尽量使用函数来比较
参数:
t1,t2:待判断的线程号。
返回值:
相等:非0
不相等:0

参考程序:

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

#include <pthread.h>

int main(){
pthread_t pt;
pt = pthread_self();
if (pthread_equal(pt, pthread_self())){
printf("两个线程号相等\n");
}
else{
printf("两个线程号不相等\n");
}
return 0;
}

6.2 线程的创建

pthread_create函数:

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

int pthred_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
功能:
创建一个线程
参数:
thread:线程标识符地址。
attr:线程属性结构体地址,通常设置为NULL
start_routine:线程函数的入口地址。
arg:传入线程函数的参数。
返回值:
成功:0
失败:非0

在一个线程中调用pthread_create()创建新的线程后,当前进程从pthread_create()返回继续在下执行,而新的线程所执行的代码由我们传给pthread_create()的函数指针start_routine决定。

由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。

实例代码:

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 <pthread.h>

void* fun(void* agv){
printf("新的线程执行任务 tid: %lu\n 传入的参数为:%d\n", pthread_self(), (int)(long)agv); //这里因为是void*类型,直接转换为int长度不够,先让它变成long,然后再转换为int
return NULL;
}

int main(){
int ret = -1;
pthread_t tid = -1;
ret = pthread_create(&tid, NULL, fun, (void*)10);
if (ret != 0){
//打印错误信息
printf("pthread_create failed...\n");
printf("%s", strerror(ret));
return 1;
}
printf("main thread... tid:%lu\n", pthread_self());
printf("请按任意键结束\n");
getchar();
return 0;
}

6.3 线程资源回收

pthread_join函数

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

int pthread_join(pthread_t thread, void** retval);
功能:
等待线程结束(此函数会阻塞),并回收线程资源,类似于进程的 wait() 函数,如果线程已经结束,那么该函数会立刻返回。
参数:
thread:被等待的线程号。
retval:用来储存线程退出状态的指针地址。
返回值:
成功:0
失败:非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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

void* fun(void* arv){
int i;
for (i = 0; i < 5; i++){
printf("%d\n", i);
sleep(1);
}
return (void*)0x03;
}

int main(){
int ret = -1;
void* retp = NULL;
pthread_t tid = -1;
//创建线程
ret = pthread_create(&tid, NULL, fun, NULL);
if (ret != 0){
printf("pthread_create failed...\n");
return 1;
}
pthread_join(tid, &retp);
printf("retp:%p\n", retp);
printf("man thread exit...\n");
return 0;
}

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程不同的方式终止,通过pthread_join得到的终止状态是不同的,总结如下:

1)如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。

2)如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED

3)如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。

6.4 线程连写

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

//线程1执行的函数
void* fun(void* arg){
int i;
for (i = 0; i < 7; i++){
fflush(stdout); //刷新缓冲区
printf("%c", ((char*)arg)[i]);
sleep(1);
}
return NULL;
}

int main(){
int ret1 = -1, ret2 = -1;
pthread_t tid1 = -1, tid2 = -1;
char arr1[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
char arr2[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
//创建两个进程
ret1 = pthread_create(&tid1, NULL, &fun, (void*)arr1);
if (ret1 != 0){
printf("线程1创建失败\n");
return 1;
}
ret2 = pthread_create(&tid2, NULL, &fun, (void*)arr2);
if (ret2 != 0){
printf("线程2创建失败\n");
return 1;
}

//释放线程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 1;
}

6.5 线程分离

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获得它的状态为止。但是线程也可以被置为detach(分离)状态,这样的线程一旦终止就立刻回收它所有资源,而不保留终止状态。

不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

pthread_detach函数:

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:
使调用线程与当前进程分离,分离后不代表此线程不依赖于当前进程,线程分离的目的是将线程资源的回收工作交给系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。
参数:
thread:线程号
返回值:
成功:0
失败:非0

6.6 线程退出

在进程中我们可以调用exit函数或_exit函数来结束进程,在一个线程中我们可以通过以下三种在不中断整个进程的情况下停止它的控制流。

  • 线程从执行函数中返回
  • 线程调用pthread_exit退出线程
  • 线程可以被同一进程中的其它线程取消。

pthread_exit函数:

1
2
3
4
5
6
7
#include <pthread.h>
void pthread_exit(void* retval);
功能:
退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源不会释放。
参数:
retval:存储线程退出状态的指针
返回值:无

参考程序:

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

void* fun(void* arg){
int i;
for (i = 0; i < 5; i++){
printf("thread:%d\n", i);
sleep(1);
}
pthread_exit(NULL);
}

int main(){
pthread_t tid = -1;
int ret = -1;
//创建线程
ret = pthread_create(&tid, NULL, &fun, NULL);
if (ret != 0){
printf("线程创建失败\n");
return 1;
}
ret = pthread_detach(tid);
if (ret != 0){
printf("分离进程失败\n");
}
printf("请按任意键继续...");
getchar();
return 0;
}

6.7 线程取消

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_cancel(thread_t thread);
功能:
杀死(取消)线程
参数:
thread:目标线程ID
返回值:
成功:0
失败:出错编号

注意:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某一个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点)才能存储进度

杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write…执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。

可粗略认为一个系统调用(进入内核)即为一个取消点

参考程序:

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

void* fun(void* arg){
int i;
for (i = 0; i < 5; i++){
printf("%d\n", i);
sleep(1);
}
}

int main(){
int ret = -1;
pthread_t tid;
//创建线程
ret = pthread_create(&tid, NULL, &fun, NULL);
if (ret != 0){
printf("pthread is error\n");
return 1;
}
ret = pthread_detach(tid);
if (ret != 0){
printf("pthread_detach is error\n");
return 1;
}
sleep(3);
ret = pthread_cancel(tid);
if (ret != 0){
printf("pthread_cancel is error\n");
return 1;
}
else{
printf("子线程被取消\n");
}
return 0;
}

7.线程属性(了解)

7.1 概述

Linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。

如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

1
2
3
4
5
6
7
8
9
10
11
typedef struct{
int etachstate; //线程分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackadd_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;

主要结构体成员:

1)线程分离状态

2)线程栈大小(默认平均分配)

3)线程栈警戒缓冲区大小(位于栈末尾)

4)线程栈最低地址

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。

线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and paraneters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

7.2 线程属性初始化和销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>
int pthread_attr_init(pthread_attr_t* attr);
功能:
初始化线程属性函数,注意:应先初始化线程属性,再pthread_create创建线程
参数:
attr:线程属性结构体
返回值:
成功:0
失败:错误号

int pthread_attr_destroy(pthread_attr_t* attr);
功能:
销毁线程属性所占用的资源函数
参数:
attr:线程属性结构体
返回值:
成功:0
失败:错误号

7.3 线程分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。

  • 非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
  • 分离状态:分离状态没有被其它的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

相关函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);
功能:设置线程分离状态
参数:
attr:已初始化的线程属性
detachstate:分离状态
PTHREAD_CREATE_DETACHED(分离状态)
PTHREAD_CREATE_JOINABLE(非分离状态)
返回值:
成功:0
失败:非0

int pthread_attr_getdetachstate(const pthread_attr_t* attr, int* detachstate);
功能:获得线程分离状态
参数:
attr:已初始化的线程属性
detachstate:分离状态
PTHREAD_CREATE_DETACHED(分离状态)
PTHREAD _CREATE_JOINABLE(非分离状态)
返回值:
成功:0
失败:非0

这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其它的线程使用,这样调用pthread_create的线程就得到了错误的线程号。

要避免这种情况可以采用一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会,留出足够的时间让函数pthread_create返回。

设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

7.4 线程栈地址

POSIX.1定义了两个常量来检查系统是否支持栈属性:

  • _POSIX_THREAD_ATTR_STACKADDR
  • _POSIX_THREAD_ATTR_STACKSIZE

也可以给sysconf函数传递来进行检测:

  • _SC_THREAD_ATTR_STACKADDR
  • _SC_THREAD_ATTR_STACKSIZE

当进程栈地址空间不够时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分配设置和获取线程的栈地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize);
功能:设置线程的栈地址
参数:
attr:指向一个线程属性的指针
stackaddr:内存首地址
stacksize:返回线程的堆栈大小
返回值:
成功:0
失败:错误号

int pthread_attr_getstack(const pthread_attr_t* attr, void** stackaddr, size_t* stacksize);
功能:获取线程的栈地址
参数:
attr:指向一个线程属性的指针
stackaddr:返回获取的栈地址
stacksize:返回获取的栈大小
返回值:
成功:0
失败:错误号

7.5 线程栈大小

当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,但线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);
功能:设置线程的大小
参数:
attr:指向一个线程属性的指针
stacksize:线程的堆栈大小
返回值:
成功:0
失败:错误号

int pthread_attr_getstacksize(const pthread_attr_t* attr, size_t* stacksize);
功能:获取线程的栈大小
参数:
attr:指向一个线程属性的指针
stacksize:返回线程的堆栈大小
返回值:
成功:0
失败:错误号

7.6 线程使用注意事项

1)主线程退出其它线程不退出,主线程应该调用pthread_exit

2)避免僵尸线程

​ a)pthread_join

​ b)pthread_detach

​ c)pthread_create指定分离属性

​ 被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

3)malloc和mmap申请的内存可以被其它线程释放

4)应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其它线程在子进程中均pthread_exit

5)信号的复杂语义很难和多线程共享,应避免在多线程中引用信号机制。

十、线程同步

1.互斥锁

1.1 同步与互斥概述

现代操作系统基本上都是多任务操作系统,即同时有大量可调度实体在运行。在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源
  • 多任务之间有依赖关系,某个任务的运行依赖于另一个任务

这两种情形是多任务编程中遇到的最基本问题,也是多任务编程中的核心问题,同步和互斥就是用于解决这两个问题的。

**互斥:**是指散步在不同任务之间的若干程序片段,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之间的任意程序片段,只能等到该任务完成这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时间只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

**同步:**是指散步在不同任务之间的若干程序片段,它们的运行必须严格按照规定的某种先后顺序来运行,这种先后顺序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后顺序运行。比如A任务的运行依赖于B任务产生的数据。

虽然,同步是一种更为复杂的互斥锁,互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但它是必须要按照某种次序来运行相应的线程(也是一种互斥),因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步任务之间则有顺序关系

1.2 为什么需要互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于公司部门里,使用打印机打印东西的同时(还没打印完),别人刚好也在此时此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

下面用程序来模拟一下这个过程,线程一需要打印“hello”,线程二需要答应“word:,不加任何处理的话,打印出来的内容会混乱

测试程序:

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 <pthread.h>

void* fun(void* argv){
int i;
for (i = 0; i < 6; i++){
fflush(stdout);
printf("%c", *((char*)argv + i));
sleep(1);
}
return NULL;
}

int main(){
int ret = -1;
char arr1[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
char arr2[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
pthread_t tid1, tid2;
//创建线程
ret = pthread_create(&tid1, NULL, fun, (void*)arr1);
if (ret != 0){
perror("pthread is error\n");
return 1;
}
ret = pthread_create(&tid2, NULL, fun, (void*)arr2);
if (ret != 0){
perror("pthread is error\n");
return 1;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}

实际上,打印机是有做处理的,我在打印着的时候别人是不允许打印的,只有等待我打印结束后别人才允许打印。这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当A需要打印时,它先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印,而这时B也需要打印,B同时先检查锁,发现锁是锁住的,它就在门口等,当A打印结束后,他会开锁出来,这时B才进去上锁打印。

1.3 互斥锁Mutex介绍

在线程里也有这么一把锁:互斥锁(mutex),也叫互斥量,互斥锁是一种简单的加锁方式来控制对共享资源的访问,互斥锁只有两种状态,即加锁(lock)和解锁(unlock)。

互斥锁的操作流程如下:

1)在访问共享资源后临界区域前,对互斥锁进行加锁。

2)在访问完成后释放互斥锁导上的锁

3)对互斥锁进行加锁后,任何其它企图再次对互斥锁加锁的线程将会阻塞,直到锁被释放。

互斥锁的数据类型是pthread_mutex_t

安装对应帮助手册:

sudo apt-get install manpages-posix-dev

1.4 pthread_mutex_init函数

初始化互斥锁:

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
功能:
初始化一个互斥锁
参数:
mutex:互斥锁地址,类型是 pthread_mutex_t
attr:设置互斥量的属性,通常可采用默认属性,即可将attr设为NULL
可以使用宏PTHREAD_MUTEX_INITIALIZER静态初始化互斥锁,比如:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
这种方法等价于使用 NULL 指定的 attr 参数调用 pthread_mutex_init() 来完成动态初始化,不同之处在

restrict,C语言中的一种限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外的所有其它直接或间接的方式修改该对象的内容

1.5 pthread_mutex_destroy函数

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t* mutex);
功能:
销毁指定的一个互斥锁,互斥锁在使用完毕后,必须要对互斥锁进行销毁,以释放资源
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0错误码

1.6 pthread_mutex_lock函数

1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t* mutex);
功能:
对互斥锁上锁,若互斥锁已经上锁,则调用者阻塞,直到互斥锁解锁后再上锁。
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0错误码
int pthread_mutex_trylock(pthread_mutex_t* mutex);
调用该函数时,若互斥锁未加锁,则上锁,返回0
若互斥锁已加锁,则函数直接返回失败,即EBUSY

1.7 pthread_mutex_unlock函数

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t* mutex);
功能:
对指定的互斥锁解锁
参数:
mutex:互斥锁地址
返回值:
成功:0
失败:非0错误码

1.8 测试程序

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

pthread_mutex_t mutex;
void* fun(void* argv){
int i, ret = -1;
ret = pthread_mutex_lock(&mutex);
if (ret != 0){
printf("error\n");
return NULL;
}
for (i = 0; i < 6; i++){
fflush(stdout);
printf("%c", *((char*)argv + i));
sleep(1);
}
pthread_mutex_unlock(&mutex);
return NULL;
}

int main(){
int ret = -1;
char arr1[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
char arr2[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
pthread_t tid1, tid2;
pthread_mutex_init(&mutex, NULL);
//创建线程
ret = pthread_create(&tid1, NULL, fun, (void*)arr1);
if (ret != 0){
perror("pthread is error\n");
return 1;
}
ret = pthread_create(&tid2, NULL, fun, (void*)arr2);
if (ret != 0){
perror("pthread is error\n");
return 1;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}

1.9 死锁(DeadLock)

1)什么是死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通讯造成的一种阻塞的现象。若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁。

2)死锁引起的原因

  • 竞争不可抢占资源引起死锁

​ 也就是我们说的第一种情况,而这都在等待对方占有的不可抢占的资源

  • 竞争可消耗资源引起死锁

​ 有p1,p2,p3三个进程,p1向p2发送消息并接收p3发送的消息,p2向p3发送消息并接收p1的消息,p3向p1发送消息并接收p2的消息,如果设置是先接到消息后发送消息,则所有的消息都不能发送,这就造成了死锁。

  • 进程推进顺序不当引起死锁

​ 有进程p1,p2,都需要资源A,B,本来可以p1运行A—>p1运行B—>p2运行A—>p2运行B,但是顺序换了,p1运行A时p2运行B,容易发生第一种死锁。互相抢占资源。

3)死锁的必要条件

  • 互斥条件

某资源只能被一个进程使用,其它进程请求该资源时,只能等待,直到资源使用完毕后释放资源

  • 请求和保持条件

程序已经保持了至少一个资源,但是又提出了新要求,而这个资源被其它进程占用,自己占用资源却保持不放。

  • 不可抢占条件

进程已获得的资源没有使用完,不能被抢占

  • 循环等待条件

必然存在一个循环链

4)处理死锁的思路

预防死锁

​ 破坏死锁的四个必要条件中的一个或多个来预防死锁

避免死锁

​ 和预防死锁的区别就是,在资源动态分配过程中,用某种方式防止系统进入不安全的状态

检查死锁

​ 运行时出现死锁,能及时发现死锁,把程序解脱出来

解除死锁

​ 发生死锁后,解脱进程,通常撤销进程,回收资源,再分配给正处于阻塞状态的进程

5)预防死锁的方法

破坏请求和保持条件

协议1:

所有进程开始前,必须一次性地申请所需的所有资源,这样运行期间就不会再提出资源要求了,破坏了请求条件,即使有一种资源不能满足需求,也不会给它分配正在闲置的资源,这样它就没有资源,就破坏了保持条件,从而预防死锁的发生

协议2:

允许一个进程只获得初期的资源就开始运行,然后再把运行完的资源释放出来,然后再请求新的资源

破坏不可抢占条件

当一个已经保持了某种不可抢占资源的进程,提出新资源请求不能被满足时,它必须释放已经保持的所有资源,以后需要时再重新申请

破坏循环等待条件

对系统中的所有资源类型进行线性排序,然后规定每个进程必须按序列号递增的顺序请求资源。假如进程请求到了一些序列号较高的资源,然后有请求一个序列较低的资源时,必须先释放系统和更高序列号的资源后才能申请低序号的资源。多个同类资源必须一起请求

死锁程序

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 <pthread.h>

//定义互斥变量
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;

void* fun1(void* argv){
pthread_mutex_lock(&mutex1);
printf("线程1请求1号资源成功...\n");
pthread_mutex_lock(&mutex2);
printf("线程1请求2号资源成功...\n");

pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}

void* fun2(void* argv){
pthread_mutex_lock(&mutex2);
printf("线程2请求2号资源成功...\n");
pthread_mutex_lock(&mutex1);
printf("线程2请求1号资源成功...\n");

pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}

int main(){
pthread_t tid1 = -1, tid2 = -1;
//初始化锁
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);
//创建线程
pthread_create(&tid1, NULL, fun1, NULL);
pthread_create(&tid2, NULL, fun2, NULL);

//关闭进程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//摧毁资源
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);

return 0;
}

2.读写锁

2.1 读写锁概述

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取整个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。

读写锁的特点如下:

1)如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
2)如果有其它线程写数据,则其它线程都不允许读、写操作

读写锁分为读锁和写锁,规则如下:

1)如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁

2)如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

POSIX定义的读写锁的数据类型是pthread_rwlock_t

2.2 pthread_rwlock_init函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
功能:
用来初始化rwlock所指向的读写锁
参数:
rwlock:指向要初始化的读写指针
attr:读写锁的属性指针。如果attr为NULL则会使用默认的属性初始化读写锁,否则使用指定的attr初始化读写锁
可以使用宏PTHREAD_RWLOCK_INITIALIZER静态初始化读写锁,比如:
pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方法等价于使用NULL指定的attr参数调用pthread_rwlock_init()来完成动态初始化,不同之处在于PTHREAD_RWLOCK_INITIALIZER宏不进行错误检查
返回值:
成功:0,读写锁的状态将成为已初始化和已解锁
失败:非0错误码

2.3 pthread_rwlock_destroy函数

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
功能:
用于摧毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由 pthread_rwlock_init()自动申请的资源)。
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0错误码

2.4 pthread_rwlock_rdlock函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
功能:
以阻塞方式在读写锁上获取读锁(读锁定)
如果没有写者持有该锁,并且写者阻塞在该锁上,则调用线程会获取读锁
如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。一个线程可以在一个读写锁上多次执行读锁定。
线程可以成功调用 pthread_rwlock_rdlock()函数n次,但是之后该线程必须调用 pthread_rwlock_unlock() 函数n次才能解除锁定
参数:
rwlock:读写锁指针。
返回值:
成功:0
失败:非0错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
用于尝试以非阻塞的方式来在读写锁上获取读锁
如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。

2.5 pthread_rwlock_wrlock函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
功能:
在读写锁上获取写锁(写锁定)
如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0错误码
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
用于尝试以非阻塞的方式来在读写锁上获取写锁
如果有任何的读者或者写者持有该锁,则立即失败返回

2.6 pthread_rwlock_unlock函数

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
功能:
无论是读锁或者写锁,都可以通过此函数解锁
参数:
rwlock:读写锁指针
返回值:
成功:0
失败:非0错误码

2.7 测试程序

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

int num = 0;

//读写锁对象
pthread_rwlock_t rwlock;

//读锁的函数
void* read_fun(void* argv){
while(1){
//添加读锁
pthread_rwlock_rdlock(&rwlock);
printf("线程%d正在读num:%d\n", (int)(long)argv, num);
//随机休眠1-3秒
pthread_rwlock_unlock(&rwlock);
sleep(random() % 3 + 1);
}
return NULL;
}

//写锁的函数
void* write_fun(void* argv){
while(1){
pthread_rwlock_wrlock(&rwlock);
num++;
printf("线程%d正在读num:%d\n", (int)(long)argv, num);
pthread_rwlock_unlock(&rwlock);
//随机休眠1-3秒
sleep(random() % 3 + 1);
}
return NULL;
}

int main(){
int i, ret = -1;
pthread_t tid[6];
//随机数种子
srand(getpid());
//初始化读写锁
ret = pthread_rwlock_init(&rwlock, NULL);
if (ret != 0){
printf("pthread_rwlock_init is error\n");
return 1;
}
//创建6个进程
for (i = 0; i < 8; i++){
//读数据
if (i < 4){
pthread_create(&tid[i], NULL, &read_fun, (void*)(long)i);
}
//写数据
else{
pthread_create(&tid[i], NULL, &write_fun, (void*)(long)i);
}
}
//释放线程
for (i = 0; i < 7; i++){
pthread_join(tid[i], NULL);
}
//释放读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}

3.条件变量

3.1 条件变量概述

在互斥锁不同,条件变量是用来等待而不是用来上锁的,条件变量本身不是锁!

条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量的两个动作:

  • 条件不满,阻塞线程
  • 当条件满足,通知阻塞的线程开始工作

条件变量的类型:pthread_cond_t。

3.2 pthread_cond_init函数

1
2
3
4
5
6
7
8
9
10
11
12
#Include <pthread.h>
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);
功能:
初始化一个条件变量
参数:
cond:指向要初始化的条件变量指针。
attr:条件变量属性,通常为默认值,传NULL即可
也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
返回值:
成功:0
失败:非0错误号

3.3 pthread_cond_destroy函数

1
2
3
4
5
6
7
8
9
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t* cond);
功能:
摧毁一个条件变量
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号

3.4 pthread_cond_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
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
功能:
阻塞等待一个条件变量
a)阻塞等待条件变量cond(参1)满足
b)释放已掌握的互斥锁(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);
a)b)两步为一个原子操作
c)当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁 pthread_mutex_lock(&mutex);
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
返回值:
成功:0
失败:非0错误码

int pthread_cond_timedwait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex, const struct timespec* restrict abstime);
功能:
限时等待一个条件变量
参数:
cond:指向要初始化的条件变量指针
mutex:互斥锁
abstime:绝对时间
返回值:
成功:0
失败:非0错误号

abstime补充说明:

1
2
3
4
5
6
7
8
struct timespec{
time_t tv_sec;//秒
long tv_nsec;//纳秒
}
time_t cur = time(NULL);//获取当前的时间
struct timespec t;//定义timespec结构体变量t
t.tv_sec = cur + 1;//定义1秒
pthread_cond_timedwait(&cond, &t);

3.5 pthread_cond_signal函数

唤醒至阻塞在条件变量上的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t* cond);
功能:
唤醒至少一个阻塞在条件变量上的线程
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号

int pthread_cond_broadcast(pthread_cond_t* cond);
功能:
唤醒全部阻塞在条件变量上的线程
参数:
cond:指向要初始化的条件变量指针
返回值:
成功:0
失败:非0错误号

3.6 生产者消费者条件变量模型

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法

假设有两个线程,一个模拟生产者的行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

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

struct node{
int data;
struct node* next;
};

//头指针
struct node* head = NULL;

//条件变量
pthread_cond_t cond;
//互斥锁变量
pthread_mutex_t mutex;

void* fun1(void* argv){
//生产者
while(1){
//加锁
pthread_mutex_lock(&mutex);
//创建一个节点
struct node* pnew = malloc(sizeof(struct node));
if (pnew == NULL){
printf("malloc is error\n");
break;
}
//节点初始化
pnew->data = random() % 100;
pnew->next = NULL;
pnew->next = head;
head = pnew;
printf("生产者正在生产%d\n", pnew->data);
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);
//随机休眠
sleep(random()%3 + 1);
}
pthread_exit(NULL);
}

void* fun2(void* argv){
//消费者线程
struct node* pTemp = NULL;
while(1){
pthread_mutex_lock(&mutex);
if (head == NULL){
//等待
pthread_cond_wait(&cond, &mutex);
}
else{
pTemp = head;
head = pTemp->next;
printf("正在消费:%d\n", pTemp->data);
free(pTemp);
}
pthread_mutex_unlock(&mutex);
}
pthread_exit(NULL);
}

int main(){
pthread_t tid1, tid2;
int ret = -1;
srand(getpid());
//初始化互斥量
ret = pthread_mutex_init(&mutex, NULL);
if (ret != 0){
printf("pthread_mutex_init is error\n");
return 1;
}
//条件变量初始化
ret = pthread_cond_init(&cond, NULL);
if (ret != 0){
printf("pthread_cond_init is error\n");
return 1;
}
//创建子线程
ret = pthread_create(&tid1, NULL, &fun1, NULL);
if (ret != 0){
printf("pthread_create is error\n");
return 1;
}
ret = pthread_create(&tid2, NULL, &fun2, NULL);
if (ret != 0){
printf("pthread_create is error\n");
return 1;
}
//销毁线程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//销毁条件变量
pthread_cond_destroy(&cond);
//摧毁互斥量
pthread_mutex_destroy(&mutex);
return 0;
}

3.7 条件变量的优缺点

相较于mutex而言,条件变量可以减少竞争。

如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。

有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

4.信号量

4.1 信号量概述

信号量广泛用于进程或进程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问

编程时可以根据操作信号量的结果判断是否对公共资源具有访问权限,当信号量值大于0时,则可以访问,负责将阻塞

PV原语是对信号量的操作,一次P操作使信号量减1,一次V操作使信号量加1

信号量主要用于进程或线程间的同步和异步两种典型情况

信号量数据类型为:sem_t

信号量用于互斥

信号量用于同步:

4.2 sem_init函数

初始化信号量

1
2
3
4
5
6
7
8
9
10
11
#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);
功能:
创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化
参数:
sem: 信号量的地址
pshared: 等于0,信号量在线程间同步(常用);不等于0,信号量在线程间共享。
value: 信号量的初始值
返回值:
成功:0
失败:-1

4.3 sem_destroy函数

销毁信号量

1
2
3
4
5
6
7
8
9
#include <semaphore.h>
int sem_destroy(sem_t* sem);
功能;
删除sem标识的信号量
参数:
sem:信号量地址
返回值:
成功:0
失败:-1

4.4 信号量P操作(减1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <semaphore.h>
int sem_wait(sem_t* sem);
功能:
将信号量的值减1,操作前,先检查信号量(sem)的值是否为0,若信号量为0,此函数会阻塞,直到信号量大于0时才进行减1操作
参数:
sem:信号量的地址
返回值:
成功:0
失败:-1

int sem_trywait(sem_t* sem);
以非阻塞的方式来对信号量进行减1操作
若操作前,信号量的值等于0,则对信号量的操作失败,函数立即返回

int sem_timedwait(sem_t* sem, const struct timespec* abs_timeout);
限时尝试将信号量的值减1
abs_timeout:绝对时间

abs_timeout补充说明:

1
2
3
4
5
6
7
8
struct timespec{
time_t tv_sec; //秒
long tv_nsec; //纳秒
};
time_t cur = time(NULL); //获取当前的时间
struct timespec t; //定义timespec结构体变量t
t.tv_sec = cur + 1; //定时1秒
sem_timedwait(&cond, &t);

4.5 信号量V操作(加1)

1
2
3
4
5
6
7
8
9
#include <semaphore.h>
int sem_post(sem_t* sem);
功能:
将信号量的值加1并发出信号唤醒等待线程( sem_wait())。
参数:
sem:信号量的地址
返回值:
成功:0
失败:-1

4.6 获取信号量的值

1
2
3
4
5
6
7
8
9
10
#include <semaphore.h>
int sem_getvalue(sem_t* sem, int* sval);
功能:
获取sem标识的信号量的值,保存在sval中
参数:
sem:信号量地址
sval:保存信号量的地址
返回值:
成功:0
失败:-1

4.7 示例程序

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

sem_t sem;

void* fun1(void* argv){
//占用后P操作
sem_wait(&sem);
char i;
for (i = 'A'; i <= 'Z'; i++){
printf("%c", i);
fflush(stdout);
sleep(1);
}
//释放后执行V操作
sem_post(&sem);
return NULL;
}

void* fun2(void* argv){
sem_wait(&sem);
char i;
for (i = 'a'; i <= 'z'; i++){
printf("%c", i);
fflush(stdout);
sleep(1);
}
sem_post(&sem);
return NULL;
}

int main(){
pthread_t tid1, tid2;
//初始化信号量
sem_init(&sem, 0, 1);
//创建线程
pthread_create(&tid1, NULL, &fun1, NULL);
pthread_create(&tid2, NULL, &fun2, NULL);
//释放线程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//销毁信号量
sem_destroy(&sem);
return 0;
}

4.8 消费者模式更改

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

struct node{
int data;
struct node* next;
};

//头指针
struct node* head = NULL;

sem_t sem_bearer, sem_consumer;

void* fun1(void* argv){
//生产者
while(1){
//创建一个节点
struct node* pnew = malloc(sizeof(struct node));
if (pnew == NULL){
printf("malloc is error\n");
break;
}
//节点初始化
pnew->data = random() % 100;
pnew->next = NULL;
pnew->next = head;
head = pnew;
printf("生产者正在生产%d\n", pnew->data);
sem_post(&sem_consumer);
sem_wait(&sem_bearer);
//随机休眠
sleep(random()%3 + 1);
}
pthread_exit(NULL);
}

void* fun2(void* argv){
//消费者线程
struct node* pTemp = NULL;
while(1){
if (head == NULL){
//等待
}
else{
pTemp = head;
head = pTemp->next;
printf("正在消费:%d\n", pTemp->data);
free(pTemp);
sem_post(&sem_bearer);
sem_wait(&sem_consumer);
}
}
pthread_exit(NULL);
}

int main(){
pthread_t tid1, tid2;
int ret = -1;
sem_t sem_bearer, sem_consumer;
srand(getpid());
//初始化信号量
sem_init(&sem_bearer, 0, 4);
sem_init(&sem_consumer, 0, 0);
//创建子线程
ret = pthread_create(&tid1, NULL, &fun1, NULL);
if (ret != 0){
printf("pthread_create is error\n");
return 1;
}
ret = pthread_create(&tid2, NULL, &fun2, NULL);
if (ret != 0){
printf("pthread_create is error\n");
return 1;
}
//销毁线程
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//销毁信号量
sem_destroy(&sem_bearer);
sem_destroy(&sem_consumer);
return 0;
}

4.9 多信号量生产者模式

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

struct node{
int data;
struct node* next;
};

//头指针
struct node* head = NULL;

sem_t sem_bearer, sem_consumer;

void* fun1(void* argv){
//生产者
while(1){
//创建一个节点
struct node* pnew = malloc(sizeof(struct node));
if (pnew == NULL){
printf("malloc is error\n");
break;
}
//节点初始化
pnew->data = random() % 100;
pnew->next = NULL;
pnew->next = head;
head = pnew;
printf("生产者正在生产%d\n", pnew->data);
sem_post(&sem_consumer);
sem_wait(&sem_bearer);
//随机休眠
sleep(random()%3 + 1);
}
pthread_exit(NULL);
}

void* fun2(void* argv){
//消费者线程
struct node* pTemp = NULL;
while(1){
if (head == NULL){
//等待
}
else{
pTemp = head;
head = pTemp->next;
printf("正在消费:%d\n", pTemp->data);
free(pTemp);
sem_post(&sem_bearer);
sem_wait(&sem_consumer);
}
}
pthread_exit(NULL);
}

int main(){
pthread_t tid[6];
int ret = -1, i;
sem_t sem_bearer, sem_consumer;
srand(getpid());
//初始化信号量
sem_init(&sem_bearer, 0, 4);
sem_init(&sem_consumer, 0, 0);
//创建子线程
for (i = 0; i < 6; i++){
if (i < 2){
ret = pthread_create(&tid[i], NULL, &fun1, NULL);
if (ret != 0){
printf("pthread_create is error\n");
return 1;
}
}
else{
ret = pthread_create(&tid[i], NULL, &fun2, NULL);
if (ret != 0){
printf("pthread_create is error\n");
return 1;
}

}
}
//销毁线程
for (i = 0; i < 6; i++){
pthread_join(tid[i], NULL);
}
//销毁信号量
sem_destroy(&sem_bearer);
sem_destroy(&sem_consumer);
return 0;
}