Linux系统编程

第一部分:gdb调试工具和makefile项目管理

一、gdb调试工具

gdb发现逻辑错误,gcc发现语法错误

-g:使用该参数编译可以执行文件,得到调试表。
gcc -g tst.c -o tst

Linux系统编程_第1张图片
启动gdb
Linux系统编程_第2张图片
Linux系统编程_第3张图片

列出源码
Linux系统编程_第4张图片
按l是继续列出
Linux系统编程_第5张图片
设置断点 b 加行号
Linux系统编程_第6张图片
设置完断点运行程序run
Linux系统编程_第7张图片
n/next: 下一条指令(遇到函数会越过函数)
s/step: 下一条指令(遇到函数会进入函数)
两者的区别在碰到函数时会有不同
但是像rand()系统函数无法使用s只能使用n,s无法进入系统函数里面,这时候只能使用n进入下一步或者使用until 加上行号直接蹦到对应行,或者使用finish结束这个局部函数的调用
在这里插入图片描述
Linux系统编程_第8张图片
finish跳出局部函数
Linux系统编程_第9张图片
continue跳到下一个断点如果没有后续断点直接运行结束
Linux系统编程_第10张图片
finish和continue的区别
Linux系统编程_第11张图片

p查看变量的值
Linux系统编程_第12张图片
条件断点
Linux系统编程_第13张图片

向main函数传递参数

一般的做法
Linux系统编程_第14张图片
Linux系统编程_第15张图片
使用gdb传入参数有两种方式,一种是先设置再开始,另一种是run加参数
Linux系统编程_第16张图片
也可以使用run加参数
在这里插入图片描述
使用info b查看断点位置清单
Linux系统编程_第17张图片
ptype:查看变量类型
Linux系统编程_第18张图片

栈帧
Linux系统编程_第19张图片
有时候会遇到当调试进入一个局部函数后,我需要看外面
这时候
用bt列出当前程序正存活着的栈帧。
frame加编号 切换栈帧。
Linux系统编程_第20张图片
添加监视,不用每次都查看,使用display命令
Linux系统编程_第21张图片
使用undisplay加变量的编号取消跟踪
Linux系统编程_第22张图片

二、gdb调试工具总结

gdb发现逻辑错误,gcc发现语法错误
1、基本用法
调试前准备 加上-g得到调试表 例如gcc -g tst.c -o tst
开始调试gdb ./a.out
list: list 1 列出源码。根据源码指定 行号设置断点。
b: b 20 在20行位置设置断点。
run/r: 运行程序
n/next: 下一条指令(遇到函数会越过函数)
s/step: 下一条指令(遇到函数会进入函数) 两者的区别在有函数时会有不同,是已经执行了,但是像rand()系统函数只能使用n,这个使用s是无法进入函数,只能用n进行下一步
p/print:p i 查看变量的值。
continue:继续执行断点后续指令。 回跳到下一个断点,如果没有下一个断点会直接运行完剩下代码
finish:结束当前函数调用。
quit:退出gdb调试。
2、设置main函数命令行参数

set args 后面接参数,然后再开始运行
run 后面接参数

3、栈帧
bt:列出当前程序正存活着的栈帧。
frame: 根据栈帧编号,切换栈帧。
4、其他命令
info b: 查看断点信息表
ptype:查看变量类型。
display:设置跟踪变量
undisplay:取消设置跟踪变量。 使用跟踪变量的编号。
5、开始运行
run和start都可以作为开始
前者蹦到第一个断点处,后者是到程序的开始处
6、结束运行
quit

三、makefile

makefile文章

一个规则

		目标:依赖条件
		(一个tab缩进)命令

		1. 目标的时间必须晚于依赖条件的时间,否则,更新目标

		2. 依赖条件如果不存在,找寻新的规则去产生依赖条件。
		
		ALL:指定 makefile 的终极目标。

两个函数

src = $(wildcard ./*.c): 匹配当前工作目录下的所有.c 文件。将文件名组成列表,赋值给变量 src。  
	src = add.c sub.c div1.c 
obj = $(patsubst %.c, %.o, $(src)): 将参数3中,包含参数1的部分,替换为参数2。 
	$(src)是用的前面的add.c sub.c div1.c ,然后将.c都改成.o,也就是下面这样
	obj = add.o sub.o div1.o
	

用法
Linux系统编程_第23张图片
clear函数
接上面的内容,删除obj代表的那些和a.out
在这里插入图片描述
执行时make clean,但在执行前先加个参数模拟执行,确认无误后再执行make clean
在这里插入图片描述
三个自动变量:

		$@: 在规则的命令中,表示规则中的目标。

		$^: 在规则的命令中,表示所有依赖条件。

		$<: 在规则的命令中,表示第一个依赖条件。如果将该变量应用在模式规则中,它可将依赖条件列表中的依赖依次取出,套用模式规则。

Linux系统编程_第24张图片
模式规则:

第二部分:系统调用/文件I/O

系统调用——内核提供的函数
Linux系统编程_第25张图片

1、open函数

函数原型:

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

open两个参数,路径+打开方式

	int open(char *pathname, int flags)  这个函数需要包含这个头文件	#include <unistd.h>

	参数:
		pathname: 欲打开的文件路径名

		flags:文件打开方式: 需要包含下面头文件	#include <fcntl.h>

			O_RDONLY|O_WRONLY|O_RDWR	O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....

	返回值:
		成功: 打开文件所得到对应的 文件描述符(整数)

		失败: -1, 设置errno	

open两个参数,路径+创建+创建权限

	int open(char *pathname, int flags, mode_t mode)		123  775	

	参数:
		pathname: 欲打开的文件路径名

		flags:文件打开方式:	O_RDONLY|O_WRONLY|O_RDWR	O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....

		mode: 参数3使用的前提, 参2指定了 O_CREAT。	取值8进制数,用来描述文件的 访问权限。 rwx    0664

			创建文件最终权限 = mode & ~umask


 
 

	返回值:
		成功: 打开文件所得到对应的 文件描述符(整数)

		失败: -1, 设置errno

例如
open(“./d.cp”,O_RDONLY | O_CREAT| O_TRUNC, 0644) 如果文件存在那么截断成0,如果文件不存在那么创造文件,并且把文件权限赋值为0644

close函数

int close(int fd);

2、错误处理函数

#include
printf("erron=%d",errno);

通过逻辑条件语句和perror函数结合的错误处理函数

if(fd==-1)
{ 
 perror("打开失败");
 exit(1);  //打开失败则没必要进行下一步 所以直接退出  或者想执行其他的用break
}
  1. 打开文件不存在
  2. 以写方式打开只读文件(打开文件没有对应权限)
  3. 以只写方式打开目录

#include
printf(“erron=%d”,errno);

3、read函数

	ssize_t read(int fd, void *buf, size_t count);

	参数:
		fd:文件描述符

		buf:存数据的缓冲区

		count:缓冲区大小

	返回值:

		0:读到文件末尾。

		成功;	> 0 读到的字节数。

		失败:	-1, 设置 errno

		-1: 并且 errno = EAGIN 或 EWOULDBLOCK, 说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。

4、write函数

	ssize_t write(int fd, const void *buf, size_t count);

	参数:
		fd:文件描述符

		buf:待写出数据的缓冲区

		count:数据大小

	返回值:

		成功;	写入的字节数。

		失败:	-1, 设置 errno

5、缓冲区

系统函数(read、write 函数)常常被称为 Unbuffered I/O。指的是无用户及缓冲区。但不保证不使用内核 缓冲区。

系统函数(read、write)
标库函数(fgetc、fputc)

如果每次读一个字节进行拷贝,比较系统函数和标库函数的效率
Linux系统编程_第26张图片

对文件进行I/O操作,需要将数据从用户区-> 内核区 ->磁盘,从用户区到内核区需要切换 CPU的访问权级;

系统函数:
每次读取一个字节,切换权级,写入一个字节到内核区,大量时间浪费在切换访问权级上;

库函数:
有用户级缓冲区,ubuntu默认4096字节,当缓冲区满了之后,切换权级,写入内核;

很明显由于后者有缓冲区并不是一个一个字节的写入,因此库函数比较快。
对于两者速度的比较,可以使用strace命令跟踪程序执行,查看调用的系统函数;
系统函数并不一定比库函数运行的快,所以如果有库函数尽量使用库函数。

6、缓冲区

Linux系统编程_第27张图片

PCB本质是一个结构体,结构体的成员维护这个进程的状态。

text段-代码段
text段存放程序代码,运行前就已经确定(编译时确定),通常为只读。

rodata段(read-only-data)-常量区
rodata段存储常量数据,比如程序中定义为const的全局变量,#define定义的常量,以及诸如“Hello World”的字符串常量。只读数据,存储在ROM中。
注意:
const修饰的全局变量在常量区;const修饰的局部变量只是为了防止修改,没有放入常量区。
编译器会去掉重复的字符串常量,程序的每个字符串常量只有一份。

data段
data存储已经初始化的全局变量,属于静态内存分配。(注意:初始化为0的全局变量还是被保存在BSS段)
static声明的变量也存储在数据段。

bss段
bss段存储没有初值的全局变量或默认为0的全局变量,属于静态内存分配。执行期间必须将bss段内容全部设为0。

stack段-栈
stack段存储参数变量和局部变量,由系统进行申请和释放,属于静态内存分配。
stack的特点是先进后出,可用于保存/恢复调用现场。

heap段-堆
heap段是程序运行过程中被动态分配的内存段,由用户申请和释放(例如malloc和free)。
申请时至少分配虚存,当真正存储数据时才分配物理内存;释放时也不是立即释放物理内存,而是可能被重复利用。

7、阻塞和非阻塞

产生阻塞的场景: 读设备文件、读网络文件。(读常规文件无阻塞概念)

1、读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。
2、从终端设备或网络读写可能会发生阻塞,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。

阻塞(Block)概念

当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:

  1. 正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
  2. 就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。

fcntl函数

改变已经打开文件的阻塞和非阻塞的属性
比如如果open时并没有设定非阻塞,但是设备已经打开了,这个时候就可以通过fcntl进行设置

#include 
#include 

 int fcntl(int fd, int cmd, ... /* arg */ );
 
	获取文件状态: cmd写成 F_GETFL,获取文件属性不需要额外参数 
		int flgs = fcntl(fd,  F_GETFL);
		返回文件的属性(位图,每一位代表一个属性),用一位表示文件是否阻塞,0 阻塞,1 非阻塞;
		

	设置文件状态: cmd写为 F_SETFL,后面需要跟一个参数 int整数,表示要设置的属性值;
		fcntl(fd,  F_SETFL, flgs);
		返回0

8、lseek函数

打开文件,默认偏移量被设置为0。
lseek函数是可以改变读写一个文件时读写指针位置一个系统调用。
可以调用lseek显式地为一个打开的文件设置其偏移量

off_t lseek(int fd, off_t offset, int whence);
	参数:
		fd:文件描述符

		offset: 偏移量

		whence:起始偏移位置: SEEK_SET/SEEK_CUR/SEEK_END

	返回值:

		成功:较起始位置偏移量

		失败:-1 errno

	应用场景:	
		1. 文件的“读”、“写”使用同一偏移位置。

		2. 使用lseek获取文件大小 
		     lseek(fd, 0, SEEK_END)(跳到最后,返回偏移量)

		4. 使用lseek拓展文件大小:要想使文件大小真正拓展,必须引起IO操作。
			lseek(fd, i, SEEK_END) i为想增长的大小数量,若无IO操作则仍为原大小。 
			 没填东西这些扩展的大小时空洞
			 
			可以使用 truncate 函数,直接拓展文件。int ret = truncate("dict.cp", 250);

			

9、传入传出参数

传入参数:

	1. 指针作为函数参数。

	2. 同常有const关键字修饰。

	3. 指针指向有效区域, 在函数内部做读操作。

传出参数:

	1. 指针作为函数参数。

	2. 在函数调用之前,指针指向的空间可以无意义,但必须有效。

	3. 在函数内部,做写操作。

	4。函数调用结束后,充当函数返回值。

通俗就是传一个指针,在内部进行写,可以充当返回值

传入传出参数:

	1. 指针作为函数参数。

	2. 在函数调用之前,指针指向的空间有实际意义。

	3. 在函数内部,先做读操作,后做写操作。

	4. 函数调用结束后,充当函数返回值。

传入一个有意义的指针,并且是一个传出参数

第三部分:文件存储函数

1、innode

其本质为结构体,存储文件的属性信息。如:权限、类型、大小、时间、用户(ls -l查询出来的信息都放在innode里面)、盘块位置(文件内容存储在磁盘上的位置)……innode也叫作文件属性管理结构,大多数的 inode 都存储在磁盘上。
文件名单独存储,存储文件名的地方还会有innode,这个地方叫dentry(目录项)
创建一个文件的硬链接,它们拥有着相同的innode,只是有不同的dentry
删除一个硬链接只是删除引用innode的链接,当删除所有的硬链接后,innode的引用值为0,但并不会删除磁盘上的内容,如果重建innode数据还会恢复
Linux系统编程_第28张图片

2、dentry

目录项,其本质依然是结构体,重要成员变量有两个{文件名,inode,…},而文件内容(data)保存在磁盘盘块中。

3、stat函数

​ 作用:获取文件信息

#include  
#include  
#include 
int stat(const char *path, struct stat *buf)
	参数一:文件名
	参数二:inode结构体指针(传出参数)

​ 返回值:成功返回0,失败返回-1;

stat结构体内容:

struct stat
{
    dev_t     st_dev;     /* ID of device containing file */文件使用的设备号
    ino_t     st_ino;     /* inode number */    索引节点号 
    mode_t    st_mode;    /* protection */  文件对应的模式,文件,目录等
    nlink_t   st_nlink;   /* number of hard links */    文件的硬连接数  
    uid_t     st_uid;     /* user ID of owner */    所有者用户识别号
    gid_t     st_gid;     /* group ID of owner */   组识别号  
    dev_t     st_rdev;    /* device ID (if special file) */ 设备文件的设备号
    off_t     st_size;    /* total size, in bytes */ 以字节为单位的文件容量   
    blksize_t st_blksize; /* blocksize for file system I/O */ 包含该文件的磁盘块的大小   
    blkcnt_t  st_blocks;  /* number of 512B blocks allocated */ 该文件所占的磁盘块  
    time_t    st_atime;   /* time of last access */ 最后一次访问该文件的时间   
    time_t    st_mtime;   /* time of last modification */ /最后一次修改该文件的时间   
    time_t    st_ctime;   /* time of last status change */ 最后一次改变该文件状态的时间   
};

例子

struct stat sbuf;
int a= stat("./a.c", &sbuf);

	获取文件大小: sbuf.st_size

	获取文件类型: sbuf.st_mode

	获取文件权限: sbuf.st_mode
	
	printf("文件大小 %d",sbuf.st_size);

stat结构体中的st_mode 则定义了下列数种情况:

    S_IFMT   0170000    文件类型的位遮罩(掩码),可以与stat结构体中的st_mode与运算
    S_IFSOCK 0140000    套接字
    S_IFLNK 0120000     符号连接
    S_IFREG 0100000     一般文件
    S_IFBLK 0060000     区块装置
    S_IFDIR 0040000     目录
    S_IFCHR 0020000     字符装置
    S_IFIFO 0010000     先进先出
​
    S_ISUID 04000     文件的(set user-id on execution)位
    S_ISGID 02000     文件的(set group-id on execution)位
    S_ISVTX 01000     文件的sticky位
​
    S_IRUSR(S_IREAD) 00400     文件所有者具可读取权限
    S_IWUSR(S_IWRITE)00200     文件所有者具可写入权限
    S_IXUSR(S_IEXEC) 00100     文件所有者具可执行权限
​
    S_IRGRP 00040             用户组具可读取权限
    S_IWGRP 00020             用户组具可写入权限
    S_IXGRP 00010             用户组具可执行权限
​
    S_IROTH 00004             其他用户具可读取权限
    S_IWOTH 00002             其他用户具可写入权限
    S_IXOTH 00001             其他用户具可执行权限
​
    上述的文件类型在POSIX中定义了检查这些类型的宏定义:
    S_ISLNK (st_mode)    判断是否为符号连接
    S_ISREG (st_mode)    是否为一般文件
    S_ISDIR (st_mode)    是否为目录
    S_ISCHR (st_mode)    是否为字符装置文件
    S_ISBLK (s3e)        是否为先进先出
    S_ISSOCK (st_mode)   是否为socket
    若一目录具有sticky位(S_ISVTX),则表示在此目录下的文件只能被该文件所有者、此目录所有者或root来删除或改名,在linux中,最典型的就是这个/tmp目录啦。

例如

	struct stat sbuf;
	int a= stat("./a.c", &sbuf);
	if(S_ISREG(sbuf.st_mode))
	{ 
 	 printf("这个是一般文件");
	}else if(S_ISDIR (sbuf.st_mode)){
  	 printf("这个是一个目录");
	}else if(S_ISLNK (sbuf.st_mode)){
  	 printf("这个是一个软链接");
	}

Linux系统编程_第29张图片

穿透符号链接:不看链接这个文件而去看链接指的内容

默认stat函数穿透符号链接
而lstat函数不会穿透符号链接
cat会穿透符号链接
ls不会穿透符号链接

4、lstat函数

将上面的stat函数改为lstat函数,其他的不变

5、access函数、chmod函数、truncate函数

  • access函数:测试指定文件是否存在/拥有某种权限。
  • chmod函数:修改文件访问权限。
  • truncate函数:截断文件长度成指定长度。常用来拓展文件大小,代替lseek。

6、link函数

link 函数专门用来创建硬链接的,功能和 ln 命令一样。它主要做两件事:

  1. 创建一个目录项
  2. inode 结构体引用计数加 1。

这两步是一个原子操作,要么全部失败,要么全部成功。

#include 
int link(const char *oldpath, const char *newpath);
	
	参数: 
 		oldpath: 原始文件名
		newpath: 新的硬链接名

7、unlink函数

删除一个名字(某些情况下删除这个名字所指向的文件)

	#include

	int unlink(const char* pathname);
	
	返回值:调用成功返回0,不成功返回-1.

删除时

  • 如果这个名字是指向这个文件的最后一个链接,并且没有进程处于打开这个文件的状态,则删除这个文件,释放这个文件占用的空间。
  • 如果这个名字是指向这个文件的最后一个链接,但有某个进程处于打开这个文件的状态,则暂时不删除这个文件,要等到打开这个文件的进程关闭这个文件的文件描述符后才删除这个文件。
  • 如果这个名字指向一个符号链接,则删除这个符号链接。
  • 如果这个名字指向一个socket、fifo或者一个设备,则这个socket、fifo、设备的名字被删除,当时打开这些socke、fifo、设备的进程仍然可以使用它们。

unlink的特征

清除文件时,如果文件的硬链接数到0了,没有dentry对应,但该文件仍不会马上被释放。要等到所有打开该文件的进程关闭该文件,系统才会挑时间将该文件释放掉。

unlink的用途

运行一个程序时有时需要创建一些临时文件。如果进程运行过程中突然终止了,而临时文件还没来的及删除,那么就会遗留下很多没用的临时文件,unlink提供了解决这个问题的一种方法。创建一个临时文件后一刻调用unlink删除文件。但是进程还是打开该文件的,所以该临时文件内容依旧是能被访问读和写的。但是进程终止后,该文件内容就会被删除。 ·

8、readlink

读取符号链接文件本身的内容,得到链接所指向的文件名。
ssize_t readlink(const char *path,char *buf,size_t bufsiz);成功返回实际读到的字节数,失败返回-1,设置errno为相应值。
也可以在bash上输入,比如readlink t.soft 会显示这个硬链接执行的位置

9、rename函数

重命名一个文件

int rename(const char *oldpath,const char *newpath);

成功:0,失败:-1,设置errno为相应值。

10、隐式回收

当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统的这一特性称之为隐式回收系统资源。·因此在程序中打开了fd但是没有回收,在程序退出后会进行隐式回收。但不能依赖这个特性。

第四部分:信号

一、信号的概念

信号在我们的生活中随处可见,如:古代战争中挠杯为号;现代战争中的信号弹;体育比赛中使用的信号机枪…他们都有共性:1.简单2.不能携带大量信息 3.满足某个特设条件才发送

1、信号的特质

信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。与硬件中断类似—―异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”"。
所有信号的产生及处理全部都是由内核完成的。

2、与信号相关的事件和状态

1)产生信号

  1. 按键产生,如:Ctrltc-Ctrltz、Ctrl+\w
  2. 系统调用产生,如: kill、raise、abort
  3. 软件条件产生,如:定时器alarm
  4. 硬件异常产生,如:非法访问内存(毁错误)、除0(浮点数例外)、内存对齐出错(总线错误)
  5. 命令产生,如: kill命令

2)递达

递送并且到达进程

3)未决

产生和递归之间的状态。主要是由于阻塞(屏蔽)导致该状态。

4)信号的处理方式:

  • 执行默认动作
  • 忽略(丢弃), 也是一种处理
  • 捕捉, 调用户处理函数.

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

5)阻塞信号集(信号屏蔽字):

将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后) kill -9无法屏蔽

6)未决信号集

1.信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
2.信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
Linux系统编程_第30张图片
但如果加到信号屏蔽字(详细的图往下翻)
Linux系统编程_第31张图片

总结

	概念:
		未决:产生与递达之间状态。  

		递达:产生并且送达到进程。直接被内核处理掉。

		信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)


		阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。

		未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。

3、信号4要素

与变量三要素类似的,每个信号也有其必备4要素,分别是:

  • 编号
  • 名称
  • 事件
  • 默认处理动作。
    Linux系统编程_第32张图片
    Linux系统编程_第33张图片

Linux系统编程_第34张图片
Linux系统编程_第35张图片

4、kill发送信号函数

kill不仅仅局限于杀死,如上面提到的那些信号


int kill(pid_t pid, int sig);
pid:    	> 0:发送信号给指定进程

			= 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。

			< -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员(由于大于0只发一个,这里加个符号表示他那个组的所有成员)。

			= -1:发送信号给,有权限发送的所有进程。

sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。

成功: 0;
失败:-1(ID非法,信号非法,普通用户杀init进程等权级问题),设置 errno

关于pid
Linux系统编程_第36张图片

Linux系统编程_第37张图片

5、alarm函数

设置定时器(闹钟)。在指定seconds 后,内核会给当前进程发送14〉SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。

unsigned int alarm(unsigned int seconds);

谁调用就给谁发,要求多少秒后给自己发信号

返回定时器剩余的秒数或0 返回0是已经过了定时器设置的时间了


常用:取消定时器:alarm(0),返回旧闹钟余下秒数。

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

time 命令 : 查看程序执行时间。 实际时间 = 用户时间 + 内核时间 + 等待时间。 --》 优化瓶颈 IO

6、setitimer函数

设置定时器(闹钟)。可代替alarm函数。精度微秒us,可以实现周期定时。

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

	参数:
	     which:	ITIMER_REAL: 进行自然计时。 ——> SIGALRM

					ITIMER_VIRTUAL: 进行用户空间计时  ---> SIGVTALRM

					ITIMER_PROF: 进行内核+用户空间计时 ---> SIGPROF
		
		new_value:定时秒数

		           类型:struct itimerval {

               				struct timeval {
               					time_t      tv_sec;         /* seconds */
               					suseconds_t tv_usec;        /* microseconds */

           				}it_interval;---> 周期定时秒数(第一次执行后进行每隔这个时间的周期执行)

               				 struct timeval {
               					time_t      tv_sec;         
               					suseconds_t tv_usec;        

           				}it_value;  ---> 第一次执行定时秒数(仅执行一次)  
           			 };

		old_value:传出参数,上次定时剩余时间。
	
		e.g.
			struct itimerval new_t;	
			struct itimerval old_t;	

			new_t.it_interval.tv_sec = 0;
			new_t.it_interval.tv_usec = 0;
			new_t.it_value.tv_sec = 1;
			new_t.it_value.tv_usec = 0;

			int ret = setitimer(&new_t, &old_t);  定时1秒

	返回值:
		成功: 0

		失败: -1 errno


当setitimer()所执行的timer时间到了,会呼叫SIGALRM signal,
itimerval.it_value设定第一次执行function所延迟的秒数,
itimerval.it_interval设定以后每几秒执行function,
若只想延迟一段时间执行function,只要设定 itimerval.it_value即可,
若要设定间格一段时间就执行function,则it_value和it_interval都要设定,否则 funtion的第一次无法执行,就别说以后的间隔执行了。

二、信号操作函数

内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的。

Linux系统编程_第38张图片
因为不允许直接对mask进行操作,因此必须要有个缓冲,如上图所示,然后再和mask进行运算

1、信号集操作函数


	sigset_t set;  自定义信号集。 首先设置一个信号集

	sigemptyset(sigset_t *set);	清空信号集

	sigfillset(sigset_t *set);	全部置1

	sigaddset(sigset_t *set, int signum);	将一个信号添加到集合中

	sigdelset(sigset_t *set, int signum);	将一个信号从集合中移除

	sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中。 在--1, 不在--0

2、sigprocmask函数

读取或修改进程的信号屏蔽字(PCB中)

严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

成功: 0;
失败:-1,设置errno参数:

set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集。

how参数取值:假设当前的信号屏蔽字为mask
1. SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号。相当于mask = mask|set
2. SlG_UNBLOCK:当how设置为此,set表示需要解除屏蔽的信号。相当于mask = mask & ~set3. 
3. SlG_SETMASK:当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set

若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

3、sigpending函数

取当前进程的未决信号集


int sigpending(sigset_t *set); 

  set传出参数。
  
  返回值:
     成功:0;
     失败:-1,设置errno

例子,下面运行后执行ctlc+c会打印0100000
Linux系统编程_第39张图片

#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

void print_set(sigset_t *set)
{
    int i;
    for (i = 1; i<32; i++) {
        if (sigismember(set, i)) 
            putchar('1');
        else 
            putchar('0');
    }
    printf("\n");
}
int main(int argc, char *argv[])
{
    sigset_t set, oldset, pedset;
    int ret = 0;

    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);
    sigaddset(&set, SIGBUS);
    sigaddset(&set, SIGKILL);

    ret = sigprocmask(SIG_BLOCK, &set, &oldset);
    if (ret == -1)
        sys_err("sigprocmask error");

    while (1) {
        ret = sigpending(&pedset);
        print_set(&pedset);
        sleep(1);
    }

    return 0;
}

三、信号捕捉

1、signal函数

注册一个信号捕捉函数,抓信号还是内核来

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

typedef定义了一个类型,函数指针类型

该函数由ANSI定义,由于历史原因在不同版本的 Unix和不同版本的 Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
Linux系统编程_第40张图片

2、sigaction函数

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 参数:
     act:传入参数,新的处理方式。
     oldact:传出参数,旧的处理方式。
 返回值:
   成功: 0;
   失败:-1,设置errno

这里要有信号屏蔽,比如捕捉到一个信号,进入回调函数处理过程,假设回调函数特别长,这时候正好又来了一个信号,由于信号比程序执行优先级高那么前面那个回调函数就没法再执行。sa_flags设置为0将默认屏蔽这个信号。
Linux系统编程_第41张图片

4、信号捕捉特性


	1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask

	2. 捕捉函数执行期间,本信号自动被屏蔽(依赖于sa_flgs = 0).

	3. 捕捉函数执行期间,被屏蔽信号多次发送,如果有多次的话解除屏蔽后只处理其中的一次!

3、内核实现信号捕捉

Linux系统编程_第42张图片

四、sigchld信号

1、sigchld的产生条件

子进程终止时
子进程接收到SIGSTOP信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时

2、借助sigchld信号回收子进程

Linux系统编程_第43张图片
Linux系统编程_第44张图片
阻塞代码
Linux系统编程_第45张图片

五、中断系统调用

Linux系统编程_第46张图片
在这里插入图片描述

第五部分:守护进程、线程

进程组和会话

1、概念和特征

多个进程组成进程组,多个进程组组成会话
Linux系统编程_第47张图片
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID=-第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID==其进程ID
可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。

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

进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。一个进程可以为自己或子进程设置进程组ID

2、创建会话

创建一个会话需要注意以下6点注意事项:
1.调用进程不能是进程组组长,该进程变成新会话首进程(session header)、2.该进程成为一个新进程组的组长进程。
3.需有root权限(ubuntu不需要)
4.新会话丢弃原有的控制终端,该会话没有控制终端5.该调用进程是组长进程,则出错返回
6.建立新会话时,先调用fork,父进程终止,子进程调用setsid()

三ID合一
进程id、进程组id、会话id

3、getsid函数

获取进程所属的会话ID

pid_t getsid(pid_t pid);

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

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

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

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

4、setsid函数

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

pid_t setsid(void);

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

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

Linux系统编程_第48张图片
Linux系统编程_第49张图片

二、守护进程

1、守护进程的概念

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

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

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

2、创建守护进程模型


	1. fork子进程,让父进程终止。

	2. 子进程调用 setsid() 创建新会话

	3. 通常根据需要,改变工作目录位置 chdir(), 防止目录被卸载。

	4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。  022 -- 755	0345 --- 432   r---wx-w-   422

	5. 通常根据需要,关闭/重定向 文件描述符

	6. 守护进程 业务逻辑。while()

chdir(path)改变程序的工作目录
Linux系统编程_第50张图片

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

int main(int argc, char *argv[])
{
    pid_t pid;
    int ret, fd;

    pid = fork();
    if (pid > 0)                // 父进程终止
        exit(0);

    pid = setsid();           //创建新会话
    if (pid == -1)
        sys_err("setsid error");

    ret = chdir("/home/itcast/28_Linux");       // 改变工作目录位置
    if (ret == -1)
        sys_err("chdir error");

    umask(0022);            // 改变文件访问权限掩码

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

    fd = open("/dev/null", O_RDWR);  //  fd --> 0
    if (fd == -1)
        sys_err("open error");

    dup2(fd, STDOUT_FILENO); // 重定向 stdout和stderr
    dup2(fd, STDERR_FILENO);

    while (1);              // 模拟 守护进程业务.

	return 0;
}

三、线程概念

1、线程


	进程:有独立的 进程地址空间。有独立的pcb。	分配资源的最小单位。

	线程:有独立的pcb。没有独立的进程地址空间。	最小单位的执行。

	ps -Lf 进程id 	---> 线程号。LWP  --》cpu 执行的最小单位。

Linux系统编程_第51张图片
Linux系统编程_第52张图片
进程号是cpu分配资源的最小id
线程号是cpu执行的最小单位

2、Linux内核线程实现原理

1、轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
2、从内核里看进程和线程是一样的,都有各自不同的 PCB,但是线程的 PCB中指向内存资源的三级页表是相同的
3、进程可以蜕变成线程
4、线程可看做寄存器和栈的集合
5、在 linux下,线程最是小的执行单位;进程是最小的分配资源单位
Linux系统编程_第53张图片

3、线程共享资源

1.文件描述符表
⒉每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间(.text/.data/.bss/heap/共享库)

注意,信号处理方式是共享的,但对于信号是不独立的,如果有信号传过来,谁抢到谁处理。对于mask不共享

4、线程非共享资源

1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno变量
5.信号屏蔽字6.调度优先级

5、线程优缺点

优点:
1.提高程序并发性
2.开销小
3.数据通信、共享数据方便
缺点:
1.库函数,不稳定
2.调试、编写困难、gdb不支持
3.对信号支持不好

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

四、线程控制原语

1、pthread_self函数获取自己的线程id

获取线程IlD。其作用对应进程中getpid()函数。

pthread_t pthread_self(void);

返回值:
  成功:0;
  失败:!

线程ID: pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)

注意:不应使用全局变量 pthread_t tid,在子线程中通过 pthread_create传出参数来获取线程ID,而应使用pthread_self。

2、pthread_create函数创建一个新线程

创建一个新线程。 其作用,对应进程中 fork()函数。


int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void
*), void *arg);
返回值:
	成功:0;
	失败:错误号  Linux环境下,所有线程特点,失败均直接返回错误号。

参数:
pthread_t:当前Linux 中可理解为: typedef unsigned long int pthread_t;

参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
参数3:函数指针,子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
参数4:线程主函数执行期间所使用的参数。

Linux系统编程_第54张图片
但如果采用地址传递
Linux系统编程_第55张图片
Linux系统编程_第56张图片
由于主程序还是在使用那个i,但i是在一直变化的,所以使用地址查看值时会发生错误,因此需要使用值传递值

3、线程之间的共享

线程之间共享全局变量

【牢记】线程默认共享数据段、代码段等地址空间,常用的是全局变量。而进程不共享全局变量,只能借助mmap

4、pthread_exit函数退出单个线程

将单个线程退出

void pthread_exit(void *retval);

参数: retval表示线程退出状态,通常传NULL

【注意】线程中,禁止使用exit函数,会导致进程内所有线程全部退出。exit(0)表示退出进程。

return:返回到调用者那里去。
pthread_exit():将调用该函数的线程退出
exit:将进程退出。

5、pthread_join函数回收线程

阻塞等待线程退出,获取线程退出状态。其作用,对应进程中 waitpid()函数。

int pthread_join(pthread_t thread, void **retval);

	成功: 0;
	失败:错误号
参数: 
  thread:线程ID  【注意】:不是指针; 
  retval:存储线程结束状态。

6、pthread_detach函数实现线程分离

实现线程分离

int pthread_detach(pthread_t thread);
	成功:0;
	失败:错误号

线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在

7、pthread_cancel函数杀死线程

杀死(取消)线程 其作用,对应进程中kill()函数。

int pthread_cancel(pthread_t thread);

  成功:0;
  失败:错误号

杀死线程不是立即完成必须要到达取消点。

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

可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以在线程函数中加上pthread_testcancel函数自行设置一个取消点。

8、终止线程方式

总结:终止某个线程而不终止整个进程,有三种方法:

  1. 从线程主函数return。这种方法对主控线程不适用,从main 函数 return相当于调用exit。
  2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
  3. 线程可以调用pthread_exit终止自己。

9、函数总结

	pthread_t pthread_self(void);	获取线程id。 线程id是在进程地址空间内部,用来标识线程身份的id号。

		返回值:本线程id


	检查出错返回:  线程中。

		fprintf(stderr, "xxx error: %s\n", strerror(ret));


	int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg); 创建子线程。

		参1:传出参数,表新创建的子线程 id

		参2:线程属性。传NULL表使用默认属性。

		参3:子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
		
		参4:参3的参数。没有的话,传NULL

		返回值:成功:0

			失败:errno


	循环创建N个子线程:

		for (i = 0; i < 5; i++pthread_create(&tid, NULL, tfn, (void *)i);   // 将 int 类型 i, 强转成 void *, 传参。	


	void pthread_exit(void *retval);  退出当前线程。

		retval:退出值。 无退出值时,NULL

		exit();	退出当前进程。

		return: 返回到调用者那里去。

		pthread_exit(): 退出当前线程。


	int pthread_join(pthread_t thread, void **retval);	阻塞 回收线程。

		thread: 待回收的线程id

		retval:传出参数。 回收的那个线程的退出值。

			线程异常借助,值为 -1。

		返回值:成功:0

			失败:errno

	int pthread_detach(pthread_t thread);		设置线程分离

		thread: 待分离的线程id

	
		返回值:成功:0

			失败:errno	

	int pthread_cancel(pthread_t thread);		杀死一个线程。  需要到达取消点(保存点)

		thread: 待杀死的线程id
		
		返回值:成功:0

			失败:errno

		如果,子线程没有到达取消点, 那么 pthread_cancel 无效。

		我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();

		成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。

10、线程和进程函数对比

	线程控制原语					进程控制原语


	pthread_create()				fork();

	pthread_self()					getpid();

	pthread_exit()					exit(); 		/ return 

	pthread_join()					wait()/waitpid()

	pthread_cancel()				kill()

	pthread_detach()
	

五、线程属性

之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的

下面是线程的一些属性

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

线程的属性值不能直接设置,应该先像上面弄mask一样,先创建一个表,然后在这个表中将属性加上,然后在创建线程时将这些属性加上

1、线程属性初始化

注意:应先初始化线程属性,再pthread_create创建线程
最后别忘了销毁这个线程属性

初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);
	成功: 0;失败:错误号


销毁线程属性所占用的资源
int pthread_attr_destroy(pthread_attr_t *attr);
	成功:0;
	失败:错误号

2、线程的分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。

应该根据自己的需要,选择适当的分离状态。

设置线程属性 分离 or 非分离

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

获取线程属性 分离or 非分离

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

Linux系统编程_第57张图片

#include 
#include 
#include 
#include 
#include 

#define SIZE 0x10000

void *th_fun(void *arg)
{
	while (1) 
		sleep(1);
}

int main(void)
{
	pthread_t tid;
	int err, detachstate, i = 1;
	pthread_attr_t attr;
	size_t stacksize;   //typedef  size_t  unsigned int 
	void *stackaddr;

	pthread_attr_init(&attr);		
	pthread_attr_getstack(&attr, &stackaddr, &stacksize);
	pthread_attr_getdetachstate(&attr, &detachstate);

	if (detachstate == PTHREAD_CREATE_DETACHED)   //默认是分离态
		printf("thread detached\n");
	else if (detachstate == PTHREAD_CREATE_JOINABLE) //默认时非分离
		printf("thread join\n");
	else
		printf("thread un known\n");

	/* 设置线程分离属性 */
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

	while (1) {
		/* 在堆上申请内存,指定线程栈的起始地址和大小 */
		stackaddr = malloc(SIZE);
		if (stackaddr == NULL) {
			perror("malloc");
			exit(1);
		}
		stacksize = SIZE;
	 	pthread_attr_setstack(&attr, stackaddr, stacksize);   //借助线程的属性,修改线程栈空间大小

		err = pthread_create(&tid, &attr, th_fun, NULL);
		if (err != 0) {
			printf("%s\n", strerror(err));
			exit(1);
		}
		printf("%d\n", i++);
	}

	pthread_attr_destroy(&attr);

	return 0;
}

六、线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应调用pthread_exit
  2. 避免僵尸线程
    pthread_joinpthread_detach
    pthread_create指定分离属性
    被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
  3. malloc和 mmap申请的内存可以被其他线程释放
  4. 应避免在多线程模型中调用fork除非,马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit(fork后子进程只有当前线程存在其他线程均不存在)
  5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

第六部分:锁、信号量、线程同步

一、同步概念

主旨在协同步调,按预定的先后次序运行。

1、线程同步

线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。

“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。

因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。

2、数据混乱原因

  1. 资源共享(独享资源则不会)
  2. 调度随机(意味着数据访问会出现竞争)
  3. 线程间缺乏必要的同步机制。

以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。
所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

二、互斥量mutex

Linux中提供一把互斥锁mutex(也称之为互斥量)。
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。资源还是共享的,线程间也还是竞争的,
但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。

建议锁!对公共数据进行保护。所有线程应该在访问公共数据前先拿锁再访问。但,锁本身不具备强制性。

1、使用mutex的基本步骤


	pthread_mutex_t 类型。 

	1. pthread_mutex_t lock;  创建锁

	2  pthread_mutex_init; 初始化		1

	3. pthread_mutex_lock;加锁		1--	--> 0

	4. 访问共享数据(stdout)		

	5. pthrad_mutext_unlock();解锁		0++	--> 1

	6. pthead_mutex_destroy;销毁锁

注意事项

		尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)

		互斥锁,本质是结构体。 我们可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)

		加锁: --操作, 阻塞线程。

		解锁: ++操作, 换醒阻塞在锁上的线 你  程。

		try锁:尝试加锁,成功--。失败,返回。同时设置错误号 EBUSY

	初始化互斥量:

		pthread_mutex_t mutex;

		1. pthread_mutex_init(&mutex, NULL);   			动态初始化。

		2. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;	静态初始化。

2、主要应用函数

pthread_mutex_init函数
pthread_mutex_destroy函数
pthread_mutex_lock函数
pthread_mutex_trylock函数
pthread_mutex_unlock函数
以上5个函数的返回值都是:成功返回0,失败返回错误号。

pthread_mutex_t类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。pthread_mutex_t mutex;变量 mutex只有两种取值1、O。

Linux系统编程_第58张图片

Linux系统编程_第59张图片
Linux系统编程_第60张图片

3、加锁与解锁

使用锁时

4、加锁步骤测试

三、死锁


	是使用锁不恰当导致的现象:

		1. 对一个锁反复lock。

		2. 两个线程,各自持有一把锁,请求另一把。

四、读写锁

  1. 锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
  2. 读共享,写独占。
  3. 写锁优先级高。

写正在占着资源时,这时候有读来了,也不会让给读
只有读和写在同一地位时,读优先的地位才会显现出来。

1、读写锁特性:

  1. 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
  2. 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
  3. 读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高时

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

读写锁非常适合于对数据结构读的次数远大于写的情况。

在这里插入图片描述
Linux系统编程_第61张图片

五、条件变量

条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。

1、主要应用函数


pthread_cond_init函数
pthread_cond_destroy函数
pthread_cond_wait函数
pthread_cond_timedwait函数
pthread_cond_signal函数
pthread_cond_broadcast函数
以上6个函数的返回值都是:成功返回o,失败直接返回错误号。
pthread_cond_t类型用于定义条件变量
pthread_cond_t cond;

1) pthread_cond_init函数 初始化一个条件变量

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);2: attr表条件变量属性,通常为默认值,传 NULL即可

也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2) pthread_cond_destroy函数 销毁一个条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

3)pthread_cond_wait函数 阻塞等待一个条件变量

要有个互斥锁当参数,
互斥锁要求是实现已经锁上,这个函数将会释放这个锁,然后等待唤醒
比如生产者和消费者模型,消费者先对容器加锁,发现没有食物了,然后就会阻塞,程序就会停到当前位置,释放所占有的锁,然后让生产者拿着这个锁去放食物,让生产者发出信号把自己唤醒。

Linux系统编程_第62张图片

	int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

	函数作用:
	1.阻塞等待条件变量cond(参1)满足
	2.释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
		1.2.两步为一个原子操作。
	3. 当被唤醒,pthread_cond_wait 函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

4) pthread_cond_timedwait函数 限时等待一个条件变量

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec*restrict abstime);3:参看man sem_timedwait函数,查看struct timespec结构体。
		struct timespec {
			time_t tv_sec;/*seconds*/long tv_nsec;/* nanosecondes*/纳秒
	形参abstime:绝对时间。
		如: time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
		struct timespec t= {1,0};
		pthread_cond_timedwait (&cond,&mutex, &t);只能定时到19701100:00:01(早已经过去)正确用法:
		time_t cur = time(NULL);获取当前时间。
		struct timespec t;定义timespec 结构体变量tt.tv_sec = cur+1;定时1pthread_cond_timedwait (&cond,&mutex,&t);
在讲解setitimer函数时我们还提到另外一种时间类型:
		struct timeval {
			time_t
			tv_sec;/* seconds */秒
			suseconds_t tv_usec;/* microseconds*/微秒
	};

5)pthread_cond_signal函数 唤醒至少一个阻塞在条件变量上的线程

int pthread_cond_signal(pthread_cond_t *cond);

6) pthread_cond_broadcast函数 唤醒全部阻塞在条件变量上的线程

int pthread_cond_broadcast(pthread_cond_t *cond);

Linux系统编程_第63张图片

2、生产者消费者条件变量

Linux系统编程_第64张图片
Linux系统编程_第65张图片
Linux系统编程_第66张图片
Linux系统编程_第67张图片

/*借助条件变量模拟 生产者-消费者 问题*/
#include 
#include 
#include 
#include 

/*链表作为公享数据,需被互斥量保护*/
struct msg {
    struct msg *next;
    int num;
};

struct msg *head;

/* 静态初始化 一个条件变量 和 一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *consumer(void *p)
{
    struct msg *mp;

    for (;;) {
        pthread_mutex_lock(&lock);
        while (head == NULL) {           //头指针为空,说明没有节点    可以为if吗
            pthread_cond_wait(&has_product, &lock);
        }
        mp = head;      
        head = mp->next;                 //模拟消费掉一个产品
        pthread_mutex_unlock(&lock);

        printf("-Consume %lu---%d\n", pthread_self(), mp->num);
        free(mp);
        sleep(rand() % 5);
    }
}

void *producer(void *p)
{
    struct msg *mp;

    for (;;) {
        mp = malloc(sizeof(struct msg));
        mp->num = rand() % 1000 + 1;        //模拟生产一个产品
        printf("-Produce ---------------------%d\n", mp->num);

        pthread_mutex_lock(&lock);
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&lock);

        pthread_cond_signal(&has_product);  //将等待在该条件变量上的一个线程唤醒
        sleep(rand() % 5);
    }
}

int main(int argc, char *argv[])
{
    pthread_t pid, cid;
    srand(time(NULL));

    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);

    return 0;
}

多个消费者那在main函数里面多建几个消费者,然后让其执行自己的程序。
Linux系统编程_第68张图片

3、条件变量的优点

相较于mutex而言,条件变量可以减少竞争。

如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

六、信号量

进化版的互斥锁(1–>N)
由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

1、主要应用函数

sem_init函数
sem_destroy函数
sem_wait函数
sem_trywait 函数
sem_timedwait函数
sem_post函数

以上6个函数的返回值都是:成功返回o,失败返回-1,同时设置errno。(注意,它们没有 pthread前缀)

sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
sem_t sem;规定信号量sem不能<0。头文件

Linux系统编程_第69张图片

1)sem_init函数 初始化一个信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);1: sem信号量
	参2: pshared取o用于线程间;取非0(一般为1)用于进程间
	参3: value指定信号量初值

2)sem_destroy函数 销毁一个信号量

int sem_destroy(sem_t *sem);

3) sem_wait函数 给信号量加锁

int sem_wait(sem_t *sem);

4)sem_post函数 给信号量解锁

int sem_post(sem_t *sem);

5) sem_trywait函数 尝试对信号量加锁

(与sem_wait的区别类比 lock 和 trylock)

int sem_trywait(sem_t *sem);

6)sem_timedwait函数 限时尝试对信号量加锁–

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);2: abs_timeout采用的是绝对时间。

	e.g.定时1:
		time_t cur = time(NULL);获取当前时间。
		struct timespec t;定义timespec结构体变量tt.tv_sec = cur+1;定时1秒
		t.tv_nsec = t.tv_sec +100;
		sem_timedwait(&sem, &t);传参

2、生产者消费者信号量模型

Linux系统编程_第70张图片
在这里插入图片描述
Linux系统编程_第71张图片
Linux系统编程_第72张图片
Linux系统编程_第73张图片

你可能感兴趣的:(#,linux系统编程,linux,unix,网络)