Linux系统编程

day1
1 学习目标
1说出Linux下的目录结构和常见目录的作用
2熟练使用Linux下的相对路径和绝对路径
3熟练使用Linux下常用文件和目录操作相关的命令
4熟练使用修改用户权限、用户和用户组相关的命令
5熟练使用文件的查找和检索相关的命令
6熟练掌握Ubuntu下的软件安装和卸载
7熟练使用压缩工具完成文件或目录的压缩解压缩

命令解析器(shell)
作用:对用户输入到终端的命令进行解析,调用对应的执行程序。
在这里插入图片描述
用户在终端输入命令,由shell在命令解析器对命令进行解析(按照path环境变量搜索命令)解析成内核能够识别的指令,然后由内核执行命令,最后由终端显示命令结果给用户。
在这里插入图片描述
Linux系统的目录结构–倒立的树状结构
在这里插入图片描述

Linux下的主要目录结构:
/bin:binary二进制文件,可执行程序,shell命令–ls,mv,cp
/sbin:super user存放系统管理员使用的系统管理程序
/dev:磁盘,驱动,显卡
/lib:运行时需要加载的一些动态库
/etc:存放配置文件
/opt:安装第三方应用程序
/tmp:存放临时文件

相对路径绝对路径:
绝对路径:从根目录开始表示的路径,也就是从/开始,例如:/home/itcast
相对路径:1.从当前所处的目录开始表示的路径。2, 表示当前目录 3… 表示当前目录的上一级目录

$:表示当前用户为普通用户, #表示当前用户为root用户

文件和目录操作相关的命令
1.tree以树状形式查看指定目录内容
2.ls查看指定目录下的文件信息
3.cd切换路径(路径可以为绝对或者相对)
4.pwd用户当前的工作目录
5.which显示命令所在的目录如which ls ,which cp
6.touch创建新的文件
7.mkdir创建新的目录
8.rm 文件名 --删除文件
rm -rf 目录名 --递归强制删除目录
9.cp 源目录或文件 目标目录或文件
10.mv改名或移动文件 ,移动文件的第二个参数一定为目录。
11.cat将文件内容一次输出到终端,如果文件太长无法全部显示
12.more文件内容分页显示到终端,但是只能一直向下浏览,不能回退。
13head从文件头部开始查看前n行内容head -n 文件名
13tail从文件尾部查看最后n行内容 tail -n 文件名一个比较重要的应用,显示日志信息
14.软链接 ln-s 文件名 链接方式的名子 软链接连接要用绝对路径,一旦这个连接文件发生位置变动,就不能找到那个文件。软连接文件大小是路径+文件名总字节数。
15.硬链接:ln 文件名 硬链接名字
本质:不同的文件名所在的inode节点是相同的,相同inode节点指向了相同的数据块,所以他们的文件内容是一样的,文件内容会同步。
在这里插入图片描述
当新创建一个文件,硬链接计数器为1,给文件创建一个硬链接后,硬链接计数加1,删除一个硬链接后,硬链接计数器减1,删除硬链接后,硬链接计数器为0会删除这个文件。
硬链接的应用场合:
1.可以同步文件的作用:修改file文件的内容,会在其余三个硬链接文件上同步。
2,可以保护文件:删除文件时候,只要硬链接计数器不为0不会真正删除,起到保护文件的作用。
16wc显示文件的总行数,单词数和总字节数
17whoami显示当前登录的用户名
18修改文件权限–chmod
命令:chmod [who] [+|-|=] [mode] 文件名
操作对象【who】
u – 用户(user)
g – 同组用户(group)
o – 其他用户(other)
a – 所用用户(all)【默认】
操作符【±=】

  • – 添加权限
  • – 取消权限
    = – 赋予给定权限并取消其他权限
    权限【mode】
    r – 读
    w – 写
    x – 执行
    19.修改文件所有者和所属组 chown 文件所有者:文件所属组 文件名
    sudo chown mytest:mytest file.txt
    20.find命令按文件名查找
    find 路径 -name 文件名
    find 路径 -type 类型
    f普通文件,d目录,l链接
    find 路径 -size 范围
    范围:大于表示+小于-等于不需要添加符号
    20.grep查找内容参数
    -r若是目录则可以递归搜索,-n显示行号 -i忽略大小写
    21find和grep命令结合使用通过find找到文件grep去查找文件里的内容
    find . -name “*.c” | xargs grep -n "main"
    22
    压缩:tar zcvf 压缩包名字.tar.gz 原材料[要打包压缩的文件或目录]
    解压缩:tar zxvf 已有的压缩包(test.tar.gz)
    23
    zip -r 压缩包名 要压缩的文件(含文件或目录)
    unzip 压缩包名 -d 解压目录
    day2
    1.vi编辑器使用
    2.gcc编译经历的四个步骤
    3.静态库和共享动态库
    什么是库:库是二进制文件,是源代码文件的另一种表现形式,是加了密的源代码,是一些功能相近或者相似函数的集合体。
    库可以提高代码的重用性可以提高程序的健壮性,减少开发者的代码开发量,缩短开发周期。
    库的使用:头文件包含库的声明,库文件包含库函数的代码实现。
    静态库:目标代码的集合体,是在可执行程序运行前就已经加入到执行代码中,成为可执行程序的一部分,一般以.a作为后缀名前缀名Lib

在这里插入图片描述
静态库的制作:
下面以fun1.c , fun2.c和head.h三个文件为例讲述静态库的制作和使用, 其中head.h文件中有函数的声明, fun1.c和fun2.c中有函数的实现.
步骤1:将c源文件生成对应的.o文件
gcc -c fun1.c fun2.c
或者分别生成.o文件:
gcc -c fun1.c -o fun1.o
gcc -c fun2.c -o fun2.o
步骤2:使用打包工具ar将准备好的.o文件打包为.a文件
在使用ar工具是时候需要添加参数rcs
r更新、c创建、s建立索引
命令:ar rcs 静态库名 .o文件
ar rcs libtest1.a fun1.o fun2.o
静态库的使用
静态库制作完成之后, 需要将.a文件和头文件一定发布给用户.
假设测试文件为main.c, 静态库文件为libtest1.a, 头文件为head.h
用到的参数:
L:指定要连接的库所在的目录
l:指定链接时需要的静态库去掉前缀和后缀
l:指定main.c文件用到的头文件head.h
gcc -o main1 main.c -L./ -ltest1 -I./
静态库的优点:函数最终打包到应用程序中,实现函数本地化,库函数的调用效率==自定义函数的使用效率,程序在运行时与库函数再无瓜葛但是消耗资源大,每个进程使用静态库都需要复制一份浪费内存空间。

动态库:
共享库在程序编译时并不会被连接到目标代码中, 而是在程序运行是才被载入. 不同的应用程序如果调用相同的库, 那么在内存里只需要有一份该共享库的拷贝, 规避了空间浪费问题.动态库在程序运行时才被载入, 也解决了静态库对程序的更新、部署和发布会带来麻烦. 用户只需要更新动态库即可, 增量更新. 为什么需要动态库, 其实也是静态库的特点导致.
按照习惯, 一般以”.so”做为文件后缀名. 共享库的命名一般分为三个部分:
前缀:lib
库名称:自己定义即可, 如test
后缀:.so
所以最终的静态库的名字应该为:libtest.so
在这里插入图片描述

动态库的制作:
1.生成.o目标文件,此时要加编译选项-fpic
gcc -fpic -c fun1.c fun2.c
-fpic创建与地址无关的预编译选项,目的是能在多个应用程序间共享
2.生成共享库,此时要加链接器选项-shared
gcc -shared fun1.o fun2.o -o libtest.so

动态库的使用
引用和静态库方式相同
L:指定要连接的库所在的目录
l:指定链接时需要的静态库去掉前缀和后缀
l:指定main.c文件用到的头文件head.h
gcc -o main1 main.c -L./ -ltest2 -I./

如何让系统找到共享库
1拷贝自己制作的共享库到/lib或者/usr/lib
2临时设置LD_LIBRARY_PATH:export LD_LIBRARY_PATH= L D L I B R A R Y P A T H : 库 路 径 3 永 久 设 置 , 把 e x p o r t L D L I B R A R Y P A T H = LD_LIBRARY_PATH:库路径 3永久设置, 把export LD_LIBRARY_PATH= LDLIBRARYPATH:3,exportLDLIBRARYPATH=LD_LIBRARY_PATH:库路径, 设置到∼/.bashrc文件中, 然后在执行下列三种办法之一:
执行. ~/.bashrc使配置文件生效(第一个.后面有一个空格)
执行source ~/.bashrc配置文件生效
退出当前终端, 然后再次登陆也可以使配置文件生效
4永久设置,把export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径,设置到/etc/profile文件中
5将其添加到 /etc/ld.so.cache文件中
编辑/etc/ld.so.conf文件, 加入库文件所在目录的路径
运行sudo ldconfig -v, 该命令会重建/etc/ld.so.cache文件
共享库的特点:
1动态库把对一些库函数的链接载入推迟到程序运行的时期。
2可以实现进程之间的资源共享。(因此动态库也称为共享库)
3将一些程序升级变得简单。
4甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)

第三天

1.makefile
makefile文件中定义了一系列的规则来指定, 哪些文件需要先编译, 哪些文件需要后编译, 哪些文件需要重新编译, 甚至于进行更复杂的功能操作, 因为makefile就像一个Shell脚本一样, 其中也可以执行操作系统的命令. makefile带来的好处就是——“自动化编译”, 一旦写好, 只需要一个make命令, 整个工程完全自动编译, 极大的提高了软件开发的效率.
make是一个命令工具, 是一个解释makefile中指令的命令工具, 一般来说, 大多数的IDE都有这个命令, 比如:Visual C++的nmake, Linux下GNU的make. 可见, makefile都成为了一种在工程方面的编译方法.
makefile文件中会使用gcc编译器对源代码进行编译, 最终生make成可执行文件或者是库文件.
makefile文件的命名:makefile或者Makefile
1.1 makefile的基本规则
目标:依赖
(tab) 命令
makefile基本规则三要素:
目标:要生成的目标文件
依赖:目标文件由哪些文件生成
命令:通过执行该命令由依赖文件生成目标
下面以具体的例子来讲解:

当前目录下有main.c fun1.c fun2.c sum.c, 根据这个基本规则编写一个简单的makefile文件, 生成可执行文件main.

第一个版本:
main:main.c fun1.c
gcc -o main main.c fun1.c fun2.c sum.c

缺点:效率低,修改一个文件所有文件都会重新编译。

makefile 工作原理
基本原则:
若想生成目标,检查规则中所有依赖文件是否都存在,如果有的依赖文件不存在,则向下搜索规则:如果有规则用来生成依赖文件,则执行规则命令中生成依赖文件,如果没有规则则用来生成该依赖文件。
在这里插入图片描述
如果所有依赖都存在,检查规则中的目标是否需要更新,必须检查它的所有依赖,依赖中有任何一个被更新,则目标必须更新。目标时间>依赖时间 不更新
目标时间<依赖时间,更新

第二个版本
main:main.o fun1.o fun2.o sum.o
gcc -o main main.o fun1.o fun2.o sum.o
main.o:main.c
gcc -o main.o -c main.c
fun1.o:fun1.c
gcc-o fun1.o -c fun1.c
fun2.o:fun2.c
gcc-o fun2.o -c fun2.c
sum.o:sum.c
gcc -o sum.o -c sum.c

缺点:冗余,若.c文件数量很多,编写起来比较麻烦
makefile中的变量优点类似于C语言中的宏定义,使用该变量相当于内容替换,使用变量可以使makefile易于维护,修改起来变得简单。makefile有三种类型变量:普通变量,自带变量,自动变量
普通变量:
1.变量定义直接用=
2.使用变量值用$变量名
定义了两个变量: foo、bar, 其中bar的值是foo变量值的引用。
除了使用用户自定义变量, makefile中也提供了一些变量(变量名大写)供用户直接使用, 我们可以直接对其进行赋值:
CC = gcc #arm-linux-gcc
CPPFLAGS : C预处理的选项 -I
CFLAGS: C编译器的选项 -Wall -g -c
LDFLAGS : 链接器选项 -L -l
自动变量:
$@:表示规则中的目标
$<:表示规则中的第一个条件
$^:表示规则中的所有条件,组成一个列表,以空格隔开,如果这个列表中有重复的选项,则取消重复值。自动变量只能在规则中使用。
模式规则:
至少在规则的目标定义中要包含%,%表示一个或多个,在依赖条件中同样可以使用%,依赖条件%的取值取决于其目标。
makefile的第三个版本:
**target=main
object=main.o fun1.o fun2.o sum. o
CC=gcc
CPPFLAGS=-I./
( t a r g e t ) : (target): (target):(object)
$(CC) -o $@ $^
%.o:%.c
$(CC) -o $@ -c $< ( C P P F L A G S ) ∗ ∗ m a k e f i l e 函 数 : w i l d c a r d − − 查 找 指 定 目 录 下 的 指 定 类 型 文 件 s r c = (CPPFLAGS)** makefile函数: wildcard--查找指定目录下的指定类型文件 src= (CPPFLAGS)makefilewildcardsrc=(wildcard .c)//找到当前目录下所有后缀为.c的文赋值给src
patsubst–匹配替换
obj= ( p a t s u b s t (patsubst %.c ,%.o, (patsubst(src))//把src变量里所有后缀为.c文件替换成.o文件
**src= ( w i l d c a r d . / ∗ . c ) ; o b j e c t = (wildcard ./*.c); object= (wildcard./.c);object=(patsubst %.c,%.o,$(src))
target=main
CC=gcc
CPPFLAGS=-I ./
( t a r g e t ) : (target): (target):(object)
$(CC) -o $@ $^
%.o:%.c
$(CC) -o $@ -c $< ( C P P F L A G S ) ∗ ∗ 缺 点 : 每 次 编 译 都 需 要 手 动 清 理 中 间 . o 文 件 和 最 终 目 标 文 件 。 m a k e f i l e 的 清 理 操 作 用 途 : 清 理 编 译 生 成 的 中 间 . o 文 件 和 最 终 目 标 文 件 。 m a k e c l e a n 如 果 当 前 目 录 下 有 同 名 c l e a n 文 件 , 则 不 会 执 行 c l e a n 对 应 的 命 令 , . P H O N Y : c l e a n 声 明 目 标 为 伪 目 标 之 后 , m a k e f i l e 将 不 会 检 查 该 目 标 是 否 存 在 或 者 目 标 是 否 需 要 更 新 m a k e f i l e 第 5 个 版 本 ∗ ∗ s r c = (CPPFLAGS)** 缺点:每次编译都需要手动清理中间.o文件和最终目标文件。 makefile的清理操作 用途:清理编译生成的中间.o文件和最终目标文件。 makeclean如果当前目录下有同名clean文件,则不会执行clean对应的命令, .PHONY:clean 声明目标为伪目标之后,makefile将不会检查该目标是否存在或者目标是否需要更新 makefile第5个版本 **src= (CPPFLAGS).omakefile.omakecleancleanclean.PHONYcleanmakefilemakefile5src=(wildcard ./
.c);
object= ( p a t s u b s t (patsubst %.c,%.o, (patsubst(src))
target=main
CC=gcc
CPPFLAGS=-I ./
( t a r g e t ) : (target): (target):(object)
$(CC) -o $@ $^
%.o:%.c
$(CC) -o $@ -c $< $(CPPFLAGS
.PHONY:clean
clean:
-rm -f $(target) $(object)**第5个版本综合使用了变量函数模式规则和清理命令是一个比较完善的版本。

gdb调试
gdb介绍:
1.启动程序,可以按照你的自定义要求随心所欲的运行程序
2.程序在断点处停止
3.当程序被停住时,检查程序中所发生的事。
4.动态改变程序的执行环境

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

启动gdb gdb program
设置运行参数set agrs
查看设置好的运行参数 show args

run断点处停止
start执行一条语句

list命令用来打印程序的源代码
list linenum:打印第lienum行的上下文内容
list function: 显示函数名为function的函数的源程序
list -:显示当前文件开始处的源程序
list file:linenum=显示file文件下的第n行
list file:function 显示file文件的函数名为function函数的源程序

设置当前文件的断点
b 10 设置断点 在源程序第10行
b func在func函数的入口

多文件设置断点
b filename:linenum–在源文件filename的linenum行处停止
b filename:function–在源文件filename的function函数的入口停止

查询断点
info b

删除断点:
delete num1 num2 …
delete num1-num2
断点无效
unable num1-nunm5

r-运行程序
n-一条执行语句
s-进入函数体内
c-跳转到下一个断点

print 打印变量,字符串,表达式

自动显示变量的值,当程序停住时,或是你在单步追踪时,这些变量会自动,相关的GDB命令是display
display 变量名
info display–查看display设置的自动显示的信息
delete display dnums… – 删除自动显示, dnums意为所设置好了的自动显式的编号。如果要同时删除几个, 编号可以用空格分隔, 如果要删除一个范围内的编号, 可以用减号表示(如:2-5)
disable和enalbe不删除自动显示的设置, 而只是让其失效和恢复。
c库I/O函数流
在这里插入图片描述

使用fopen函数打开一个文件, 返回一个FILE* fp, 这个指针指向的结构体有三个重要的成员.
文件描述符: 通过文件描述可以找到文件的inode, 通过inode可以找到对应的数据块
文件指针: 读和写共享一个文件指针, 读或者写都会引起文件指针的变化
文件缓冲区: 读或者写会先通过文件缓冲区, 主要目的是为了减少对磁盘的读写次数, 提高读写磁盘的效率.

c库函数是对系统函数的在一次封装
在这里插入图片描述
Linux为每一个运行程序都会分配一个0~4g的地址空间
在这里插入图片描述
进程的虚拟地址空间分为用户区和进程区,其中内核区是受保护的,用户不能够对其进行读写操作。内核中有一个很重要的就是进程管理,进程管理区中有一个区域就是PCB(结构体),PCB中有文件描述符表,文件描述符表中存放着文件描述符,设计IO操作都会用到这个文件描述符。

pcb和文件描述符表、在这里插入图片描述
C标准函数与系统函数区别
什么是系统调用
由操作系统实现并提供给外部应用程序的编程接口。是应用程序同系统之间数据交换的桥梁。
一个进程启动之后,默认打开3个文件描述符
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR FILENO 2
新打开文件返回文件描述符表中未使用的最小文件描述符,调用open函数打开或创建一个文件,得到一个文件描述符。、

open函数:打开或创建一个文件
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname参数是要打开或创建的文件名,和fopen一样,pathname可以是相对路径也可以是绝对路径。
flags:O_RDONLY 只读打开,O_WRONLY 只写打开 O_RDWR 可读可写打开
一下选项可以同时指定0个或多个,和必选项按位或做flags方式打开。
O_APPEND文件末尾追加
O_EXCL如果同时指定了O_CREAT,文件已存在,则出错返回。
函数返回值:
成功:返回一个最小且未被占用的文件描述符
失败:-1

close函数-关闭文件
int close(int fd);
函数返回值:
成功返回0
失败返回-1,并设置errno值
当一个进程终止时,内核对进程所有尚未关闭的文件描述符调用close关闭,所以当用户程序不再调用close,终止时内核会关闭所有文件描述符,但对于一个常年累月服务器,打开文件描述符一定要关闭,否则会占用大量文件描述符和系统资源。

read函数:从打开的设备或文件中读取数据
ssize_t read(int fd, void *buf, size_t count);
函数参数:
fd:文件描述符
buf:读上来的数据保存到缓冲区buf中
count:buf缓冲区存放的最大字节数
返回值:
>0 读取到字节数
=0文件读取完毕
=-1出错

write:向打开设备或文件中写数据
ssize_t write(int fd, const void *buf, size_t count);
函数参数
fd:文件描述符
buf:缓冲区,要写入文件或设备的数据
count:buf中数据的长度
函数返回值:
成功:返回写入的字节数
错误:返回-1

lseek
所有打开的文件都有一个文件偏移量简称为cfo,cfo通常是非负整数,用于表明文件开始处到文件当前位置的字节数,文件被打开时,cfo初始为0,除非使用了O_APPEND。
使用lseek函数可以改变文件的cfo
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
函数描述:移动文件指针
参数offset的含义取决于参数whence:
SEEK_SET:文件偏移量将设置为offset
SEEK_CUR:文件偏移量将被设置为cfo加上offset,offset可以设置为正或负
SEEK_END:文件偏移量将被设置为文件长度加上offset
lseek常用操作:
文件移动到头部
lseek(fd,0,SEEK_SET);
获取文件当前的位置
lseek(fd,0,SEEL_CUR);
获取文件的长度
lseek(fd,0,SEEK_END);
实现文件的拓展
curpos=lseek(fd,1000,SEEK_END);//从文件尾部拓展1000个字节

perror和errno
errno是一个全局变量,当系统调用后会出错将errno进行设置,perror可以将errno对应的描述信息打印出来。

read函数对普通文件是非阻塞的,终端设备,管道和套接字是阻塞的。

stat/lstat:获取文件的属性
int stat(const char *pathname, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
函数返回值:
成功返回0
失败返回-1

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; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};
If (st_mode & S_IRUSR) -----为真表明可读
If (st_mode & S_IWUSR) ------为真表明可写
If (st_mode & S_IXUSR) ------为真表明可执行

if(S_ISREG(st_mode)) ------为真表示普通文件
if(S_ISDIR(st.st_mode)) ------为真表示目录文件

stat和lsat函数区别:
普通文件没有区别
链接文件lstat函数获取的是链接文件本身的属性信息,而stat函数获取是链接文件指向的文件信息。

opendir:打开一个目录
DIR *opendir(const char *name);
返回值:指向目录的指针

readdir函数:读取目录内容-目录项
struct dirent readdir(DIR dirp);
strucr dirent
{
ino_t d_ino; // 此目录进入点的inode
off_t d_off; // 目录文件开头至此目录进入点的位移
signed short int d_reclen; // d_name 的长度, 不包含NULL 字符
unsigned char d_type; // d_name 所指的文件类型
char d_name[256]; // 文件名
};
d_type的取值:
DT_BLK - 块设备
DT_CHR - 字符设备
DT_DIR - 目录
DT_LNK - 软连接
DT_FIFO - 管道
DT_REG - 普通文件
DT_SOCK - 套接字
DT_UNKNOWN - 未知
在这里插入图片描述
closedir:关闭目录
int closedir
dir
drip);
成功返回0失败返回-1

读取目录内容的一般步骤
1 DIR *pDir = opendir(“dir”); //打开目录
2 while((p=readdir(pDir))!=NULL){} //循环读取文件
3 closedir(pDir); //关闭目录

dup函数:复制文件描述符
int dup(int podfd);//要复制旧的文件描述符
成功:返回最小且未被占用的文件描述符
失败返回-1;

dup2函数
int dup2(int oldfd,int newfld);
成功:将旧的文件描述符复制给新的文件描述符,两个文件描述符指向同一个文件。
失败:返回-1
假设newfd已经指向了一个文件,首先close原来打开的文件,然后newfd指向oldfd的文件,如果newfd没有被占用,newfd指向oldfd的文件。

day5
2.1进程相关概念
程序和进程
程序是指编译好的二进制文件,在磁盘上,占用磁盘空间,是一个静态的概念。
进程:是一个启动的程序,进程占用的是系统资源,如:物理内存,CPU,终端,是一个动态的概念。
程序:剧本
进程:戏
同一个剧本可以在多个舞台上同时上演,同一个程序可以加载不同的进程(彼此之间互补影响)。

并行和并发
并发:在同一个时间段内,是同一个cpu上,运行多个程序。
如:若将CPU的1S的时间分成1000个时间片,每个进程执行完一个时间片必须无条件让出CPU的使用权,这样1S中就可以执行1000个进程。
并行性指两个或两个以上的程序在同一时刻发生(需要有多颗)。

PCB进程控制块:每一个进程在内核中都有一个PCB来维护进程相关信息。
进程ID:系统中每一个进程有唯一的id,在c语言中用pid_t类型ji表示,其实就是一个非负整数。
进程的状态:就绪运行挂起停止状态
进程切换需要保存和恢复一些CPU寄存器。
描述虚拟空间的信息。
用户id和组id。

fork函数:创建子线程
pid_t fork(void);
调用成功返回子进程的pid,失败返回-1
在这里插入图片描述
调用fork函数之后父子进程分别返回一个值。
调用fork函数之后,父进程执行到什么位置子进程就从哪里执行。
通过fork函数的返回值来区分父子进程
哪个进程优先获得CPU的时间片哪个进程优先执行。

ps aux|grep “XXXX”–查找进程相关信息
kill -9 pid 杀死进程

getpid-获得当前进程的pid
getppid–获得当前父进程的pid

进程退出后,进程能够回收自己用户区的资源,但是不能够回收内核空间的PCB资源,必须由父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。
孤儿进程:父进程死掉,而子进程还活着,这个进程变成了孤儿进程,孤儿进程被init进程领养,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。
僵尸进程:子进程死了,父进程活着,父进程没有调用wait函数或waitpid函数完成对子进程的回收,则子进程就变成了僵尸进程。
如何解决僵尸进程:
僵尸进程是一个死亡的进程,所以不能用kill命令将其杀死,通过杀死父进程的方法可以杀死僵尸进程,杀死父进程,僵尸进程被init进程领养,由intit进程完成对僵尸进程的领养。

wait函数:
pid_t wait(int *status);
函数作用:阻塞并等到子进程退出
回收子进程资源
获取子进程结束状态
status参数:子进程的退出状态
WIFEXITED(status):为非0 → 进程正常结束
WEXITSTATUS(status):获取进程退出状态
WIFSIGNALED(status):为非0 → 进程异常终止
WTERMSIG(status):取得进程终止的信号编号。

waitpid
pid_t waitpid(pid_t pid, int *status, in options);
函数作用
同wait函数
函数参数
参数:
pid:
pid = -1 等待任一子进程。与wait等效。
pid > 0 等待其进程ID与pid相等的子进程。
status:子进程的退出状态
opinions:设置未WNOHANG,函数非阻塞,设置未0,函数阻塞。
函数返回值:

0 返回回收掉的子进程ID
=-1无子进程
=0参3为WNOHANG,且子进程正在运行

day6 进程间的通信
Linux环境下,进程地址空间是相互独立的,每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另外一个进程中都看不到,所以进程和进程之间不能够进行相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2在从内核缓冲区把数据读走,内核提供这种机制称为进程通信IPC
在这里插入图片描述
进程间的通信方式
管道使用最简单,信号开销最小,共享映射区无血缘关系,本地套接字最稳定。
管道:基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。

管道的本质是一块内核缓冲区。
由两个文件描述符引用,一个读端一个写端
规定数据从管道的写端流入管道,从读端流出。
当两个进程都终结时候,管道也会自动消失。
管道的写端和读端默认都是阻塞的。
管道的原理:
管道的实质是内核缓冲区,内部使用环形队列实现。
默认缓冲区大小为4K,可以使用ulimit-a命令获取大小。

管道的局限性:
数据一旦被读走,便不在管道中存在,不可反复读取。
数据只能在一个方向上流动。
只能在具有血缘关系的进程间使用通道,

创建管道–pipe函数
int pipe(int fd[2]);
创建成功fd[0]读端,fd[1]写端
返回值:成功返回0,失败返回-1.并设置errno值
函数调用成功返回读端和写端的文件描述符,其中f[0]是读端,f[1]是写端,向管道读写数据是通过使用两个文件描述符进行的,读写管道的实质是操作内核缓冲区。管道创建成功后父进程同时掌握着管道的读端和写端。

父子进程使用管道通信
一个进程由pipe()创建管道后,一般再由fork()一个子进程,然后通过管道实现父子进程的通信,父子进程间具有相同的文件描述符,且指向同一个管道,其他没有血缘关系的进程不能获得pipe产生两个相同的文件描述符,因此不能利用管道进行通信。
1.父进程创建管道
2.父进程fork子进程
3.父进程关闭读端,子进关闭写端

管道的读写行为
read读操作:
有数据正常读,返回读出的字节数
无数据 写端全部关闭,read解除阻塞,返回0,相当于读到文件末尾
没有全部关闭 read阻塞
写操作:
读端全部关闭,管道破裂,进程终止,内核给当前进程发SIGPIPE信号
读端没全部关闭:
缓冲区写满了:write阻塞
缓冲区没有满:继续write
如何设置管道为非阻塞
第1步: int flags = fcntl(fd[0], F_GETFL, 0);
第2步: flag |= O_NONBLOCK;
第3步: fcntl(fd[0], F_SETFL, flags);
若是读端设置为非阻塞:
写端没有关闭,管道中没有数据可读,则read返回-1;
写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
写端已经关闭,管道中有数据可读,则read返回实际读到的字节数
写端已经关闭,管道中没有数据可读,则read返回0

FIFO称为命名管道,以区分管道,管道只能用于有血缘关系的进程间通信,但通过FIFO,不相关的进程也能够进行交换数据。
创建管道:
-1使用命令
mkfifo 管道名
-2使用函数
int mkfifo(const char *pathname, mode_t mode);
参数说明和返回值可以查看man 3 mkfifo
当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可用于FIFO。如:close、read、write、unlink等。
FIFO严格遵循先进先出(first in first out),对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
使用FIFO完成两个进程的通信
在这里插入图片描述
进程A:
创建一个fifo文件:myfifo
调用open函数打开myfifo文件
write函数写入字符串其实将数据写入到内核缓冲区
close关闭myfifo
进程B:
调用open函数打开myfifo文件
read函数读取文件内容(其实就是从内核中读取数据)
打印显示的数据内容
close关闭myfifo文件

内存映射区:
内存映射区使一个磁盘文件与磁盘空间中的一个缓冲区相映射。从缓冲区取数据,相当于读文件中的相应字节,将数据写入缓冲区,则会将数据写入文件。这样,就可以不使用read和write函数情况下,使用地址完成I/O操作。使用存储映射这种办法,首先通知内核,将一个指定文件映射到存储区域中,这个映射工作可以通过mmap函数来实现。
在这里插入图片描述
mmap:建立存储映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
函数返回值:
成功:返回创建的映射区首地址;
失败:MAP_FAILED宏
addr: 指定映射的起始地址, 通常设为NULL, 由系统指定
length:映射到内存的文件长度
prot: 映射区的保护方式, 最常用的:
读:PROT_READ
写:PROT_WRITE
读写:PROT_READ | PROT_WRITE
flags: 映射区的特性, 可以是
MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。
MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
fd:由open返回的文件描述符, 代表要映射的文件。
offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射。

munmap:释放由mmap函数建立的存储映射区
int munmap(void *addr, size_t length);
返回值:
成功:返回0
失败:返回-1,设置errno值
函数参数:
addr:调用mmap函数成功返回的映射区首地址
length:映射区大小(mmap函数的第二个参数)

mmap注意事项:
创建映射区过程中,隐含着一次对映射文件的读操作,将文件内容映射到缓冲区中。
当MAP_SHARED时,要求映射区的权限<=文件打开的权限,而MAP_PPRIVE则无所谓,因为mmap中权限是对内存的限制。
映射区释放与文件关闭无关,只要映射建立成功文件可以立刻关闭
当映射文件大小为0时,不能建立映射区。所以,用于映射区的文件必须有实际大小,mmap使用时常常会出现总线错误。通常是由于文件共享文件存储空间大小引起的。
文件偏移量必须为0或者4K整数倍
mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功在进行后续操作。

有关mmap函数的使用总结
第一个参数写成NULL
第二个参数要映射的文件大小 > 0
第三个参数:PROT_READ 、PROT_WRITE
第四个参数:MAP_SHARED 或者 MAP_PRIVATE
第五个参数:打开的文件对应的文件描述符
第六个参数:4k的整数倍

day7信号
信号是信息的载体,现依然是主要的通信手段。
信号的特点:简单,不能携带大量信息,满足特定条件才会产生

信号的机制:进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕后在继续执行。每个进程收到的所有信号,都是由内核负责发送的。

信号有三种状态:产生,未决,和递达
信号的产生:
按键产生
系统调用产生
系统软件条件产生
硬件异常产生
未决:产生和递达之间的状态:主要由于阻塞导致该状态。
递达:递达并且到达该进程
信号的处理方式:
执行默认动作
忽略信号
调用自己处理函数
信号的特质:信号的实现手段导致信号有很强的延时性,但对于用户来说时间非常短,不易察觉。
Linux内核的PCB,除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和为未决信号集。

阻塞信号集和未决信号集
阻塞信号集中保存的都是被当前进程阻塞的信号。若当前这个进程收到的是阻塞信号集中的某些信号,这些信号需要暂时被阻塞,不予处理。
信号产生后由于某些原因(主要是阻塞)不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态,若是信号从阻塞信号集中解除阻塞,则该信号会被处理,并且从未决信号集中去除。

信号相关的函数
signal函数:注册信号捕捉函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数参数
signum:信号编号
handler:信号处理函数

kill函数/命令
描述:给指定进程发送信号
kill命令:kill-SIGKILL 进程PID
kill函数:int kill(pid_t pid,int sig);
函数返回值:
成功:0
失败:-1,设置errno
函数参数:
sig信号参数,不推荐直接使用数字,应使用宏名,因为不同操作系统编号名称可能不同,但名称一致。

pid参数:
pid>0发送信号给指定的进程
pid=0 发送信号给调用kill函数同属于同一组进程的所有进程
进程组:每一个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。

alarm函数:
unsigned int alarm(unsigned int seconds);
函数描述:设置定时器。在指定seconds后,内核会给当前进程发送14SIGALRM信号,进程收到该信号,默认动作终止。每一个进程有且只有唯一的一个定时器。
函数返回值:返回0或剩余的秒数。
在这里插入图片描述
常用操作:取消定时器alarm(0),返回闹钟剩下的秒数
alarm使用的是自然定时法,与进程状态无关,就绪,运行,挂起,阻塞,终止,僵尸,无论进程处于哪种状态,alarm都计时。

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
函数描述:设置定时器,可代替alarm函数,精度微妙us,可以实现周期定时。
函数参数:
which:指定定时方式
自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间
虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用cpu的时间
运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
new_value:struct itimerval, 负责设定timeout时间。
itimerval.it_value: 设定第一次执行function所延迟的秒数 itimerval.it_interval: 设定以后每几秒执行function
struct itimerval {
struct timerval it_interval; // 闹钟触发周期
struct timerval it_value; // 闹钟触发时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
}
old_value: 存放旧的timeout值,一般指定为NULL

信号集相关
阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还处于没有执行状态的集合,这两个集合存储在内核的PCB中。
下面以SIGINT为例说明信号未决信号集和阻塞信号集的关系:
当进程收到一个SIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1:
如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集上该位置上的值保持为1,表示该信号处于未决状态;
如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。
当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。
在这里插入图片描述
信号集相关函数
由于信号集属于内核的一块区域,用户不能直接操作内核空间,因此内核提供了一些信号集相关的函数,使用这些函数可以完成对信号集相关的操作。
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
信号集相关函数
int sigemptyset(sigset_t *set);
函数说明:将某个信号集清0
函数返回值:成功:0;失败:-1,设置errno
int sigfillset(sigset_t *set);
函数说明:将某个信号集置1
函数返回值:成功:0;失败:-1,设置errno
int sigaddset(sigset_t *set, int signum);
函数说明:将某个信号加入信号集合中
函数返回值:成功:0;失败:-1,设置errno
int sigdelset(sigset_t *set, int signum);
函数说明:将某信号从信号清出信号集
函数返回值:成功:0;失败:-1,设置errno
int sigismember(const sigset_t *set, int signum);
函数说明:判断某个信号是否在信号集中
函数返回值:在:1;不在:0;出错:-1,设置errno
sigprocmask函数
函数说明:用来屏蔽信号、解除屏蔽也使用该函数。其本质,读
取或修改进程控制块中的信号屏蔽字(阻塞信号集)。
函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数返回值:成功:0;失败:-1,设置errno
函数参数:
how参数取值:假设当前的信号屏蔽字为mask
SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
set:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字。
oldset:传出参数,保存旧的信号屏蔽字。
sigpending函数
函数原型:int sigpending(sigset_t *set);
函数说明:读取当前进程的未决信号集
函数参数:set传出参数
函数返回值:成功:0;失败:-1,设置errno

5.信号捕捉函数
sigaction函数
函数说明:
注册一个信号处理函数
函数原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum:捕捉的信号
act:传入参数,新的处理方式
oldact:传出参数,旧的处理方式
struct sigaction{
void (*sa_handler)(int); //信号处理函数
sigset_t sa_mask; // 信号处理函数执行期间要阻塞的信号
int sa_flags;//默认为0,通常使用默认标识
};
sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
sa_mask: 用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sa_flags:通常设置为0,使用默认属性。
信号处理不支持排队:信号处理函数期间,信号被阻塞,信号只会被处理一次。

内核实现信号捕捉的过程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,比如:
1.用户程序注册了SIGQUIT信号的处理函数sighandler。
2.当前正在执行main函数,这时发生中断或异常切换到内核态。
3.在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
4.内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
5.sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
6.如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

SIGCHILD信号
产生SIGCHILD信号的条件:
子进程结束时候
子进程收到SIGSTOP信号
子进程停止时,收到SIGCONT信号
SIGCHILD信号的作用:
子进程退出后,内核会给它的父进程发送SIGCHILD信号,父进程接收到这个信号后会对子进程进行回收。
使用SIGCHLD信号完成对子进程的回收可以避免父进程阻塞等待而不能执行其他操作,只有当父进程收到SIGCHLD信号之后才去调用信号捕捉函数完成对子进程的回收,未收到SIGCHLD信号之前可以处理其他操作。

day8 守护进程和线程

学习目标
1说出守护进程的特点
2独立完成守护进程的创建
3独立实现多个线程的创建
4独立实现线程的退出和资源回收
5理解线程同步的思想

守护进程
守护进程是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,一般采用d结尾。
Linux后台的一些系统服务进程,没有终端控制,不能直接和用户交互,不受用户登录注销影响,一再在运行着,他们都是守护进程。
进程组和会话
进程组:一个或者多个进程的集合,每一个进程都属于一个进程组,引入进程组是为了简化对进程的管理,当父进程创建子进程的时候,默认父进程与子进程同属于一个进程组。
进程组ID==第一个进程ID(组长进程)。如父进程创建了多个子线程,父进程和多个子线程同属于一个组,而由于父进程是进程组里的第一个进程,所有父进程就是这个组的组长进程。
注意:
可以使用kill -SIGKILL -进程组ID来将整个进程组内的进程全部杀死
只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关,
进程生存周期:从进程组创建到最后一个进程离开

会话:
一个会话是多个进程组的集合
创建会话的进程不能是进程组的组长
创建会话的进程称为一个进程组的组长进程,同时也成为会话的会长。
在这里插入图片描述
ps -ajx查看进程组id和会话id

创建守护进程的模型
1.fork子进程,父进程退出
子进程继承了父进程的进程组id,但具有一个新的进程id,这样就保证了子进程不是一个进程组的组长id,这对于下面要做的setsid函数的调用是必要的前提条件。
2.子进程调用setsid函数创建会话
该进程称为新会话的首进程,是会话的会长。
成为一个新进程组的组长进程,是进程组组长
不受终端的影响
3.改变当前工作目录chdir
如:a.out在U盘上,启动这个程序,这个程序的当前的工作目录就是这个u盘,如果u盘拔掉后进程的当前工作目录将消失,a.out将不能正常工作。
4.重设文件掩码
子进程会继承父进程的掩码
增加子进程操作的灵活性
5.关闭文件描述符
守护进程不受终端的影响所以可关闭,以释放资源
close(STDIN_FILENO)
close(STDOUT_FILENO)
close(STDERR_FILENO)
6.守护进程的核心代码逻辑

线程
轻量级的进程,在Linux环境下线程的本质仍是进程
进程:拥有独立的地址空间,拥有PCB,相当于独居
线程:有PCB,但没有独立的地址空间,多个线程共享进程空间,相当于合租

在这里插入图片描述
线程最小执行单位,进程分配资源的最小单位。实际上,无论是创建进程的fork还是创建线程的pthred_create,底层都是调用同一个内核函数clone。
如果复制对方的地址空间,就会产生一个进程
如果共享对方的地址空间,就会产生一个线程

线程共享资源:
文件描述符表
每种信号的处理方式
当前工作目录
用户ID和组ID
内存地址空间
线程非共享资源:
线程ID
errno变量
用户空间栈
处理器现场和栈指针
信号屏蔽字
调度优先级
线程的优点:提高程序的并发性,开销小,数据通信,共享数据方便
缺点:库函数不稳定,gdb调试编写困难,对信号支持不好

pthread_create:创建一个新线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void (*start_routine) (void *), void *arg);
返回值
成功,返回0
失败,返回错误号
函数参数:
pthread_t:传出参数,保存系统为我们分配好的线程ID
当前Linux中可理解为:typedef unsigned long int pthread_t。
attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
start_routine:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
arg:线程主函数执行期间所使用的参数。
注意点:
pthread_create的错误码不在于errno中,因此不能用perror()打印错误信息,可以先用sterror()把错误码的转换成错误信息在打印
如果一个线程调用exit,则整个进程的所有线程都终止,由于从main函数return也相当于exit,为了防止创建的线程没有得到执行就终止,可以在main函数renturn之前延时1s。

pthread_exit:使一个线程退出,如果主函数调用pthread_exit函数不会让整个进程退出,不影响其他线程的执行
void pthread_exit(void *retval);
retval表示线程退出状态,通常传为NULL

pthread_join:阻塞等待线程退出,获取线程退出状态,其作用,对应进程中的waitpid()函数
int pthread_join(pthread_t thread, void **retval);
函数返回值:
成功:0;
失败:错误号
函数参数:
thread:线程ID
retval:存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址。

pthread_detach:线程分离状态,线程主动与主控线程断开关系,线程结束后,其退出状态不由其他线程获取,而直接自己手动释放。
进程若有该机制,则不会产生僵尸进程,僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源存在于系统中,导致内核认为该进程存在。
int pthread_detach(pthread_t thread);
函数返回值
成功:0;
失败:错误号
一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立即回收它的所有资源,而不保留终止状态。不能对于一个已经detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。

pthread_cancel:杀死取消线程
int pthread_cancel(pthread_t thread);
返回值
成功:0
失败:错误号
线程的取消并不是实时的,而是有一定的延时。需要等待线程到达某个取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置,通常是一些调用。可粗略认为一个系统调用就是一个取消点。

pthread_equal:判断两个线程ID是否相等
int pthread_equal(pthread_t t1, pthread_t t2);

进程函数和线程函数对比:
进程 线程
fork pthread_create
exit pthread_exit
wait/waitpid pthread_join
kill pthread_cancel
getpid pthread_self

线程属性
Linux下线程属性可以根据实际项目需要,进行设置,之前讨论的线程都是采用线程的默认属性,默认属性可以解决大多数开发时遇到问题,如果对线程的性能提出更高的要求,则需要设置线程属性。本节以设置线程的分离属性为例设置讲解线程属性。
线程的分离状态决定一个线程以什么样方式来终止自己,有两种状态:
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
设置线程属性分为一下步骤:
第1步:定义线程属性类型的变量
pthread_attr_t attr;
第2步:对线程属性变量进程初始化
int pthread_attr_init(pthread_attr_t * attr);
第3步:设置线程为分离属性
int pthread_attr_setdetachstate(pthread_attr_t * attr,int detachstate);
参数:
attr:线程属性
detachstate:PTHREAD_CREATE_DETACHED(分离)
PTHREAD_CREATE_JOINABLE(非分离)
pthread_create创建线程
第4步:释放线程属性资源
int pthread_attr_destroy(pthread_attr_t * attr);

线程同步:
线程同步指一个线程发出某一功能调用时,在没有得到结果前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
数据混乱的原因:
资源共享(独享资源则不会)
调度随机(线程操作共享资源的先后顺序不确定)
线程间缺乏必要的同步机制

以上3点中,前两点不能改变,要提高效率,传递数据,资源必须共享,只要共享资源,就一定会出现竞争,只要存在竞争关系数据就很容易出现混乱。所以只能从第三点解决问题,使多个线程在访问共享资源时候出现互斥。
如何解决问题:
原子操作要么做要么不做。
使用互斥锁来模拟原子操作。
Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
在这里插入图片描述
线程1访问共享资源时候要先判断锁是否锁着,如果锁着就阻塞等待,若锁是解开的就加锁,此时访问共享资源,访问完成后解锁,其他线程有机会访问共享资源,
在同一时刻只有一个线程拥有该锁,只要该线程未完成操作就不释放锁,使用互斥锁之后,两个线程由并行操作变成了串行操作,效率降低,但是数据不一致的问题得到解决。
互斥锁相关函数
pthread_mutex_t 类型
其本质是一个结构体,为了简化理解,可以当作整型看待。
pthread_mutex_t mutex; 变量mutex只有两种取值1、0。
pthread_mutex_init:初始化一个互斥锁,初值看成1
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
函数参数:mutex传出参数,调用时应传&mutex
attr:互斥锁属性,通常为NULL,设置为默认属性
pthread_mutex_init(&mutex, NULL)

pthread_mutex_destroy:销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_lock:加锁,mutex–
int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_muttex_unlock:解锁,mutex++

加锁和解锁:lock尝试加锁,加锁不成功,线程阻塞,阻塞到持有该互斥变量的其他线程解锁为止
unlock主动解锁,同时将阻塞在该线程上的所有线程全部唤醒,至于哪一个线程优先被唤醒,取决于优先级,调度。默认先阻塞先唤醒。
day9
死锁两种方式:
自己锁自己
线程A拥有A锁,请求获得B锁;线程B拥有B锁,请求获得A锁,这样造成线程A和线程B都不释放自己的锁,而且还想得到对方的锁,从而产生死锁

读写锁:
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

读写锁使用场合
读写锁非常适合于对数据结构读的次数远大于写的情况。
读写锁特性
读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

3 条件变量
条件本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
使用互斥量保护共享数据;
使用条件变量可以使线程阻塞, 等待某个条件的发生, 当条件满足的时候解除阻塞.
条件变量的两个动作:
条件不满足, 阻塞线程
条件满足, 通知阻塞的线程解除阻塞, 开始工作.
条件变量相关函数
pthread_cond_t cond;
定义一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
函数描述:初始化条件变量
函数参数:
cond: 条件变量
attr: 条件变量属性, 通常传NULL
函数返回值:成功返回0, 失败返回错误号
int pthread_cond_destroy(pthread_cond_t *cond);
函数描述: 销毁条件变量
函数参数: 条件变量
返回值: 成功返回0, 失败返回错误号
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
函数描述: 条件不满足, 引起线程阻塞并解锁;
条件满足, 解除线程阻塞, 并加锁
函数参数:
cond: 条件变量
mutex: 互斥锁变量
函数返回值: 成功返回0, 失败返回错误号
int pthread_cond_signal(pthread_cond_t *cond);
函数描述: 唤醒至少一个阻塞在该条件变量上的线程
函数参数: 条件变量
函数返回值: 成功返回0, 失败返回错误号

信号量相当于多把锁, 可以理解为是加强版的互斥锁
2 相关函数
定义信号量 sem_t sem;
int sem_init(sem_t *sem, int pshared, unsigned int value);
函数描述: 初始化信号量
函数参数:
sem: 信号量变量
pshared: 0表示线程同步, 1表示进程同步
value: 最多有几个线程操作共享数据

函数返回值:成功返回0, 失败返回-1, 并设置errno值
int sem_wait(sem_t *sem);
函数描述: 调用该函数一次, 相当于sem–, 当sem为0的时候, 引起阻塞
函数参数: 信号量变量
函数返回值: 成功返回0, 失败返回-1, 并设置errno值

int sem_post(sem_t *sem);
函数描述: 调用一次, 相当于sem++
函数参数: 信号量变量
函数返回值: 成功返回0, 失败返回-1, 并设置errno值

int sem_trywait(sem_t *sem);
函数描述: 尝试加锁, 若失败直接返回, 不阻塞
函数参数: 信号量变量
函数返回值: 成功返回0, 失败返回-1, 并设置errno值

int sem_destroy(sem_t *sem);
函数描述: 销毁信号量
函数参数: 信号量变量
函数返回值: 成功返回0, 失败返回-1, 并设置errno值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值