个人通过学习,手打了一份48000字的Linux系统编程的笔记,包含了【文件IO、进程、进程间通信、信号、多线程、互斥】等知识点,并给出了大量的代码案例对每个重要的知识点进行了代码演示,通过理论和实操的结合,更好的透析每个知识点,为后续的unix网络编程打下基础。
本文内容较长,包含的知识点很多,建议使用Ctrl+f 来查找知识点来学习。
内容中的源代码都可以在我的github上下载:https://github.com/jiong1998/Linux-.github.io
注:本人运行环境mac/Ubuntu+clion+C99
目录:
第二章:gcc、库
第三章:makefile、文件IO、PCB概念
第四章:文件IO操作
第五章:进程
第六章:进程间通信
第七章:信号
第八章:多线程编程、互斥、条件变量、信号量
linux如何解压文件
Vi有三种基本工作模式: 命令模式、文本输入模式、末行模式。
通过shell命令进入命令模式。
从hello.c到可执行文件实际上有四步
库是二进制文件, 是源代码文件的另一种表现形式, 是加了密的源代码; 是一些功能相近或者是相似的函数的集合体.
需要两个文件,用户才能使用:
注意: 库不能单独使用, 只能作为其他执行程序的一部分完成某些功能, 也就是说只能被其他程序调用才能使用.
静态库可以认为是一些目标代码的集合, 是在可执行程序运行前就已经加入到执行码中, 成为执行程序的一部分. 按照习惯, 一般以.a做为文件后缀名。静态库文件一般由多个.o文件制作成一个.a文件,具体看下一小节静态库的制作
所以最终的静态库的名字应该为:libtest.a
注意:一个.o文件也能看作是库文件,是库文件的一个特例,库文件一般由多个.o文件构成
gcc -c fun1.c fun2.c
//可以不加-o默认生成对应的.o文件
ar rcs libtest1.a fun1.o fun2.o
gcc -o main1 main.c -I./ -L./ -l test1
gcc -o main1 main.c -I./include -L./lib -l test1
静态库是.a结尾,动态库则是以.so结尾,还没学,略。
makefile文件中定义了一系列的规则来指定, 哪些文件需要先编译, 哪些文件需要后编译, 哪些文件需要重新编译, 甚至于进行更复杂的功能操作, 因为makefile就像一个Shell脚本一样, 其中也可以执行操作系统的命令. makefile带来的好处就是——“自动化编译”, 一旦写好, 只需要一个make命令, 整个工程完全自动编译, 极大的提高了软件开发的效率。 makefile文件是用来管理项目工程文件,通过执行make命令,make就会自动解析并执行makefile文件。
makefile由一组规则组成,规则如下:
目标(要生成的目标文件): 依赖(生成目标所依赖的文件)
(tab)命令
makefile基本规则三要素:
例如
main: main.c fun1.c fun2.c
gcc -o main main.c fun1.c fun2.c
要想生成目标文件,先要检查依赖条件是否都存在:
在makefile中使用变量有点类似于C语言中的宏定义, 使用该变量相当于内容替换, 使用变量可以使makefile易于维护, 修改起来变得简单。
makefile有三种类型的变量:
模式规则:
%.0:%.c //前后的%代表字符串,必须相同。
比如:main.o:main.c fun1.o: fun1.c fun2.o:fun2.c, 说的简单点就是: xxx.o:xxx.c(%.0:%.c)
标记一下,没学。对应07-makefile的第五个版本。
一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序, 首先在编译时, 我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的 -g 参数可以做到这一点。如:
gcc -g hello.c -o hello
GDB 可以打印出所调试程序的源代码, 当然, 在程序编译时一定要加上 -g 的参数, 把源程序信息编译到执行文件中。不然就看不到源程序了。当程序停下来以后, GDB会报告程序停在了那个文件的第几行上。你可以用list命令来打印程序的源代码, 默认打印10行, list命令(可以简写成l) 的用法如下所示:
一般是打印当前行的上5行和下5行, 如果显示函数是是上2行下8行, 默认是10行, 当然, 你也可以定制显示的范围, 使用下面命令可以设置一次显示源程序的行数。
简单断点—当前文件:
多文件设置断点—其他文件:
查询所有断点:
条件断点:
一般来说, 为断点设置一个条件, 我们使用if关键词, 后面跟其断点条件。设置
维护断点
查看运行时变量的值:
自动显示变量的值:
你可以设置一些自动显示的变量, 当程序停住时, 或是在你单步跟踪时, 这些变量会自动显示。相关的GDB命令是display。
你可以使用 set var 命令来告诉GDB, width不是你GDB的参数, 而是程序的变量名, 如:
set var width=47 // 将变量var值设置为47
在你改变程序变量取值时, 最好都使用set var格式的GDB命令。
C语言使用fopen函数打开一个文件, 返回一个FILE* fp, fp指向堆空间,堆空间存放的不是文件本身,而是文件的描述信息的结构体。
这个指针指向的结构体有三个重要的成员:
用户区的内存4区模型:
代码段:.text段。 程序源代码(二进制形式)。
数据段:只读数据段 .rodata段。初始化数据段 .data段。 未初始化数据段 .bss 段。
stack:栈。 在其之上开辟 栈帧。 windows 1M — 10M Linux: 8M — 16M
heap:堆。 给用户自定义数据提供空间。 约 1.3G+
进程的虚拟地址空间分为用户区和内核区, 其中内核区是受保护的, 用户是不能够对其进行读写操作的;
内核区中很重要的一个就是进程管理, 进程管理中有一个区域就是PCB(本质是一个结构体),称进程控制块;
PCB中有文件描述符表, 文件描述符表中存放着打开的文件描述符, 涉及到文件的IO操作都会用到这个文件描述符.
每一个进程有一个PCB。PCB是一个结构体,其中有一项是文件描述符表,文件描述符表中存放着打开的文件描述符。
一个进程默认会打开三个文件:标准输入,标准输出,标准出错,对应index:0,1,2。每当进程每打开一个文件,就会在文件描述符表上记录一个文件描述符。
强调:文件描述符的作用:通过文件描述符可以找到文件的inode, 通过inode可以找到对应的数据块。
头文件
#include
#include
open函数常这么用:
int fd = open("test.log", O_RDWR | OCREAT, 0755);
需要说明的是,当一个进程终止时, 内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close, 在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器), 打开的文件描述符一定要记得关闭, 否则随着打开的文件越来越多, 会占用大量文件描述符和系统资源。
头文件
int newfd = dup(fd);//此时newfd和fd这两个文件描述符都指向同一个文件
注意:当调用dup后,内核会维护描述符计数,close一个文件描述符时,计数-1,只有减到0时,文件才真正的关闭。
假设newfd已经指向了一个文件,首先close原来打开的文件,然后newfd指向oldfd指向的文件.
若newfd没有被占用,newfd指向oldfd指向的文件.
调用dup2后,内核会修改内部的描述符计数为2。
7.5 重定向操作:
dup和dup2都可以实现重定向操作:例如printf的时候可以不把内容输出到终端,而是输出到指定的文件上。
int main()
{
//dup实现文件重定向操作
//打开文件
int fd = open("./test123.txt", O_RDWR | O_CREAT, 0755);
if(fd<0)
{
perror("open error");
return -1;
}
//调用dup实现文件重定向
close(STDOUT_FILENO);
dup(fd);
/* 我们先关闭文件描述符STDOUT_FILENO(其值是1),然后调用dup(fd),
因为dup总是返回系统最小可用的fd,所以他的返回值实际上是1,
也就是之前关闭的标准输出,所以会将输出的内容输出到文件上 */
printf("你好宝贝");
return 0;
}
//dup2实现文件重定向操作
int main()
{
//打开文件
int fd = open("./test123.txt", O_RDWR | O_CREAT, 0755);
if(fd<0)
{
perror("open error");
return -1;
}
//调用dup2实现文件重定向
dup2(fd, STDOUT_FILENO);
printf("你好宝贝");
return 0;
}
实现文件重定向原理
所以当调用dup2时,会把STDOUT_FILENO指向其他文件,所以printf会把内容输出到对应文件。
头文件
fcntl函数常用的操作:
fcntl常用函数:
//获得和设置文件描述符的flag属性:
int flag = fcntl(fd, F_GETFL, 0);
flag |= O_APPEND;//末尾追加
fcntl(fd, F_SETFL, flag);
//获取和设置文件描述符为非阻塞
int flags = fcntl(fd[0], F_GETFL, 0);
flag |= O_NONBLOCK;//非阻塞
fcntl(fd[0], F_SETFL, flags);
#include
buf为传出参数,具体如下所示
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; //最后一次改变时间(指属性)
};
stat函数和lstat函数的区别
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
PCB中有很多信息:主要有
头文件
fork函数的作用:产生子进程
fork函数一次调用,两次返回:父进程返回子进程的ID,子进程返回0
调用fork函数的内核实现原理:除了修改内核区中PCB的进程id外,其他全部复制一份到子进程.(最重要的是,文件描述符也会一同复制过去)
fork函数总结:
1.fork函数的返回值:
pid_t pid=fork()
if(pid<0)
{
printf("创建失败");
exit(1);
}
if(pid==0)//子进程执行的片段
{
pid=getpid();//获取当前进程的pid
pid=getppid();//获取当前进程的父进程pid
}
if(pid>0)//父进程执行的片段
{
yyy;
}
当父进程比子进程先结束时,子进程的父进程pid会变成1,由init进程进行领养。
如标题所示,原因是:会把父进程的用户区完全copy一份给子进程,所以子进程也有自己的全局变量。而且,如果共享的话,还要进程间通信干嘛?
写时复制,读时共享:
所以父子进程不能通过全局变量进程通信!如果父子进程想要通信,必须借助其他工具。
ps:查看进程相关信息。
ps aux | grep “xxx”//grep 关键字:可以查看启动信息中包含关键字的进程
ps ajx | grep “xxx”
kill:用于向运行中的进程发送信号,默认发送的信号是终止信号,会请求进程退出。不一定是杀死进程,也可以发送其他信号。
kill -l 查看系统有哪些信号
kill -9 pid 杀死某个线程
exec:想在一个进程中:执行一个应用程序或者想执行一个系统命令,应先fork,再在子进程中执行execl拉起可执行程序或者命令。
pid = fork()
if(pid==0)//让子进程执行
{
int excel (path,"ls", "-l", NULL)
//调用execl函数后,子进程的代码段会被ls命令代码段替换。
//注意:子进程的地址空间没有变化 ,子进程的PID 也没有变化。
}
execl实现原理:
当调用子进程使用execl时,子进程的地址空间中的代码段(.txt)、数据段,堆和栈等将被替换成execl中所执行的新进程。原有的进程空间没有发生变化,并没有创建新的进程,进程PID没有发生变化。
exec有六大函数,记住两个主要的函数:
一般execl用于自己写的一个应用程序
函数原型:
int execl(“绝对路径”, “标识符”, “参数1”,“参数2” ,NULL);
excel("./test", "test", "Hello","world", NULL);
excel("/bin/ls", "ls", "-l", NULL);
参数介绍:
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;
若是失败,会执行execl 后面的代码,可以用perror打印错误原因。
一般execlp用于执行系统命令
函数原型:
int execlp(“命令名/程序名”,“标识符”, “参数1”,“参数2”, NULL);
execlp("ls", "ls", "-l", NULL);
参数介绍:
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行
当一个子进程退出之后,子进程只能回收自己的用户区的资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程资源的回收,避免造成系统资源的浪费。
若父进程先退出,子进程就变成孤儿进程。
并且为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。
子进程先退出,父进程没有完成对子进程的回收(回收内核区资源),此时子进程变成僵尸进程。
如何解决僵尸进程:
使用杀死僵尸进程父进程的方法来解决僵尸进程。原因是:杀死其父进程可以让init进程领养僵尸进程,最后由init进程回收僵尸进程.
头文件
所有的进程回收函数都必须由父进程完成!!
函数原型:pid_t wait(int *status);
函数作用:
返回值:
status参数:传出参数
如果对状态不关心,就传NULL就行。
pid_t pid=fork()
if(pid>0)//父进程执行
{
printf("this is father pid=%d", getpid());
pid_t w_pid=wait(NULL);//在父进程执行wait函数进行子进程的资源回收
}
if(pid==0)//子进程执行
{
printf("this is son pid=%d", getpid());
}
函数原型:pid_t waitpid(pid_t pid, int *status, in options);
函数作用:
同wait函数。但是比wait函数灵活,可以控制是否阻塞父进程。
函数参数:
返回值:
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问, 要交换数据必须通过内核 。具体来说,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
在进程间完成数据传递需要借助操作系统提供特殊的方法,现今常用的进程间通信方式有:
管道应用于有血缘关系的进程之间。
管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,进行数据传递。调用pipe函数即可创建一个管道。
管道的特点
注意:父进程创建管道,子进程会继承。
原理:
局限性:
为什么管道只能在有血缘关系的进程间使用:
管道的两端是通过文件描述符表示的,如果是两个毫不相关的进程,在一个进程内创建了管道,没办法让另一个进程获得该管道的文件描述符。但是在父子进程中就可以。
头文件:
用法:
int fd[2];
int result=pipe(fd);
函数调用成功返回读端和写端的文件描述符,向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区。对管道的读写操作其实就有点像对文件的操作。
管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?
创建管道通信的步骤:
注意 管道的读端是阻塞的,如果没有数据,会阻塞到有数据才运行。
管道的写端是阻塞的,如果数据满了,会阻塞到可以写数据时才运行。
//管道通信的简单案例
#include
#include
#include
#include
#include
#include
//利用管道实现父子间的进程通信
int main() {
int fd[2];
int result= pipe(fd);
if(result<0)
{
perror("pipe error");
return -1;
}
pid_t pid=fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid>0)//父进程
{
//关闭管道读端
close(fd[0]);
//向管道写入数据
write(fd[1],"Hello world", sizeof("Hello world"));
wait(NULL);
// waitpid(-1,NULL,0);
}
else//pid==0,子进程
{
//关闭管道写端
close(fd[1]);
//向管道读取数据
char buf[100];
//初始化buf
memset(buf,0x00,sizeof(buf));
int n = read(fd[0], buf, sizeof(buf));
printf("read over,n==[%d], text=[%s]", n, buf);
}
return -1;
}
在父子进程间完成ps aux | grep bash命令,ps aux在父进程执行,grep bash在子进程执行。
这个代码需要运用到不同的知识点。
具体需求分析如下:
具体重定向分析:
整体流程:
#include
#include
#include
#include
#include
#include
#include
//实现ps aux|grep bash
int main() {
// 1. 开通管道
int fd[2];
int result=pipe(fd);
if(result<0)
{
perror("pipe error");
return -1;
}
//2. 创建子进程
pid_t pid=fork();
if(pid<0)
{
perror("pipe error");
return -1;
}
if(pid>0)//父进程
{
//父进程关闭读端
close(fd[0]);
//标准输出重定向到管道写端
dup2(fd[1],STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);
perror("execlp error");//当execlp执行成功,则不会执行后续代码。若执行失败,打印错误
wait(NULL);
}
if(pid==0)//子进程
{
//子进程关闭写端
close(fd[1]);
//标准输入重定向到管道读端
dup2(fd[0],STDIN_FILENO);
execlp("grep","grep","bash", NULL);
perror("execlp error");
}
return 0;
}
对于读端而言:
对于写端而言:
由之前学的可知,管道读写两端是默认阻塞的,可以通过fcntl(fd[0], F_SETFL, flags)函数,将管道的读写两端设置成为非阻塞
int test1()
{
int fd[2];
int result= pipe(fd);
if(result<0)
{
perror("pipe error");
return -1;
}
//设置为非阻塞
int flags=fcntl(fd[0], F_GETFL,0);
flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
char buf[100];
memset(buf,0x00,sizeof(buf));
//此时管道没有写入数据,如果没有设置成非阻塞,进程将一直阻塞在这
read(fd[0],buf, sizeof(buf));
return -1;
}
头文件:
#include
#include
FIFO常被称为命名管道。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种(文件类型为p,可通过ls -l查看文件类型)。但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信。
FIFO 不同于PIPE的地方在于,他是双向的,两端都可读可写。
int result=mkfifo("./myfifo", 0777);
当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可用于FIFO。如:close、read、write、unlink等。
FIFO严格遵循先进先出(first in first out),对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。FIFO不支持诸如lseek()等文件定位操作。
创建完FIFO后,就和普通文件一样操作就行:一个进程写,另一个进程读。
具体代码案例:
FIFO_write.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//写入FIFO
int main()
{
//判断FIFO是否存在,如果不存在则创建
int result= access("./myfifo1", F_OK);
//access函数用来判断文件是否存在
if(result!=0)//不存在该文件
{
result= mkfifo("./myfifo1",0777);
if(result<0)
{
perror("mkfifo error");
return -1;
}
}
int fd= open("./myfifo1", O_RDWR);
//写fifo文件
write(fd,"Hello world", strlen("Hello world"));
sleep(20);
//sleep是因为要让read进程读到FIFO数据后在关闭,不然读不到。
close(fd);
}
FIFO_read.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//从FIFO中读
int main()
{
int fd= open("./myfifo1", O_RDWR);
if(fd<0)
{
perror("open error");
return -1;
}
//读fifo文件
char buf[100];
memset(buf,0x00,sizeof(buf));
read(fd,buf, sizeof (buf));//如果没有数据会阻塞到有数据
printf("buf=[%s]", buf);
close(fd);
}
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与内存中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。即操作内存=操作文件
使用存储映射这种方法,首先应通知内核,将一个指定文件映射到内存区域中。这个映射工作可以通过mmap函数来实现。
头文件
头文件
具体代码案例看我的github中linux区的issues。
附上网址:https://github.com/jiong1998/Linux-.github.io/issues/7
匿名映射:不需要文件,但是只能用于父子间进程通信。(悄悄问:那为什么不用PIPE?)
mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED |
MAP_ANONYMOUS , -1, 0);
不需要指定fd和偏移量。
进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕后再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。
每个进程收到的所有信号,都是由内核负责发送的
注意:信号相关的内容都保存在内核区当中的PCB中
几个常用到的信号:
SIGINT、SIGQUIT、SIGKILL、SIGSEGV、SIGUSR1、SIGUSR2、SIGPIPE、SIGALRM、SIGTERM、SIGCHLD、SIGSTOP、SIGCONT
2)SIGINT:Ctrl-C发出
3)SIGQUIT:Ctrl-\发出
10)SIGUSR1:用户自定义信号1
12)SIGUSR2:用户自定义信号2
14)SIGALRM:定时器到点后发送的信号
17)SIGCHLD:子进程退出后,内核会给父进程发送该信号。
信号有三种状态:产生、未决和递达。
信号的处理方式有三种:
注意:SIGKILL和SIGSTOP不能捕获,不能阻塞,不能忽略。只能执行他们的默认动作。SIGKILL的默认动作是终止,SIGSTOP的默认动作是暂停。
不要使用信号来完成进程间的通信!!
看下面第四节
头文件 #include
kill分为kill命令和kill函数,这里讲的是kill函数。
注意:父进程可以给子进程发送信号,子进程也可以给父进程发送信号,因此子进程可以杀死父进程( kill(getppid(), SIGKILL) )。
头文件 #include
注意:alarm(0)取消闹钟,返回旧闹钟剩下的秒数
#include
具体使用看代码案例:
//设置一个周期性2s发送一次时钟信号
struct itimerval tm;
//触发周期赋值
tm.it_interval.tv_sec=2;//每隔两秒触发一次
tm.it_interval.tv_usec=0;//微秒,不管他
//第一次触发事件赋值
tm.it_value.tv_sec=3;//三秒后第一次触发
tm.it_value.tv_usec=;//微秒,不管他
setitimer(ITIMER_REAL, &tm, NULL);
未决信号集:没有被处理的信号的集合(假设产生了某信号,如果此时该信号存在于阻塞信号集中,则会被放入未决信号集)
阻塞信号集:被当前进程阻塞的信号的集合
这两个集合都存储在内核的PCB。
未决信号集是否应该被处理取决于阻塞信号集对应标志位是1还是0,当这个信号被处理之前,先检查阻塞信号集对应标志位:
如果是1,说明该信号被阻塞,暂不处理;
如果是0说明该信号没有被阻塞,可以处理。 处理完后未决信号机的这个标识位从1变为0;表示这个信号已经被处理了(抵达)。
处理有三种方式:1默认 2被当前进程捕获 3忽略。
一个标志位代表序号为x的信号,比如标志位为9代表SIGKILL信号。
注意:只有当阻塞信号集中某个信号x置为1了,当进程在运行过程中碰到x信号,该信号才会进未决信号集。否则会直接处理。
头文件
信号集变量 sigset_t set;
由于信号集属于内核的一块区域,用户不能直接操作内核空间,为此,内核提供了一些信号集相关的接口函数,使用这些函数用户就可以完成对信号集的相关操作。
信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。既然是一个集合,就需要对集进行添加、删除等操作。
注意前面这些函数都没有操作内核的信号集,只是在进程自己的栈上定义了一个信号集,并对自己定义的信号集的处理,处理完要通过下面两个函数才能对内核区的信号集进行真正的处理
sigprocmask函数(操作内核中的阻塞信号集)
sigpending函数 (读取当前进程的未决信号集)
//sigprocmask简单示范:
//需求:将SIGINT和SIGQUIT信号的加入到阻塞信号集
//创建信号集
sigset_t set
//初始化信号集
sigemptyset(&set);
//将SIGINT和SIGQUIT加入set集合中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
//将信号集set中SIGINT和SIGQUIT信号的加入到阻塞信号集中
sigprocmask(SIG_BLOCK,&set, NULL);
信号集代码相关案例请移步github:设置阻塞信号集并把所有常规信号的未决状态打印至屏幕 #9------https://github.com/jiong1998/Linux-.github.io/issues/9
头文件 #include
void handler(int signum);//信号处理函数
sighandler_t signal(int signum, sighandler_t handler);//注册信号处理函数
注意:内核执行的信号处理函数,不是进程执行。
额外:设置忽略某个信号
signal(SIGPIPE, SIG_IGN);
signal不同的unix版本动作会不同,用sigaction代替
sigaction结构体介绍:
struct sigaction
{
// 信号处理函数。可赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作
void (*sa_handler)(int);
//信号处理函数(不用,不管他)
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //信号处理函数执行期间需要阻塞的信号
int sa_flags; //通常为0,表示使用默认标识
void (*sa_restorer)(void);//废弃
};
sigaction函数结论:
1. 若a信号处理函数执行期间,又产生了多次a信号,信号a本身会被阻塞,信号处理函数不会被打断,当信号处理函数执行完后,后面产生的多次信号a也只会被处理一次,即信号不支持排队。
2. 在a信号处理函数执行期间(并且sa_mask中阻塞了b信号),若此时收到了b的信号,则b信号会被阻塞,当a信号处理函数执行完后,才会转去执行b信号处理函数(若收到多次b信号,也只会执行一次)。
sigaction用法示例:
//sigaction函数测试:完成信号的注册
#include
#include
#include
#include
#include
#include
void sighandle(int signum)
{
printf("signum=%d", signum);
}
int main()
{
struct sigaction act;
act.sa_handler=sighandle;//信号处理函数
//设置sa_mask:在执行信号处理函数时需要阻塞的信号
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,SIGQUIT);//在信号处理函数执行期间,阻塞SIGQUIT信号
act.sa_flags=0;
//利用sigaction注册信号捕捉函数
sigaction(SIGINT,&act,NULL);
//利用signal注册信号捕捉函数
signal(SIGINT, sighandle);
return 0;
}
另外:在使用sigaction函数的时候,可以:
act.flags = SA_RESTART
原因:
比如有些系统调用函数:read、write、accept在阻塞期间若收到信号,会被信号中断,解除阻塞返回-1,error设置为EINTR。而这样的错误不应该被看成是错误。
而这么设置的效果是:若当前进程阻塞在一个系统调用上,这时来了一个信号,前边注册过的并且sa_flags |= SA_RESTART,那么当信号处理完之后,这个阻塞的系统调用会继续执行,而不是被打断。
额外:设置忽略某个信号
signal(SIGPIPE, SIG_IGN);
当子进程退出时,内核会给父进程发送SIGCHLD信号。(或者子进程收到SIGSTOP信号;又或者子进程停止时,收到SIGCONT信号)
SIGCHLD信号作用:
SIGCHLD信号的默认动作是忽略。若父进程没有捕获该信号,则会使得子进程变成僵尸进程。
需求分析:
父进程创建出三个子进程,并且,父进程通过接收SIGCHLD信号完成对子进程的回收。
原始的版本不放出来,需要的移步去github:https://github.com/jiong1998/Linux-.github.io/issues/11
原始的版本存在的问题:
问题1:
在信号函数注册之前,子进程可能已经开始运行并退出了。此时由于信号函数还没有注册,会导致退出的进程成为僵尸进程。
解决办法:
在fork()之前,先将SIGCHLD信号阻塞,完成信号处理函数的注册后,在解除阻塞。此时如果有SIGCHLD信号产生,会放入未决信号集。(更简单的方法是把信号注册函数放在fork函数之前,但是这会让子进程继承,既然子进程没有子子进程,就没有必要继承)
问题2:
前面提到过,信号不支持排队,所以在执行信号处理函数回收子进程的时候,如果有2个及以上的子进程同时给父进程发送SIGCHLD信号,父进程也只会回收一次!会导致存在僵尸进程。
解决办法:
可以在信号处理函数里面使用while(1)循环回收, 这样就有可能出现捕获一次SIGCHLD信号但是回收了多个子进程的情况,从而可以避免产生僵尸进程。
//改进后的父进程通过SIGCHLD回收子进程
//SIGCHLD信号处理函数
void waitchild_new(int signum)
{
//当收到SIGCHLD信号时,使用waitpid函数回收进程
pid_t wpid ;//
while(1)//利用循环回收解决僵尸进程问题
{
wpid=waitpid(-1,NULL, WNOHANG);
if(wpid>0)
{
printf("已利用信号SIGCHLD回收子进程资源,pid=[%d]\n", wpid);
}
if(wpid==0)//目前没有子进程退出
{
printf("还有子进程存活\n");
break;
}
if(wpid==-1)
{
printf("子进程已经全部回收\n");
break;
}
}
}
//创造出三个进程,每个进程执行自己的事。
//当父进程接收到子进程的SIGCHLD信号时,转向执行信号处理函数,对子进程进程资源的释放
int main()
{
int i;
//将SIGCHLD信号阻塞。
//创建信号集
sigset_t set;
//初始化信号集
sigemptyset(&set);
//将SIGCHLD加入set集合中
sigaddset(&set, SIGCHLD);
//将set集合加入到阻塞信号集
sigprocmask(SIG_BLOCK, &set,NULL);
for(i=0;i<3;++i)//循环创建三个子进程
{
pid_t pid=fork();
if(pid < 0)
{
perror("fork error");
return -1;
}
if(pid > 0)//父进程
{
printf("子进程创建成功,子进程pid=[%d]\n", pid);
}
if(pid==0)//子进程
break;
}
if(i==0)//第一个子进程
{
printf("the first child, pid=[%d],ppid=[%d]\n", getpid(), getppid());
sleep(1);//模拟子进程在执行其他事
}
if(i==1)//第二个子进程
{
printf("the second child, pid=[%d],ppid=[%d]\n", getpid(), getppid());
sleep(2);//模拟子进程在执行其他事
}
if(i==2)//第三个子进程
{
printf("the third child, pid=[%d],ppid=[%d]\n", getpid(), getppid());
sleep(2);//模拟子进程在执行其他事
}
if(i==3)//父进程
{
printf("the father, pid=[%d]\n", getpid());
//父进程注册SIGCHLD信号处理函数
struct sigaction act;
act.sa_handler=waitchild_new;//信号处理函数
//设置sa_mask:在执行信号处理函数时需要阻塞的信号
sigemptyset(&act.sa_mask);
act.sa_flags=0;
//注册信号处理函数
sigaction(SIGCHLD,&act, NULL);
//SIGCHLD解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
while(1)//这个while函数模拟父进程在执行其他事情。
{
sleep(5);//模拟父进程在执行其他事情。
printf("asd");
}
}
}
Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字,如vsftpd
Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。
总结守护进程的特点:
一个进程组包含多个进程
每个进程都属于一个进程组,引入进程组是为了简化对进程的管理。 当父进程创建子进程的时候,默认子进程与父进程属于同一个进程组。
进程组的特点:
多个进程组组成一个会话
会话的特点:
编写一个守护进程,每隔2S钟获取一次系统时间,并将这个时间写入磁盘文件。
需求分析:
//编写一个守护进程,每隔2S钟获取一次系统时间,并将这个时间写入磁盘文件。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int flags=0;
void handler()
{
//文件只需要打开一次,设置flag来判断是否第一次打开
int fd;
if(flags==0)
{
fd= open("./time.log",O_RDWR | O_APPEND | O_CREAT, 0755);
if(fd<0)
return;
}
//获取时间
time_t t;
time(&t);
char * p = ctime(&t);
//写入文件
write(fd, p, strlen(p));
return;
}
int main()
{
pid_t pid= fork();
if(pid < 0 || pid>0)//父进程退出
{
exit(1);
}
//子进程创建会话
setsid();
//改变当前工作目录
chdir("./");//这里不该
//重设文件掩码
umask(0000);
//核心操作:
//注册信号
struct sigaction act;
act.sa_handler= handler;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
// signal(SIGALRM, handler);
//设置时钟信号
struct itimerval tm;
//触发周期赋值
tm.it_interval.tv_sec=2;//每隔两秒触发一次
tm.it_interval.tv_usec=0;//微秒,不管他
//第一次触发事件赋值
tm.it_value.tv_sec=3;//三秒后第一次触发
tm.it_value.tv_usec=0;//微秒,不管他
setitimer(ITIMER_REAL, &tm, NULL);//该函数每隔2s发送一次SIGALRM信号
while (1)//让守护进程持续运行
{
sleep(1);
//注意:结束程序要先用命令:ps ajx | grep day8_Daemo查找进程号,然后kill -9 进程号 来结束进程
}
}
线程----轻量级的进程
进程:拥有独立的地址空间,拥有PCB。
线程:有PCB,但没有独立的地址空间,多个线程共享进程空间,但是每个线程都有自己独立的栈空间。
由图可知:在使用pthread_create创造线程后,会在内核区复制pcb来创造线程(也没有完全复制,比如文件描述符。所有线程共享同一个文件描述符),共享除了栈以外的所有资源
特点:
所以由于线程共享地址空间,所以当线程读全局变量时,需要加锁。
从经验来说:一般业务处理用进程,一般网络通信用线程
要链接-pthread库
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
简单创建子线程案例
//创建子线程接收参数并执行printf
#include
#include
#include
#include
#include
#include
void *mythread(void *arg)
{
printf("child thread, pid=%d, id=%ld\n", getpid(),pthread_self());
pthread_exit(NULL);//子线程退出
}
int main()
{
//创建子进程
pthread_t pthread_id;
int ret = pthread_create(&pthread_id, NULL, mythread,NULL);
if(ret!=0)
{
printf("pthread_create error", strerror(ret));
return -1;
}
printf("father thread, pid=%d, id=%ld\n", getpid(),pthread_self());
pthread_join(pthread_id, NULL);//线程回收
return 1;
}
在原始版本中(原版本代码可以移步至https://github.com/jiong1998/Linux-.github.io/issues/13中看),最后每个子线程打印出来的值并不是想象中的值,比如都是5。
为什么?
原因是:由于主线程可能会在一个cpu时间片内连续创建了5个子线程,此时变量i的值变成了5,并且传入的值是int*地址, 当主线程失去cpu的时间片后,子线程得到cpu的时间片,子线程访问的是变量i的内存空间的值,所以打印出来值为5.
解决办法:
可以在主线程定义一个数组:int arr[5];,然后创建线程的时候分别传递不同的数组元素,这样每个子线程访问的就是互不相同的内存空间,这样就可以打印正确的值。
分析的如图所示
左图是修改前的代码所示(所有线程共享一个i的内存空间),右边图是修改后的代码所示(每个线程访问互不相同的内存空间)
代码修改后如下:
//子线程执行函数
void *mythread(void * arg)
{
int i= *(int *)arg;
printf("i=[%d],thread_id=[%ld]\n", i, pthread_self());
}
int main()
{
pthread_t thread[5];
int i,ret;
int arr[5];
for(i=0;i<5;++i)
{
arr[i]=i;
ret = pthread_create(&thread[i],NULL,mythread,&arr[i]);
if(ret!=0)
{
printf("pthread_create error", strerror(ret));
return -1;
}
}
sleep(1);
return 0;
}
输出结果:
i=[2],thread_id=[6167113728]
i=[0],thread_id=[6165966848]
i=[1],thread_id=[6166540288]
i=[3],thread_id=[6167687168]
i=[4],thread_id=[6168260608]
在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出, 如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。
另注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。
当主线程提前退出,并不会影响子线程的运行,而此时子线程还没有退出,会导致整个进程变成僵尸进程。
只有最后一个退出的是主线程,整个进程空间才能回收,才能避免僵尸进程。因此需要线程回收–pthread_join
类似进程的wait()函数
也可使用 pthread_create函数参2(线程属性)来设置线程分离。pthread_detach函数是在创建线程之后调用的。
注意:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 。可粗略认为一个系统调用(进入内核)即为一个取消点(可以让进程阻塞的一般都取消点)。还以通过调用 pthread_testcancel() 函数设置一个取消点。
进程 | 线程 |
---|---|
fork | pthread_create |
exit | pthread_exit |
wait/waitpid | pthread_join |
kill | pthread_cancel |
getpid | pthread_self |
linux下线程的属性是可以根据实际项目需要,进行设置,之前讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题,如果对程序的性能提出更高的要求,则需要设置线程属性,
本节以设置线程的分离属性为例讲解设置线程属性。
设置线程属性分为以下步骤:
第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);
注意:这一步完成之后调用pthread_create函数创建线程,则创建出来的线程就是分离线程;其实上述三步就是pthread_create的第二个参数做准备工作。
第4步:释放线程属性资源
int pthread_attr_destroy(pthread_attr_t *attr);
参数:线程属性
创建子线程的时候设置分离属性的简单案例:
//在创建子线程的时候设置分离属性
#include
#include
#include
#include
#include
#include
void * mythread(void * arg)
{
printf("chlid pthread[%ld]\n", pthread_self());
}
int main()
{
pthread_t pthread_id;
//1 定义线程属性类型的变量
pthread_attr_t attr;
//2 对线程属性变量进行初始化
pthread_attr_init(&attr);
//3 设置线程为分离属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
int ret = pthread_create(&pthread_id, &attr, mythread, NULL);
if(ret!=0)
{
printf("pthread_create error", strerror(ret));
return -1;
}
//4 释放线程属性
pthread_attr_destroy(&attr);
printf("father pthread[%ld]\n", pthread_self());
return 1;
}
同步:对运行次序的一种制约关系。
互斥:一种对共享资源的制约关系(信号灯)
创建两个线程,让两个线程共享一个全局变量int number, 然后让每个线程数5000次数,最后打印出来的数不是10000。
造成这样的原因:
解决办法:
提供互斥机制,即加互斥锁。
当某个线程加锁,另一个线程也想访问该资源发现上锁了,就会阻塞等待,直到解锁。
使用互斥锁之后,两个线程由并行操作变成了串行操作,效率降低了,但是数据不一致的问题得到解决了。
pthread_mutex_t 类型
pthread_mutex_init函数:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
互斥量mutex的两种初始化方式:
ead_mutex_trylock函数
加锁和解锁
互斥锁步骤:
//需求:定义一个全局变量number,利用互斥锁让每个线程数5000个数,
//最后全局变量number应该输出10000
#include
#include
#include
#include
#include
#include
//定义一个互斥锁
pthread_mutex_t mutex;
int number=0;
void * mythread(void * arg)//线程调用函数
{
int i;
int j;
for(i=0;i<5000;++i)
{
pthread_mutex_lock(&mutex);//加锁
j=number;
j++;
number=j;
pthread_mutex_unlock(&mutex);//解锁
}
pthread_exit(NULL);
}
int main()
{
//初始化锁
pthread_mutex_init(&mutex, NULL);
//创建子进程
pthread_t pthread_id[2];
for (int j=0;j<2;++j)
{
int ret = pthread_create(&pthread_id[j], NULL, mythread,NULL);
if(ret!=0)
{
printf("pthread_create error", strerror(ret));
return -1;
}
// pthread_detach(pthread_id[j]);
}
pthread_join(pthread_id[0],NULL);
pthread_join(pthread_id[1],NULL);
printf("number=%d", number);
//销毁锁
pthread_mutex_destroy(&mutex);
return 1;
}
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
如何解决死锁:
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
读写锁是“读模式加锁”时,此时如果既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁就会阻塞读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
总结就是:读并行,写独占,当读写同时等待锁的时候写的优先级高。
读写锁使用场合
读写锁非常适合于对数据结构读的次数远大于写的情况。
定义一把读写锁: pthread_rwlock_t rwlock;
int pthread_rwlock_init(
pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
#include
#include
#include
#include
#include
#include
#include
//定义一把锁
pthread_rwlock_t rwlock;
//定义全局变量number
int number=0;
void* write_function(void * arg)
{
int temp;
while(1)
{
//加写锁
pthread_rwlock_wrlock(&rwlock);
temp=number;
temp++;
number=temp;
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
}
void* read_function(void * arg)
{
int i=*(int *) arg;
int temp;
while(1)
{
//加读锁
pthread_rwlock_rdlock(&rwlock);
temp=number;
printf("[%ld]---read:[%d]\n", i,temp);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
}
int main()
{
int i;
int n=8;
int arr[8];
pthread_t thread[8];
//初始化读写锁
pthread_rwlock_init(&rwlock,NULL);
//创建3个写线程
for(i=0;i<3;++i)
{
arr[i]=i;
pthread_create(&thread[i],NULL,write_function, &arr[i]);
}
//创建5个读线程
for(i=3;i<n;++i)
{
arr[i]=i;
pthread_create(&thread[i],NULL,read_function, &arr[i]);
}
for(int j=0;j<n;++j)
{
pthread_join(thread[j], NULL);
}
//销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的线程.
条件本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
条件变量的两个动作:
定义一个条件变量:pthread_cond_t cond;
初始化条件变量:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
互斥锁步骤:
生产者每次生产一个链表,消费者每次消费一个链表。(链表不带头节点)
#include
#include
#include
#include
#include
#include
//定义一把锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//定义链表
typedef struct node
{
int data;
struct node * next;
}NODE;
NODE *head = NULL;
void * producer(void*arg)
{
NODE * pnode= NULL;
while(1)
{
//生产一个链表
pnode = (NODE *) malloc(sizeof(NODE));
pnode->data=rand()%1000;
//加锁
pthread_mutex_lock(&mutex);
pnode->next=head;
head=pnode;
printf("Producing---[%d]\n", head->data);
//解锁
pthread_mutex_unlock(&mutex);
//通知消费者线程解除阻塞
pthread_cond_signal(&cond);
sleep(rand()%2);
}
}
void * consumer(void*arg)
{
NODE * pnode = NULL;
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
while(head==NULL)
{
//如果头节点为空,则阻塞在此并解锁,直到生产者给出信号(被生成者线程调用pthread_cond_signal函数通知),才唤醒并重新加锁
pthread_cond_wait(&cond, &mutex);
}
pnode=head;
head=head->next;
printf("Consuming---[%d]\n", pnode->data);
//解锁
pthread_mutex_unlock(&mutex);
free(pnode);
pnode=NULL;
sleep(rand()%2);
}
}
int main()
{
int ret;
//初始化锁
pthread_mutex_init(&mutex, NULL);
//初始化条件变量
pthread_cond_init(&cond, NULL);
pthread_t pthread[2];
ret=pthread_create(&pthread[0], NULL, producer,NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
ret=pthread_create(&pthread[1], NULL, consumer,NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//等待线程结束
pthread_join(pthread[0],NULL);
pthread_join(pthread[1],NULL);
//销毁锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 1;
}
分析:多个生产者和多个消费者如果还是使用上面的代码改成多线程,程序会崩掉。原因是:
假设生产者只生产了一个节点。通过pthread_cond_signal唤醒了两个消费者,于是两个消费者同时解除阻塞,但是此时只有一个消费者加锁成功,而另一个消费者此时又会被阻塞,但是这次的阻塞时阻塞在互斥锁上,而不是条件变量。当第一个消费者消费完并解锁后,head=NULL。此时第二个消费者就会加锁并获取head->data,但是这时head=NULL,所以系统会崩。
解决办法 :
在消费者中的这行代码把if改成while
if(head==NULL)
{
pthread_cond_wait(&cond, &mutex);
}
修改后:
while(head==NULL)
{
pthread_cond_wait(&cond, &mutex);
}
具体代码如下:
需求:生产者每次生产一个链表,消费者每次消费一个链表。(链表不带头节点)
#include
#include
#include
#include
#include
#include
//定义一把锁
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
//定义链表
typedef struct node
{
int data;
struct node * next;
}NODE;
NODE *head = NULL;
void * producer(void * arg)
{
NODE * pnode= NULL;
int i=*(int *)arg;
while(1)
{
//生产一个链表
pnode = (NODE *) malloc(sizeof(NODE));
pnode->data=rand()%1000;
//加锁
pthread_mutex_lock(&mutex);
pnode->next=head;
head=pnode;
printf("[%d]Producing---[%d]\n", i, head->data);
//解锁
pthread_mutex_unlock(&mutex);
//通知消费者线程解除阻塞
pthread_cond_signal(&cond);
sleep(rand()%2);
}
}
void * consumer(void * arg)
{
NODE * pnode = NULL;
int i=*(int *)arg;
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
while(head==NULL)//通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait。所以这里改成while不要用if
{
//如果头节点为空,则阻塞在此并解锁,直到生产者给出信号(被生成者线程调用pthread_cond_signal函数通知),才唤醒并重新加锁
pthread_cond_wait(&cond, &mutex);
}
pnode=head;
head=head->next;
printf("[%d]Consuming---[%d]\n", i, pnode->data);
//解锁
pthread_mutex_unlock(&mutex);
free(pnode);
pnode=NULL;
sleep(rand()%3);
}
}
int main()
{
int ret;
int i;
int P_arr[5];
int C_arr[5];
//初始化锁
pthread_mutex_init(&mutex, NULL);
//初始化条件变量
pthread_cond_init(&cond, NULL);
pthread_t P_pthread[3];
pthread_t C_pthread[5];
//创造生产者
for(i=0;i<5;++i)
{
P_arr[i]=i;
ret=pthread_create(&P_pthread[i], NULL, producer,&P_arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
//创造消费者
for(i=0;i<5;++i)
{
C_arr[i]=i;
ret=pthread_create(&C_pthread[i], NULL, consumer,&C_arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
//等待线程结束
for(i=0;i<5;++i)
{
pthread_join(P_pthread[i],NULL);
}
for(i=0;i<5;++i)
{
pthread_join(C_pthread[i],NULL);
}
//销毁锁
pthread_mutex_destroy(&mutex);
//销毁条件变量
pthread_cond_destroy(&cond);
return 1;
}
#include
信号量是先做加/减法,再判断是否进入。
定义一个信号量 sem_t sem
int sem_init(sem_t *sem, int pshared, unsigned int value);
1 定义信号量变量
sem_t sem1;
sem_t sem2;
2 初始化信号量
sem_init(&sem1, 0, 5);
sem_init(&sem2, 0, 0);
3 加锁
sem_wait(&sem1);
//共享资源
sem_post(&sem2);
sem_wait(&sem2);
//共享资源
sem_post(&sem1);
4 释放资源
sem_destroy(sem1);
sem_destroy(sem2);
案例:利用信号量解决多线程间的生产者和消费者问题
需求:假设有5个生产者,4个消费者,生产者每次生产一个苹果到桌子上,消费者每次从桌子上消费一个苹果,假设桌子最多放3个苹果,利用信号量解决该问题
#include
#include
#include
#include
#include
#include
#include
sem_t empty;//剩余空间信号量
sem_t full;//占用空间信号量
pthread_mutex_t mutex;//互斥锁
int apple_number=0;//苹果的数量
void * producer(void * arg)
{
int i = *(int *)arg;
while(1)//生产
{
sem_wait(&empty);//生产苹果,剩余空间-1
pthread_mutex_lock(&mutex);
sleep(1);
apple_number++;
printf("序号[%d]生产了一个苹果,目前桌子上有[%d]个水果\n", i, apple_number);
pthread_mutex_unlock(&mutex);
sem_post(&full);//生产完,占用空间+1
sleep(rand()%2);
}
}
void * consumer(void * arg)
{
int i = *(int *)arg;
while(1)//消费
{
sem_wait(&full);//消费苹果,占用空间-1
pthread_mutex_lock(&mutex);
sleep(1);
apple_number--;
printf("序号[%d]消费了一个苹果,目前桌子上有[%d]个水果\n", i, apple_number);
pthread_mutex_unlock(&mutex);
sem_post(&empty);//消费完,剩余空间+1
sleep(rand()%3);
}
}
int main()
{
int i,ret;
int P_arr[5];
int C_arr[4];
pthread_mutex_init(&mutex,NULL);
sem_init(&empty,0,3);//空间多大就设多少
sem_init(&full,0,0);
pthread_t P_pthread[5];
pthread_t C_pthread[4];
//创造生产者
for(i=0;i<5;++i)
{
P_arr[i]=i;
ret=pthread_create(&P_pthread[i], NULL, producer,&P_arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
//创造消费者
for(i=0;i<4;++i)
{
C_arr[i]=i;
ret=pthread_create(&C_pthread[i], NULL, consumer,&C_arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
//等待线程结束
for(i=0;i<5;++i)
{
pthread_join(P_pthread[i],NULL);
}
for(i=0;i<5;++i)
{
pthread_join(C_pthread[i],NULL);
}
//销毁锁
pthread_mutex_destroy(&mutex);
//销毁信号量
sem_destroy(&empty);
sem_destroy(&full);
return 1;
}