学习方法
1. 重点放在函数的应用,不需要关心底层逻辑,不要太钻牛角尖太探究内核的东西,把当下学习的内容都掌握了已经很好了,可以以后有能力了再继续研究更底层的,现在先把重点放在应用层。
2. 好好理解概念,但是需要记住的例如函数的功能必须要记住,函数的参数记住重要的,可以把代码多敲几遍自然而然就记住了,但是要带着脑子敲代码。
3. 练习题主要是记住逻辑,不要死记硬背代码。
大纲
IO:input、output
标准IO
文件IO
库
Linux IO模型
进程:process
进程基础
进程间通信:无名管道(pipe)、有名管道(fifo)、信号(signal)、共享内存(shared memory)、消息队列(message queue)、信号灯集(semphore set)
线程(thread)、同步、互斥、条件变量
一、标准IO
1、什么是标准IO
1.1 概念
标准IO:在C库中定义的一组专门用来输入输出的函数
1.2. 特点
1)通过缓冲机制减少系统调用,提高效率
系统调用:内核向上提供的一组接口
2)围绕着流进行操作,流FILE*来描述,FILE代表的是结构体
用 vi 的ctags索引:
先建立ctags索引:密码1
1) vi -t 查找的名称
输入序号,回车
2) 继续追踪
将光标定位到要追踪的内容,ctrl ]
回退:ctrl t
3) 跳转到上次位置:ctrl o
跳转到下次位置:ctrl i
q:退出
vscode索引:‘
跳转到定义:ctrl 左键
前后跳转:
ctrl alt -
ctrl shift -
3)默认打开了三个流:stdin(标准输入)、stdout(标准输出)、stderr(标准错误)
1.3. 操作
打开文件:fopen
关闭文件:fclose
读写操作:fgets、fputs、fread、fwrite
定位操作:rewind、fseek、ftell
2.缓存区
1)全缓存:和文件相关
刷新输出条件:
● 程序正常退出
● 强制刷新:fflush(NULL);
● 缓存区满
1)行缓存:和终端相关
刷新标准输出缓存的条件:
● \n
● 程序正常退出
● 强制刷新:fflush(NULL);
● 缓存区满
\n:
缓存区满刷新:
强制刷新:
1)不缓存:标准错误
综上:当我们每次要打印数据时,并不是将数据直接发送给标准输出设备,也就是并直接发送给显示器,而是将要打印的数据先存放到缓存区,当缓冲存数据满时,或者遇到\n,或者程序结束时,或者手动刷新缓存区时,缓冲区才会把数据传输到标准输出设备中,也就是显示器中进行输出。
3.函数接口
3.1. 打开文件 fopen
// man 3 fopen #include <stdio.h> FILE *fopen(const char *pathname, const char *mode); 功能:打开文件 参数:pathname: 打开的文件路径 mode: 打开的方式 r:只读,流被定位到文件开头 r+:可读可写,流被定位到文件开头 w:只写,文件不存在创建,文件存在清空,流被定位到文件开头 w+:可读可写,文件不存在创建,文件存在清空,流被定位到文件开头 a:追加,文件不存在创建存在追加,流被定位到文件末尾 a+:可读可写,文件不存在创建,存在追加,开始进行读时从头读,进行写时流被定位到文件末尾 返回值:成功:文件流 失败:NULL,并且设置errno(错误码)3.2. 关闭文件
int fclose(FILE* stream); 功能:关闭文件 参数:stream:文件流3.3. 读写文件操作fgets、fputs、fread、fwrite
3.3.1. 每次读写一个字符串:
fgets
#include <stdio.h> char *fgets(char *s, int size, FILE *stream); 功能:从文件中读取一个字符串 参数:s:存放读取的字符串首地址 size:读取的大小 stream:文件流 返回值:成功:读取的字符串首地址 失败读到文件末尾:NULL 特性:1. 一次调用最多读取一行数据,遇到\n或者达到文件末尾后不在继续下一行 2. 实际读到个数为size-1个,末尾自动添加\0fputs
#include <stdio.h> int fputs(const char *s, FILE *stream); 功能:向文件中写一个字符串 参数:s:要写入的内容 stream:文件流 返回值:成功:非负整数 失败:EOF针对终端
针对文件
创建文件
练习:编程实现wc -l命令的功能
wc -l 文件名:显示文件的行数
思路:打开文件,循环读文件,当读到文件末尾时循环结束,在循环中判断是否是一行,如果是则对整型变量进行自加,关闭文件。
优化:
3.3.2. 二进制读写 fread、fwrite
fread
#include <stdio.h> size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 功能:从文件流读取多个元素(将二进制数据从文件读出) 参数:ptr:用来存放读取元素(可以用来存放任意类型的数据) size:元素大小 sizeof(数据类型) nmemb: 读取的对象个数 stream:要读取的文件 返回值:成功:读取元素的个数 失败或读取到文件尾:0fwrite
#include <stdio.h> size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); 功能:将二进制数据写入文件 参数:ptr :是一个指针,保存要输出数据的空间的地址。 size :要写入的字节数 sizeof(数据类型) nmemb : 要进行写入元素的个数 stream: 目标文件流指针 返回值:成功:写元素的个数 失败:-13.4. 文件定位操作
rewind
void rewind(FILE* stream); 功能:将文件的位置指针定位到起始位置fseek
#include <stdio.h> int fseek(FILE *stream, long offset, int whence); 功能:文件的定位操作 参数:stream:文件流 offset:偏移量:正数表示向后文件尾部偏移,负数表示向文件开头偏移 whence:相对位置: SEEK_SET :相对于文件开头 SEEK_CUR :相对于文件当前位置 SEEK_END :相对于文件末尾 返回值:成功:0 失败:-1 注:当打开方式为a或a+时,fseek不起作用 补充:其中SEEK_SET、SEEK_CUR、SEEK_END依次为0、1、2 例子: 把fp指针移动到离文件开头10个字节处:fseek(fp, 10, SEEK_SET); 把fp指针移动到离文件当前位置10个字节处:fseek(fp, 10, SEEK_CUR); 把fp指针移动到离文件结尾10个字节处:fseek(fp, -10, SEEK_END);ftell
long ftell(FILE *stream) 功能:获取当前的文件位置 参数:要检测的文件流 返回值:成功:当前的文件位置 失败:-1练习1
// 3.题目要求:编程读写一个文件test.txt,每隔1秒向文件中写入一行数据,类似这样: // 1, hello // 2, hello // 该程序应该无限循环,直到按Ctrl-C中断程序。 // 再次启动程序写文件时可以追加到原文件之后,并且序号能够接续上次的序号,比如: // 1, hello // 2, hello // 3, hello // 4, hello // 5, hello #include <stdio.h> #include <string.h> #include <unistd.h> int main(int argc, char const *argv[]) { FILE *fp = fopen("test.txt", "a+"); if (NULL == fp) { perror("fp err"); return -1; } char buf[32] = ""; int i = 0; rewind(fp); while (fgets(buf, sizeof(buf), fp) != NULL) { if (buf[strlen(buf) - 1] == '\n') { i++; } } while (1) { i++; fprintf(fp, "%d,hello\n", i); fflush(fp); sleep(1); } fclose(fp); return 0; }练习2
// 4.拔高题目要求:编程读写一个文件test.txt,每隔1秒向文件中写入一行数据,类似这样: // 1, 2007-7-30 15:16:42 // 2, 2007-7-30 15:16:43 // 该程序应该无限循环,直到按Ctrl-C中断程序。 // 再次启动程序写文件时可以追加到原文件之后,并且序号能够接续上次的序号,比如: // 1, 2007-7-30 15:16:42 // 2, 2007-7-30 15:16:43 // 3, 2007-7-30 15:19:02 // 4, 2007-7-30 15:19:03 // 5, 2007-7-30 15:19:04 // time()\localtime() // sleep(1); //睡眠函数 // fprintf/sprintf #include <stdio.h> #include<string.h> #include <unistd.h> #include <time.h> int main(int argc, char const *argv[]) { char buf[32] = {}; int n = 0; time_t t; FILE *fp; struct tm *tm; fp = fopen("test.txt", "a+"); if (NULL == fp) { perror("fopen err"); return -1; } while (fgets(buf, 32, fp) != NULL) { if (buf[strlen(buf) - 1] == '\n') n++; } while (1) { time(&t); tm = localtime(&t); fprintf(fp, "%d. %d-%d-%d %d:%d:%d\n", ++n, tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday + 1, tm->tm_hour, tm->tm_min, tm->tm_sec); fflush(NULL); sleep(1); } return 0; }练习3
// 5.实现"head -h 文件名"命令的功能 // ./a.out -5 xx.c // atoi: "123" --> 123 // argv[1]: "-5" // argv[1]+1: "5" //+1目的是去掉- // 思想:循环打印,数换行,来一行行数+1,打印这行,知道换行的数量达到n就结束。 #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char const *argv[]) { char buf[32] = {}; int n = 0; if (argc != 3) { printf("usage:%s -n <filename>\n", argv[0]); return 0; } FILE *fp; fp = fopen(argv[2], "r"); if (NULL == fp) { perror("fopen err"); return -1; } int num = atoi(argv[1] + 1); while (fgets(buf, 32, fp) != NULL) { if (buf[strlen(buf) - 1] == '\n') n++; printf("%s", buf); if (n == num) { break; } fclose(fp); } return 0; }二、文件IO
1.什么是文件IO
1.1. 概念
又称系统IO,是系统调用,是操作系统提供的函数接口
在posix(可移植操作系统接口)中定义的一组输入输出的函数
1.2. 特点
1. 没有缓冲机制,每次操作都会经过系统调用,效率比较低
2. 围绕着文件描述符进行操作,文件描述符是非负整数:0、1、2
3. 默认打开三个文件描述符:0(标准输入)、1(标准输出)、2(标准错误)
4. 除目录外其他任意类型的文件都可以操作b、c、-、l、s、p
问题:打开三个文件,描述符:3、4、5
关闭3以后,重新打开文件,描述符是几?
答:还是3
问题:一个进程的文件描述符最大到几?最多能打开多少个文件描述符?最多能打开多少个文件?
答:一个进程的文件描述符最大到1023(0-1023),最多能打开1024个文件描述符, 最多能打开1024-3=1021个文件。
1.3. 操作
打开文件:open
关闭文件:close
读写操作:read、write
定位操作:lseek
2.函数接口
2.1. 打开文件 open
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); 功能:打开文件 参数:pathname: 文件路径名 flags:打开方式 O_RDONLY:只读 O_WRONLY:只写 O_RDWR:可读可写 O_CREAT:不存在创建 O_TRUNC:清空 O_APPEND:追加 返回值:成功:文件描述符 失败:-1 当第二个参数中存在O_CREAT选项时,需要open函数传递第三个参数,创建文件的权限 int open(const char *pathname, int flags, mode_t mode); 权限值 & (~umask) = 实际的文件权限 umask: 0002 0666 & (~umask) 110 110 110 & 111 111 101 || 110 110 100 = 0664补充:
1. 当第二个参数中有O_CREAT选项时,需要给open函数传递第三个参数,指定创建文件的权限 ,open(file, xx|O_CREAT|xx, 0666);
2. 实际创建出来的文件权限为指定权限值&(~umask)//umask为文件权限掩码,通过umask命令可以查看
3. 打开文件方式对应表:
标准IO | 文件IO |
r 只读,不会创建 | O_RDONLY 只读 |
r+ 可读可写,不会创建 | O_RDWR 可读可写 |
w 只写, 会创建,会清空 | O_WRONLY|O_CREAT|O_TRUNC 可写 | 创建 | 清空 |
w+ 可读可写,会创建,会清空 | O_RDWR|O_CREAT|O_TRUNC 可读可写 | 创建 | 清空 |
a 可读,会创建,会追加 | O_WRONLY|O_CREAT|O_APPEND 只写 | 创建 | 追加 |
a+ 可读,会创建,会追加 | O_RDWR|O_CREAT|O_APPEND 可读可写 | 创建 | 追加 |
2.2. 关闭文件close
#include <unistd.h> int close(int fd); 参数:fd:文件描述符#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char const *argv[]) { int fd; fd = open("./test.txt", O_RDWR | O_CREAT | O_TRUNC, 0776); if (fd < 0) { perror("open err"); return -1; } char buf[32] = {}; printf("%ld\n", read(fd, buf, 5)); write(fd, "hello world\n", 12); lseek(fd, 0, 0); printf("%ld\n", read(fd, buf, 5)); printf("%s\n", buf); return 0; }2.3. 读写文件
read
#include <unistd.h> size_t read(int fd, void *buf, size_t count); 功能:从一个已经打开的可读文件中读取数据 参数:fd 文件描述符 buf 读到数据的存放位置 count 期望的个数 返回值: 成功:实际读到的个数(小于期望的值,说明实际没这么多) 返回0:表示读到文件结尾 返回值-1:表示出错,并设置errno号write
#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); 功能:向指定的文件描述符中,写入count个字节的数据 参数:fd 文件描述符 buf 要写的内容 count 期望写入的字节数 返回值:成功:实际写入数据的个数 失败:-1fgets: NULL末尾或失败
fread: 0末尾或失败
read: 0末尾 -1失败
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char const *argv[]) { int fd; fd = open("./test.txt", O_RDWR | O_CREAT | O_TRUNC, 0776); if (fd < 0) { perror("open err"); return -1; } char buf[32] = {}; printf("%ld\n", read(fd, buf, 5)); write(fd, "hello world\n", 12); lseek(fd, 0, 0); printf("%ld\n", read(fd, buf, 5)); printf("%s\n", buf); return 0; }2.4. 定位操作 lseek
#include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence); 功能:设置文件的偏移位置 参数:fd 文件描述符 offset 偏移量 正数:向文件结尾位置移动 负数:向文件开始位置移动 whence 相对位置 SEEK_SET 开始位置 SEEK_CUR 当前位置 SEEK_END 结尾位置 补充:和fseek一样其中SEEK_SET,SEEK_CUR和SEEK_END和依次为0,1和2 返回值:成功:文件的当前位置 失败:-1练习:文件IO实现cp功能。cp 源文件 新文件名
./a.out src dest
、
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int main(int argc, char const *argv[]) { int fp1, fp2; fp1 = open("./src.c", O_RDONLY); if (fp1 < 0) { perror("src err"); return -1; } fp2 = open("./dest.c", O_WRONLY | O_CREAT, 0776); if (fp2 < 0) { perror("dest err"); return -1; } char buf[32] = {}; int n; while ((n = read(fp1, buf, 32)) > 0) { write(fp2, buf, n); } close(fp1); close(fp2); return 0; }#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char const *argv[]) { if (argc != 3)//./a.out src.c dest.c { printf("argc err\n"); return -1; } int fd1 = open(argv[1], O_RDONLY); if (fd1 < 0) { perror("argv[1] err"); return -1; } int fd2 = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0766); if (fd2 < 0) { perror("argv[2] err"); return -1; } char buf[32]={}; int n=0; while((n=read(fd1, buf,32))>0) { write(fd2,buf,n); } close(fd1); close(fd2); return 0; }标准IO和文件 IO区别
标准IO | 文件IO | |
概念 | 在C库中定义的一组输入输出的函数 | 在posix中定义的一组输入输出的函数 |
特点 | 1. 有缓冲区,减少系统调用,提高效率 2. 围绕流操作,FILE * 3. 默认打开三个流:stdin\stdout\stderr 4. 只操作普通文件 5. 可移植性相对较好 | 1. 无缓冲区,每次操作都引起系统调用 2. 围绕文件描述符操作 3. 默认打开三个文件描述符:0\1\2 4. 除目录外其他文件 5. 可移植性相对较弱 |
函数 | 打开文件:fopen\freopen 关闭文件:fclose 读写文件:fgetc/fputc fgets/fputs fread/fwrite 文件定位:fseek/rewind/ftell | 打开文件:open 关闭文件:close 读写文件:read/write 文件定位:lseek |
三、库
1.库的定义
当使用别人的函数时除了包含头文件以外还要有库
linux系统存储库的位置一般在/lib或/usr/lib
库文件:通俗讲将用户写好的程序打包形成一个整体;当其他用户或其他模块使用时,只要有这个库文件就可以,不需要源代码。也可以理解为一组预先编译好的方法集合。
本质上来说库就是一种可执行代码的二进制形式
由于windows和linux的本质不同,因此二者库的二进制是不兼容的
Linux:.so.awindows:dll
lib:库文件
include:头文件
2.库的分类
静态库、动态库,本质的区别是代码载入的时刻不同。
2.1. 静态库
静态库是在程序编译时会被复制到目标代码中,以.a结尾
优点:程序运行时将不再需要该静态库;运行时无需加载库,运行速度更快
缺点:静态库中的代码复制到了程序中,因此体积较大;
静态库升级后,程序需要重新编译链接
2.2. 动态库
动态库是在程序运行时才会被载入到代码中。也叫共享库,以.so结尾
优点:程序在执行时加载动态库,代码体积小;程序升级更简单;
不同应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。
缺点:运行时还需要动态库的存在,移植性较差
3.库的制作
3.1. 静态库的制作
1-将源文件编译生成目标文件
gcc -c xxx.c -o xxx.o
2-创建静态库用ar命令,它将很多.o转换成.a
ar crs libxxx.a xxx.o
静态库文件名的命名规范是以lib为前缀,紧接着跟静态库名,扩展名为.a
3-测试使用静态库:
gcc xxx.c -L. -l指定库名 // -L指定库的路径 -l指定库名
执行./a.out
3.2. 动态库的制作
1-我们用gcc来创建共享库
gcc -fPIC -c xxx.c -o xxx.o
-fPIC 创建与地址无关的编译程序 (就是不和路径进行关联)
gcc -shared -o libxxx.so xxx.o
2-测试动态库使用
gcc xxx.c -L. -l指定库名
可以正常编译通过,但是运行时报错./a.out: error while loading shared libraries: libmyadd.so: cannot open shared object file: No such file or directory
原因:当加载动态库时,系统会默认从/lib或/usr/lib路径下查找库文件,所以不用加-L指定路径了,直接gcc main.c -lmyfun就可以了。
解决方法(有三种):
1)把库拷贝到/usr/lib和/lib目录下。(此方法编译时不需要指定库的路径)
2) 在LD_LIBRARY_PATH环境变量中加上库所在路径。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
(终端关闭,环境变量就没在了)
3)添加/etc/ld.so.conf.d/*.conf文件。把库所在的路径加到文件末尾,并执行ldconfig刷新
sudo vi xx.conf
添加动态库存在的路径使用绝对路径
-L路径:指定库的路径
-l库名:指定链接的库名
-I(大写i) 路径:指定头文件的路径 默认查找的路径/usr/include
<> 代表从系统路径下查找
"" 代表从当前路径下查找如果没有再去系统路径下查找
ldd 可执行文件名:查看链接的动态库
补充:
同名的静态库和动态库:默认优先使用动态库,如果想使用静态库 需要在后面加 -static,这是内核规定的。
如果链接没有lib前缀的库文件,可以直接用-指定库的全名无需加l选项。
4.总结静态库和动态库
静态库:编译阶段,以.a结尾,执行速度快,体积大,移植性好,升级麻烦。
动态库:运行节点,以.so结尾,执行速度慢,体积小,移植性差,升级简单。
可以看出静态库编译出来的程序体积大:
四、LinuxIO模型
阻塞IO、非阻塞IO、信号驱动IO(了解)、IO多路复用
场景假设一
假设有一个孩子,孩子在房间里睡觉,需要及时获知孩子是否醒了,如何做?
1.在房间待着,和孩子一起睡;不累,但是不能干其他事情
2.时不时的来看一下孩子,其他的时间我可以干一些其他的事情;累,但是可以干其他事情
3.我做其他的事情,听孩子是否哭了;二者互不耽误
1.阻塞IO:最常见、效率低、不浪费CPU
阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
学习的读写函数在调用过程中会发生阻塞相关函数如下:
•读操作中的read
读阻塞-->需要读缓冲区中有数据可读,读阻塞解除
•写操作中的write
写阻塞-->阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。
2.非阻塞IO:轮询、耗费CPU、可以同时处理多路IO
•当我们设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
•这种模式使用中不普遍。
通过设置文件描述符的属性设置非阻塞
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ ); 功能:设置文件描述符属性 参数: fd:文件描述符 cmd:设置方式 - 功能选择 F_GETFL 获取文件描述符的状态信息 第三个参数化忽略 F_SETFL 设置文件描述符的状态信息 通过第三个参数设置 O_NONBLOCK 非阻塞 O_ASYNC 异步 O_SYNC 同步 arg:设置的值 in 返回值: 特殊选择返回特殊值 - F_GETFL 返回的状态值(int) 其他:成功0 失败-1,更新errno0 --> 阻塞、读权限 修改或者添加非阻塞
int flage = fcntl(0, F_GETFL);// 1. 获取文件描述符原有的属性信息 flage |= O_NONBLOCK; // 2. 添加非阻塞权限 fcntl(0, F_SETFL, flage); // 3. 将修改好的权限重新设置#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> int main(int argc, char const *argv[]) { char buf[32] = {}; // 1. 获取文件描述符原有的属性信息 int flage = fcntl(0, F_GETFL); // 2. 添加非阻塞权限 flage |= O_NONBLOCK; // 3. 将修改好的权限重新设置 fcntl(0, F_SETFL, flage); while(1) { fgets(buf, sizeof(buf), stdin); printf("buf=%s\n", buf); memset(buf, 0, 32); printf("------------\n"); sleep(1); } return 0; }注意:恢复阻塞模式
设置回去:
flag &= ~O_NONBLOCK;
fcntl(0, F_SETFL, flag);
3.信号驱动IO:异步通知方式,底层驱动支持
异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。通知发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知。
1. 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
2. 应用程序收到信号后做异步处理即可。
3. 应用程序需要把自己的进程号告诉内核,并打开异步通知机制。
//1.设置将文件描述符和进程号提交给内核驱动 //一旦fd有事件响应, 则内核驱动会给进程号发送一个SIGIO的信号 fcntl(fd,F_SETOWN,getpid()); //2.设置异步通知 int flags; flags = fcntl(fd, F_GETFL); //获取原属性 flags |= O_ASYNC; //给flags设置异步 O_ASUNC 通知 fcntl(fd, F_SETFL, flags); //修改的属性设置进去,此时fd属于异步 //3.signal捕捉SIGIO信号 --- SIGIO:内核通知会进程有新的IO信号可用 //一旦内核给进程发送sigio信号,则执行handler signal(SIGIO,handler);总结
阻塞IO(Blocking IO) | 非阻塞IO(Non-blocking IO) | 信号驱动IO(Signal-driven IO) | |
同步性 | 同步 | 非同步 | 异步 |
描述 | 调用IO操作的线程会被阻塞,直到操作完成 | 调用IO操作时,如果不能立即完成操作,会立即返回,线程可以继续执行其他操作 | 当IO操作可以进行时,内核会发送信号通知进程 |
特点 | 最常见、效率低、不耗费cpu, | 轮询、耗费CPU,可以处理多路IO,效率高 | 异步通知方式,需要底层驱动的支持 |
适应场景 | 小规模IO操作,对性能要求不高 | 高并发网络服务器,减少线程阻塞时间 | 实时性要求高的应用,避免轮询开销 |
场景假设二
假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?
阻塞IO?在一个房间
非阻塞IO? 不停的每个房间查看
信号驱动IO? 不行,因为只有一个信号,不知道那个孩子醒
1. 不停的每个房间看:超级无敌累,但是也可以干点其他的事情
2. 妈妈在客厅睡觉,孩子醒了之后自己找妈妈:既可以休息,也可以及时获取状态
4.IO多路复用:select、poll、epoll
● 应用程序中同时处理多路输入输出流,若采用阻塞模式,得不到预期的目的;
● 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
● 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;
● 比较好的方法是使用I/O多路复用技术。其基本思想是:
○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。
○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
○ 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
4.1. select
4.1.1.特点
1. 一个进程最多只能监听1024个文件描述符
2. select被唤醒之后要重新轮询,效率相对低
3. select每次都会清空未发生响应的文件描述符,每次拷贝都需要从用户空间到内核空间,效率低,开销大
4.1.2.编程步骤
1. 先构造一张关于文件描述符的表
2. 清空表 FD_ZERO
3. 将关心的文件描述符添加到表中 FD_SET
4. 调用select函数
5. 判断是哪一个或者式哪些文件描述符产生了事件 FD_ISSET
6. 做对应的逻辑处理
4.1.3.函数接口
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 功能: 实现IO的多路复用 参数: nfds:关注的最大的文件描述符+1 readfds:关注的读表 writefds:关注的写表 exceptfds:关注的异常表 timeout:超时的设置 NULL:一直阻塞,直到有文件描述符就绪或出错 时间值为0:仅仅检测文件描述符集的状态,然后立即返回 时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值 struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微秒 = 10^-6秒 */ }; 返回值: 准备好的文件描述符的个数 -1 :失败: 0:超时检测时间到并且没有文件描述符准备好 注意: select返回后,关注列表中只存在准备好的文件描述符 操作表: void FD_CLR(int fd, fd_set *set); //清除集合中的fd位 void FD_SET(int fd, fd_set *set);//将fd放入关注列表中 int FD_ISSET(int fd, fd_set *set);//判断fd是否在集合中 是--》1 不是---》0 void FD_ZERO(fd_set *set);//清空关注列表练习
输入鼠标的时候,响应鼠标事件,输入键盘的时候,响应键盘事件 (两路IO)
#include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/select.h> #include <unistd.h> #include <fcntl.h> #include <string.h> int main(int argc, char const *argv[]) { int ret = 0; char buf[32] = {}; int fd = open("/dev/input/mouse0", O_RDONLY); if (fd < 0) { perror("open err"); return -1; } // 1.先构造一张关于文件描述符的表 fd_set rfds; while (1) { // 2.清空表 FD_ZERO FD_ZERO(&rfds); // 3.将关心的文件描述符添加到表中 FD_SET FD_SET(fd, &rfds); // 鼠标 FD_SET(0, &rfds); // 键盘 struct timeval tm = {2, 0}; // 4.调用select函数 ret = select(fd + 1, &rfds, NULL, NULL, &tm); if (ret < 0) { perror("select err"); return -1; } else if (ret == 0) { printf("time out\n"); continue; } // 5.判断是哪一个或者式哪些文件描述符产生了事件 FD_ISSET if (FD_ISSET(0, &rfds)) { // 6.做对应的逻辑处理 fgets(buf, sizeof(buf), stdin); printf("buf: %s\n", buf); } if (FD_ISSET(fd, &rfds)) { // 6.做对应的逻辑处理 read(fd, buf, sizeof(buf)); printf("mouse: %s\n", buf); } memset(buf, 0, sizeof(buf)); } close(fd); return 0; }4.1.4: 超时检测
概念
什么是网络超时检测呢,比如某些设备的规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,那么需要做出一些特殊的处理
比如: 链接wifi的时候,等了好长时间也没有连接上,此时系统会发送一个消息: 网络连接失败;
必要性
1. 避免进程在没有数据时无限制的阻塞;
2. 规定时间未完成语句应有的功能,则会执行相关功能
4.2. poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout); 功能:同select相同实现IO的多路复用 参数: fds:指向一个结构体数组的指针,用于指定测试某个给定的文件描述符的条件。 nfds:指定的第一个参数数组的元素个数。 timeout:超时设置 -1:永远等待 0:立即返回 >0:等待指定的毫秒数 struct pollfd { int fd; // 文件描述符 short events; // 等待的事件 short revents; // 实际发生的事件 }; 返回值: 成功时返回结构体中 revents 域不为 0 的文件描述符个数 0: 超时前没有任何事件发生时,返回 0 -1:失败并设置 errno特点
1. 优化文件描述符的限制,文件描述符的限制取决于系统
2. poll被唤醒之后要重新轮询一遍,效率相对低
3. poll不需要重新构造表,采用结构体数组,每次都需要从用户空间拷贝到内核空间
实现过程
1. 创建一个表,也就是一个结构体数组 struct pollfd fds[100];
2. 将关心的描述符添加到表中并赋予事件
3. 循环调用poll更新表while(1){poll();}
4. 逻辑判断 if(fds[i].revents==POLLIN) {}
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/poll.h> #include <sys/time.h> #include <string.h> int main(int argc, char const *argv[]) { int ret=0; char buf[128]=""; //打开鼠标文件描述符 int fd_mouse = open("/dev/input/mouse0", O_RDONLY); if (fd_mouse < 0) { perror("open err"); return -1; } printf("fd_moust: %d\n", fd_mouse); //1.创建pollfd类型结构体数组 struct pollfd fds[2]; //2. 添加关心的文件描述符到数组中,并赋予事件 fds[0].fd=0; //键盘 fds[0].events=POLLIN; //监听读事件 //fds[0].revents 是实际发生的事件 fds[1].fd=fd_mouse; //鼠标 fds[1].events=POLLIN; //监听读事件 //3.保存数组最后一个有效元素下标 int last=1; //4.调用poll函数监听,循环判断是那个文件描述符产生了IO事件 while (1) { ret = poll(fds,last+1, 2000); if(ret<0) { perror("poll err"); return -1; } else if(ret == 0) { printf("time out\n"); continue; } //5.判断结构体内文件描述符实际发生事件是否存在 if(fds[0].revents == POLLIN) { //6. 根据不同描述符触发的不同事件做对应的逻辑处理 fgets(buf,sizeof(buf),stdin); printf("key: %s\n", buf); } if(fds[1].revents == POLLIN) { int n=read(fd_mouse, buf, sizeof(buf)-1); buf[n]='\0'; printf("mouse:%s\n",buf); } } close(fd_mouse); return 0; }4.3. epoll
特点
1. 监听的最大的文件描述符没有个数限制
2. 异步IO,epoll当有事件产生被唤醒之后,文件描述符主动调用callback函数(回调函数)直接拿到唤醒的文件描述符,不需要轮询,效率高
3. epoll不需要重新构造文件描述符表,只需要从用户空间拷贝到内核空间一次。
IO多路复用总结
select | poll | epoll | |
监听个数 | 一个进程最多监听1024个文件描述符 | 由程序员自己决定 | 百万级 |
方式 | 每次都会被唤醒,都需要重新轮询 | 每次都会被唤醒,都需要重新轮询 | 红黑树内callback自动回调,不需要轮询 |
效率 | 文件描述符数目越多,轮询越多,效率越低 | 文件描述符数目越多,轮询越多,效率越低 | 不轮询,效率高 |
原理 | 每次使用select后,都会清空表 每次调用select,都需要拷贝用户空间的表到内核空间 内核空间负责轮询监视表内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环 | 不会清空结构体数组 每次调用poll,都需要拷贝用户空间的结构体到内核空间 内核空间负责轮询监视结构体数组内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环 | 不会清空表 epoll中每个fd只会从用户空间到内核空间只拷贝一次(上树时) 通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝 |
特点 | 一个进程最多能监听1024个文件描述符 select每次被唤醒,都要重新轮询表,效率低 select每次都清空未发生相应的文件描述符,每次都要拷贝用户空间的表到内核空间 | 优化文件描述符的个数限制 poll每次被唤醒,都要重新轮询,效率比较低(耗费cpu) poll不需要构造文件描述符表(也不需要清空表),采用结构体数组,每次也需要从用户空间拷贝到内核空间 | 监听的文件描述符没有个数限制(取决于自己的系统) 异步IO,epoll当有事件产生被唤醒,文件描述符会主动调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高 epoll不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。 |
结构 | 数组 | 数组 | 红黑树+就绪链表 |
开发复杂度 | 低 | 低 | 中 |
五、进程Process
1.什么是进程
1.1.概念
程序:编译好的可执行文件
存放在磁盘上的指令和数据的有序集合(文件)
程序是静态的,没有任何执行的概念
进程:一个独立的可调度的任务
执行一个程序所分配资源的总称
进程是程序的一次执行过程
进程是动态的,包括创建、调度、执行和消亡
1.2.特点
1. 系统会为每个进程分配0-4g的虚拟空间,其中0-3g是用户空间,每个进程独有;3g-4g是内核空间,所有进程共享
2. 轮转调度:时间片,系统为每个进程分配时间片(几毫秒~几十毫秒),当一个进程时间片用完时,CPU调度另一个进程,从而实现进程调度的切换 (没有外界干预是随机调度)
1.3. 进程段
Linux中的进程大致包含三个段:
数据段:存放的是全局变量、常数以及动态数据分配的数据空间(如malloc函数取得的空间)等。
正文段:存放的是程序中的代码
堆栈段:存放的是函数的返回地址、函数的参数以及程序中的局部变量 (类比内存的栈区)
1.4. 进程分类
交互进程:该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行。该类进程经常与用户进行交互,需要等待用户的输入,当接收到用户的输入后,该类进程会立刻响应,典型的交互式进程有:shell命令进程、文本编辑器等
批处理进程:该类进程不属于某个终端,它被提交到一个队列中以便顺序执行。(目前接触不到)
守护进程:该类进程在后台运行。它一般在Linux启动时开始执行,系统关闭时才结束。
1.5. 进程状态
1)运行态(TASK_RUNNING):R
指正在被CPU运行或者就绪的状态。这样的进程被成为runnning进程。
2)睡眠态(等待态):
可中断睡眠态(TASK_INTERRUPTIBLE)S:处于等待状态中的进程,一旦被该进程等待的资源被释放,那么该进程就会进入运行状态。
(只能通过特定的函数进行唤醒,是不能随便去中断的)
不可中断睡眠态(TASK_UNINTERRUPTIBLE)D:该状态的进程只能用wake_up()函数唤醒。
3)暂停态(TASK_STOPPED):T
当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。
4)死亡态:进程结束 X
5)僵尸态:Z 当进程已经终止运行,但还占用系统资源,要避免僵尸态的产生
< 高优先级
N 低优先级
s 会话组组长
l 多线程
+ 前台进程
1.6. 进程状态切换图
进程创建后,进程进入就绪态,当CPU调度到此进程时进入运行态,当时间片用完时,此进程会进入就绪态,如果此进程正在执行一些IO操作(阻塞操作)会进入阻塞态,完成IO操作(阻塞结束)后又可进入就绪态,等待CPU的调度,当进程运行结束即进入结束态
1.7. 进程相关命令
ps 查看系统中的进程 -aux -ef
top 动态显示系统的进程
nice 按用户指定的优先级运行进程
renice 改变正在运行进程的优先级
kill 发送信号给进程
jobs查看当前终端的后台进程
bg 将进程切换到后台执行
fg 将进程切换到前台执行
2.进程函数
2.1. 创建进程fork()
#include <sys/types.h> #include <unistd.h> pid_t fork(void); 功能:创建子进程 参数:无 返回值: 成功:在父进程中,返回子进程的进程号 > 0 在子进程中,返回值为0 失败:-1 并设置errno#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; pid = fork(); if(pid < 0) { perror("fork err"); return -1; } // 当返回值为0,相当于在子进程中运行 else if(pid == 0) { printf("in then child\n"); // while(1); } // 当返回值大于零的时候相当于在父进程中运行 else { printf("in the parent\n"); } return 0; }解释:./a.out会启动一个进程,执行到fork()函数时会在当前进程中创造了一个子进程并把代码以及数据信息拷贝到子进程,这两个进程只有个别数据例如进程号不一样,此时这两个进程由CPU随机调度。注意!!子进程会得到fork函数返回值然后执行fork之后的代码,fork函数之前的代码不会执行。
特点:
1)子进程几乎拷贝了父进程的全部内容。包括代码、数据、系统数据段中的pc值、栈中的数据、父进程中打开的文件等;但它们的PID、PPID是不同的。
2)父子进程有独立的地址空间,互不影响;当在相应的进程中改变全局变量、静态变量,都互不影响。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; int num = 10; pid = fork(); if(pid < 0) { perror("fork err"); return -1; } // 当返回值为0,相当于在子进程中运行 else if(pid == 0) { num++; printf("in then child %d\n", num); } // 当返回值大于零的时候相当于在父进程中运行 else { sleep(2); printf("in the parent %d\n", num); } while(1); return 0; }3) fork之前的代码会被复制但是不会被重新执行一遍,fork之后的代码会被复制,并且父子进程分别执行一遍
4) fork之前打开的文件,fork之后会拿到同一个文件描述符,操作同一个文件指针
5) 若父进程先结束,子进程成为孤儿进程,被init进程收养,子进程变成后台进程。
6)若子进程先结束,父进程如果没有及时回收,子进程变成僵尸进程(要避免僵尸进程产生)
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; pid = fork(); if(pid < 0) { perror("fork err"); return -1; } // 当返回值为0,相当于在子进程中运行 else if(pid == 0) { printf("in then child %d\n", pid); } // 当返回值大于零的时候相当于在父进程中运行 else { printf("in the parent %d\n", pid); while(1); } return 0; }2.2. 回收资源
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *wstatus); 功能:回收子进程资源(阻塞) 参数:wstatus:子进程退出状态,不接受子进程状态设为NULL 返回值:成功:回收的子进程的进程号 失败:-1 pid_t waitpid(pid_t pid, int *wstatus, int options); 功能:回收子进程资源 参数: pid:> 0 指定子进程进程号 == -1 任意子进程 == 0 等待其组ID等于调用进程的组ID的任一子进程 < -1 等待其组ID等于pid的绝对值的任一子进程 wstatus:子进程退出状态 options:0 阻塞 WNOHANG:非阻塞 (没有子进程退出立刻返回) 返回值:正常:回收的子进程的进程号 当使用选项WNOHANG且没有子进程结束时:0 失败:-1#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char const *argv[]) { pid_t pid; pid = fork(); if(pid < 0) { perror("fork err"); return -1; } // 当返回值为0,相当于在子进程中运行 else if(pid == 0) { sleep(2); // 让子进程等待一会结束 printf("in then child %d\n", pid); } // 当返回值大于零的时候相当于在父进程中运行 else { printf("in the parent %d\n", pid); // wait(NULL); // 回收子进程资源 // 0: 阻塞 // WNOHANG:非阻塞,有可能调用的时候,子进程还没有结束回收不到资源还是会产生僵尸 // 需要轮询 // waitpid(-1, NULL, 0); while(1) { if(waitpid(-1, NULL, WNOHANG) > 0) break; } } return 0; }2.3. 结束进程
#include <stdlib.h> void exit(int status); 功能:结束进程,刷新缓存 #include <unistd.h> void _exit(int status); 功能:结束进程,不刷新缓存 参数:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。 通常0表示正常结束; 其他数值表示出现了错误#include <stdio.h> #include <stdlib.h> #include <unistd.h> int fun() { printf("hello"); // exit(0); // 刷新缓存区 // _exit(0); // 不刷新缓存区 // 返回函数调用位置,继续向下执行代码 return 0; } int main(int argc, char const *argv[]) { fun(); while(1); return 0; }补充:
exit和return区别:
exit:不管在子函数还是主函数,都可以结束进程
return:当子函数中有return时返回到函数调用位置,并不结束进程
2.4. 获取进程号
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); 功能:获取当前进程的进程号 pid_t getppid(void); 功能:获取当前进程的父进程号#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; pid = fork(); if(pid < 0) { perror("fork err"); return -1; } // 当返回值为0,相当于在子进程中运行 else if(pid == 0) { sleep(2); printf("in then child %d %d\n", getpid(), getppid()); } // 当返回值大于零的时候相当于在父进程中运行 else { printf("in the parent %d %d\n", pid, getpid()); } return 0; } 两者出现不一致的情况是,父进程结束了,子进程成为了孤儿进程六、进程间通信IPCInterProcess Communication
1、进程间通信方式IPC
1) 早期的进程间通信:
无名管道(pipe)、有名管道(fifo)、信号(signal)
2) system V IPC对象
共享内存(share memory)、信号灯集(semaphore)、消息队列(message queue)
3) BSD
socket套接字
2. 无名管道
2.1. 特点
1) 只能用于具有亲缘关系的进程之间通信
2) 具有固定的读端和写端,半双工通信模式
单工:只能单向通信 广播
半双工:可以双向通信,但是同一时间不可以同时发送 对讲机
全双工:可以双向同时通信 电话
3) 管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数
4) 管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符
fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。
2.2. 函数接口
int pipe(int fd[2]) 功能:创建无名管道 参数:文件描述符fd[0]:读端 fd[1]:写端 返回值:成功:0 失败:-13. 有名管道
3.1. 特点
1) 有名管道可以使互不相关的两个进程互相通信
2) 有名管道可以通过路径名来指出,并在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。
3) 进程通过文件IO来操作有名管道
4) 有名管道遵循先进先出规则
5) 不支持如lseek() 操作
3.2. 函数接口
int mkfifo(const char *filename, mode_t mode); 功能:创建有名管道 参数:filename:有名管道文件名 mode:权限 返回值:成功:0 失败: -1,并设置errno号#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> int main(int argc, char const *argv[]) { // 创建有名管道 // fifo管道文件的权限值是664 // 因为我指定的是666 它和umask取反之后按位相与嘛 if (mkfifo("fifo", 0666) < 0) { // 如果返回的错误码等于EEXIST我并不希望我们的程序退出 if (errno == EEXIST) { printf("file exist\n"); } // 如果是其他的错误,再让它打印错误信息,并return else { perror("mkfifo error"); return -1; } } printf("mkfifo success\n"); return 0; }补充:
1. 当管道文件存在(报错提示file exists)时的处理方式:
判断errno的值为EEXIST时,只是打印提示语句,if(errno == EEXIST)
2. 注意代码中出现errno,需要添加头文件#include <errno.h>
注意:函数只是在路径下创建管道文件,往管道中写的数据是存在内核空间中的。
步骤:先创建有名管道mkfifo,然后再文件IO的open获取文件描述符之后才能读写read/write文件。
4. 信号
kill -l:显示系统中的信号
kill -num PID:给某个进程发送信号
信号是进程通信方式中的唯一的一种异步的方式
同步:按照一定顺序去执行
异步:没有顺序的,它不要求先后顺序,它是来什么信号处理什么信号
4.1. 概念
● 信号是在软件层次上对中断机制的一种模拟,是一种 异步通信方式
● 信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
● 如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
4.2. 信号的响应方式
忽略信号:对信号不做任何的处理,但是有两个信号不能忽略:即:SIGKILL、SIGSTOP
捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数
执行默认(缺省)操作:Linux对每种信号都规定了默认操作
4.3. 信号种类
SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程
SIGQUIT(3):退出信号, Ctrl\ 产生,用于退出进程并生成核心转储文件
SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。
SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。
SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination
SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。
SIGCONT(18):继续执行信号,用于恢复先前停止的进程。
SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。
SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。
4.4. 函数接口
4.4.1. 信号发送和挂起
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); 功能:信号发送 参数:pid:指定的进程 sig:要发送的信号 返回值:成功:0 失败:-1 #include <signal.h> int raise(int sig); 功能:进程向自己发送信号 参数:sig:信号 返回值:成功:0 失败:-1 #include <unistd.h> int pause(void); 功能:用于将调用进程挂起,直到收到被捕获处理的信号为止#include <sys/types.h> #include <signal.h> #include <unistd.h> int main(int argc, char const *argv[]) { // kill(getpid(), SIGKILL); // raise(SIGKILL); // while(1); pause(); // 将进程挂起,作用和死循环类似,但是不占用CPU return 0; }父子之间进行信号发送
#include <stdio.h> #include <sys/types.h> #include <signal.h> #include <unistd.h> int main(int argc, char const *argv[]) { pid_t pid; pid = fork(); if(pid < 0) { perror("fork err"); return -1; } // 当返回值为0,相当于在子进程中运行 else if(pid == 0) { sleep(3); kill(getppid(), SIGKILL); } // 当返回值大于零的时候相当于在父进程中运行 else { while(1) { printf("in the parent %d %d\n", pid, getpid()); } } return 0; }4.4.2. 定时器
#include <unistd.h> unsigned int alarm(unsigned int seconds); 功能:在进程中设置一个定时器,当定时器指定的时间到了,会向进程发送SIGALRM信号 参数:seconds:定时时间,单位:秒s 返回值: 如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。 注意:一个进程只能有一个闹钟时间。如果在调用alarm时 已设置过闹钟时间,则之前的闹钟时间被新值所代替 常用操作:取消定时器alarm(0),返回旧闹钟余下秒数#include <stdio.h> #include <sys/types.h> #include <signal.h> #include <unistd.h> int main(int argc, char const *argv[]) { printf("%d\n", alarm(10)); // 第一次调用返回 0 sleep(2); printf("%d\n", alarm(3)); // 不是第一次调用,返回上一次闹钟剩余的时间 pause(); // 让进程不结束,等待闹钟 // linux 系统对SIGALRM默认处理方案就是结束进程 return 0; }4.4.3. 信号处理函数 signal
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 功能:信号处理函数 参数:signum:要处理的信号 handler:信号处理方式 SIG_IGN:忽略信号 (忽略 ignore) SIG_DFL:执行默认操作 (默认 default) handler:捕捉信号 (handler为函数名,可以自定义) void handler(int sig){} //函数名可以自定义, 参数为要处理的信号 返回值:成功:设置之前的信号处理方式 失败:-1#include <stdio.h> #include <sys/types.h> #include <signal.h> #include <unistd.h> void handler(int sig) { printf("handler:%d\n", sig); } int main(int argc, char const *argv[]) { // signal(SIGINT, SIG_IGN); // 忽略信号 // signal(SIGINT, SIG_DFL); // 执行默认信号 signal(SIGINT, handler); while(1); return 0; }用信号的知识实现司机和售票员问题。
1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)
2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)
3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)
4)司机等待售票员下车,之后司机再下车。
分析:司机(父进程)、售票员(子进程)
售票员:捕捉:SIGINT、SIGQUIT、SIGUSR1
忽略:SIGTSTP
司机:捕捉:SIGUSR1、SIGUSR2、SIGTSTP
忽略:SIGINT、SIGQUIT
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <signal.h> #include <unistd.h> #include <wait.h> pid_t pid; void saler(int sig) { if(sig == SIGINT) { kill(getppid(), SIGUSR1); } if(sig == SIGQUIT) { kill(getppid(), SIGUSR2); } if(sig == SIGUSR1) { printf("please get off the bus\n"); exit(0); } } void driver(int sig) { if(sig == SIGUSR1) { printf("let's gogogo\n"); } if(sig == SIGUSR2) { printf("stop the bus\n"); } if(sig == SIGTSTP) { kill(pid, SIGUSR1); wait(NULL); // 司机等待售票员下车 exit(0); // 司机下车 } } int main(int argc, char const *argv[]) { pid = fork(); if(pid < 0) { perror("fork err"); return -1; } // 当返回值为0,相当于在子进程中运行 else if(pid == 0) { signal(SIGINT, saler); signal(SIGQUIT, saler); signal(SIGUSR1, saler); signal(SIGTSTP, SIG_IGN); } // 当返回值大于零的时候相当于在父进程中运行 else { signal(SIGUSR1, driver); signal(SIGUSR2, driver); signal(SIGTSTP, driver); signal(SIGINT, SIG_IGN); signal(SIGQUIT, SIG_IGN); } while(1) pause(); return 0; }4.5. 信号的处理过程
程序运行在用户空间时->进程由于系统调用或中断进入内核->转向用户空间执行信号处理函数->信号处理函数完毕后进入内核->返回用户空间继续执行程序
5. 共享内存
5.1. 概念
共享内存指的是操作系统在物理内存中申请一块空间,应用程序可以映射到这块空间,进行直接读写操作
5.2. 特点
1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝
2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间
3)进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
4)由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等
5.3. 步骤
1. 创建唯一key值 ftok
2. 创建或打开共享内存 shmget
3. 映射共享内存到用户空间(拿到映射的地址后就可以操作共享内存) shmat
4. 撤销映射 shmdt
5. 删除共享内存 shmctl
5.4. 函数接口
5.4.1. 创建key值
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id); 功能:创建key值 参数: pathname: 文件名 proj_id:取整数的低8位数据 返回值:成功:key值 失败:-1#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> int main(int argc, char const *argv[]) { key_t key; int shmid; // 共享内存id key = ftok("./app", 'a'); if (key < 0) { perror("ftok err"); } printf("%#x\n", key); return 0; }补充:
key值是根据pathname的inode号和proj_id的低8位组合而成的。如:0x61013096
pathname只要是路径中存在的文件即可
ls -i 查看文件inode号
5.4.2. 创建共享内存
#include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg); 功能:创建或打开共享内存 参数:key: 键值 size:共享内存的大小 创建 检测错误 shmflg:IPC_CREAT | IPC_EXCL | 0777 创建共享内存时候的权限 返回值:成功:shmid 共享内存的id 出错:-1#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> int main(int argc, char const *argv[]) { key_t key; int shmid; // 共享内存id key = ftok("./app", 'a'); if (key < 0) { perror("ftok err"); } printf("%#x\n", key); // 创建共享内存 shmid = shmget(key, 64, IPC_CREAT | IPC_EXCL | 0666); if (shmid <= 0) { if (errno == EEXIST) { shmid = shmget(key, 64, 0666); } else { perror("shmget err"); return -1; } } printf("%d\n", shmid); return 0; }查看创建的共享内存的命令:ipcs -m
再次执行时会出现报错的情况:
我第一次执行的时候是不是已经创建了这个共享内存,当再一次去执行的时候,肯定会提示文件已存在,共享内存已经存在了
解决方案:
5.4.3. 映射共享内存
#include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg); 功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问 参数:shmid:共享内存的id号 shmadd:一般为NULL,表示由系统自动完成映射 如果不为NULL,那么由用户指定 shmflg:SHM_RDONLYH就是对该共享内存进行只读操作 0 可读可写 返回值:成功:完成映射后的地址 出错:(void *)-1的地址#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> int main(int argc, char const *argv[]) { key_t key; int shmid; // 共享内存id key = ftok("./app", 'a'); if (key < 0) { perror("ftok err"); } printf("%#x\n", key); // 创建共享内存 shmid = shmget(key, 64, IPC_CREAT | IPC_EXCL | 0666); if (shmid <= 0) { if (errno == EEXIST) { shmid = shmget(key, 64, 0666); } else { perror("shmget err"); return -1; } } printf("%d\n", shmid); // 映射共享内存 // 让系统完成映射 char *p = shmat(shmid, NULL, 0); if(p == (char *)-1) { perror("shmat err"); return -1; } // 阻塞,使用命令查看有没有映射成功 getchar(); return 0; }5.4.4. 取消映射
#include <sys/shm.h> int shmdt(const void *shmaddr); 功能:取消映射 参数:shmaddr:要取消映射的共享内存地址 返回值:成功:0 失败:-15.4.5. 删除共享内存
#include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); 功能:(删除共享内存), 对共享内存进行各种操作 参数:shmid 共享内存id cmd IPC_STAT 获取shmid属性信息,存放在第三个参数 IPC_SET设置shmid属性信息,要设置的属性存放在第三个参数 IPC_RMID删除共享内存,此时第三个参数为NULL buf 是一个结构体指针,但是我们是删除共享内存,所以没有意义,我们直接设置为NULL就可以 返回值:成功 0 失败 -15.5. 操作命令:
ipcs -m: 查看系统中的共享内存
ipcrm -m shmid:删除共享内存
ps: 可能不能直接删除掉还存在进程使用的共享内存。
这时候可以用ps -ef对进程进行查看,kill掉多余的进程后,再使用ipcs查看。
5.6.实例:
input.c
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> #include <string.h> int main(int argc, char const *argv[]) { key_t key; int shmid; // 共享内存id key = ftok("./app", 'a'); if (key < 0) { perror("ftok err"); } printf("%#x\n", key); // 创建共享内存 shmid = shmget(key, 64, IPC_CREAT | IPC_EXCL | 0666); if (shmid <= 0) { if (errno == EEXIST) { shmid = shmget(key, 64, 0666); } else { perror("shmget err"); return -1; } } printf("%d\n", shmid); // 映射共享内存 // 让系统完成映射 char *p = shmat(shmid, NULL, 0); if(p == (char *)-1) { perror("shmat err"); return -1; } while(1) { scanf("%s", p); if(!strcmp(p, "quit")) break; } return 0; }output.c
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> #include <string.h> int main(int argc, char const *argv[]) { key_t key; int shmid; // 共享内存id key = ftok("./app", 'a'); if (key < 0) { perror("ftok err"); } printf("%#x\n", key); // 创建共享内存 shmid = shmget(key, 64, IPC_CREAT | IPC_EXCL | 0666); if (shmid <= 0) { if (errno == EEXIST) { shmid = shmget(key, 64, 0666); } else { perror("shmget err"); return -1; } } printf("%d\n", shmid); // 映射共享内存 // 让系统完成映射 char *p = shmat(shmid, NULL, 0); if(p == (char *)-1) { perror("shmat err"); return -1; } while(1) { if(!strcmp(p, "quit")) break; printf("p:%s\n", p); } // 使用完成之后需要取消映射 shmdt(p); // 删除共享内存 shmctl(shmid, IPC_RMID, NULL); return 0; }6. 信号灯集
6.1. 特点
信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制;
System V信号灯集是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。而Posix信号灯指的是单个计数信号灯。
通过信号灯集实现共享内存的同步操作
6.2. 步骤
1. 创建key值:ftok
2. 创建或打开信号灯集: semget
3. 初始化信号灯: semctl
4. PV操作:semop
5. 删除信号灯集: semctl
6.3. 操作命令
ipcs -s:查看信号灯集
ipcrm -s semid:删除信号灯集
6.4. 函数接口
6.4.1. 创建信号灯集
#include <sys/sem.h> int semget(key_t key, int nsems, int semflg); 功能:创建/打开信号灯 参数:key:ftok产生的key值 nsems:信号灯集中包含的信号灯数目 semflg:信号灯集的访问权限,通常为IPC_CREAT | 0666 返回值:成功:信号灯集ID 失败:-1#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <errno.h> int main(int argc, char const *argv[]) { int semid; key_t key; key = ftok("./app", 'a'); if (key < 0) { perror("ftok err"); return -1; } semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666); if (semid <= 0) { if (errno == EEXIST) { semid = semget(key, 2, 0666); } else { perror("semid err"); return -1; } } printf("%d\n", semid); return 0; }出现这种情况就是semid等于0了
我们可以手动去删除这个信号灯集:
ipcs -s :查看创建的信号灯集
ipcrm -s [semid]:删除信号灯集
6.4.2. 初始化或删除信号灯集
#include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...); 功能:信号灯集的控制(初始化、删除) 参数:semid:信号灯集id semnum:要操作集合中的信号灯编号 cmd: GETVAL:获取信号灯的值 SETVAL:设置信号灯的值 IPC_RMID:从系统中删除信号灯集合 ...:当cmd为SETVAL,需要传递共用体 返回值:成功 0 失败 -1 共用体格式: union semun { int val; /* 信号量的初值 */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ };补充:
1. 当cmd为SETVAL时需要传递第四个参数,类型为共用体,
用法:
union semun { int val; }; union semun sem; sem.val = 10; semctl(semid, 0, SETVAL, sem); //对编号为0的信号灯设置初值为101. 当cmd为IPC_RMID时,表示删除信号灯集
用法:semctl(semid, 0, IPC_RMID) // 0:表示信号灯的编号,指定任意一个即可删除
2. 当cmd为GETVAL时,表示获取信号灯的值
用法:printf("%d\n", semctl(semid, 0, GETVAL));
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <errno.h> union semnum { int val; }; int main(int argc, char const *argv[]) { int semid; key_t key; key = ftok("./app", 'a'); if (key < 0) { perror("ftok err"); return -1; } semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666); if (semid <= 0) { if (errno == EEXIST) { semid = semget(key, 2, 0666); } else { perror("semid err"); return -1; } } else { union semnum sem; sem.val = 10; semctl(semid, 0, SETVAL, sem);// 对编号为0的信号灯设置初值为10 sem.val = 0; semctl(semid, 1, SETVAL, sem);// 对编号为1的信号灯设置初值为0 } printf("%d\n", semid); // 获取信号灯的值 printf("%d\n", semctl(semid, 0, GETVAL)); printf("%d\n", semctl(semid, 1, GETVAL)); return 0; }6.4.3. pv操作
int semop ( int semid, struct sembuf *opsptr, size_t nops); 功能:对信号灯集合中的信号量进行PV操作 参数:semid:信号灯集ID opsptr:操作方式 nops: 要操作的信号灯的个数 1个 返回值:成功 :0 失败:-1 struct sembuf { short sem_num; // 要操作的信号灯的编号 short sem_op; // 0 : 等待,直到信号灯的值变成0 // 1 : 释放资源,V操作 // -1 : 分配资源,P操作 short sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO };使用:
申请资源 P操作:
mysembuf.sem_num =0;
mysembuf.sem_op =-1;
mysembuf.sem_flg =0;
semop(semid,&mysembuf,1);
释放资源 V操作:
mysembuf.sem_num =0;
mysembuf.sem_op =1;
mysembuf.sem_flg =0;
semop(semid,&mysembuf,1);
实例:
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <errno.h> union semun { int val; }; int main(int argc, char const *argv[]) { int semid; key_t key; key = ftok("./app", 'a'); if (key < 0) { perror("ftok error"); return -1; } // 创建信号灯集 semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666); if (semid <= 0) { if (errno == EEXIST) semid = semget(key, 2, 0666); else { perror("semget error"); return -1; } } else { // 初始化(只需要在创建时进行初始化即可) // 我们初始化的操作执行一次就可以了,避免重复的初始化 union semun sem; sem.val = 10; semctl(semid, 0, SETVAL, sem); // 对编号为0的信号灯设置初值为10 sem.val = 0; semctl(semid, 1, SETVAL, sem); // 对编号为1的信号灯设置初值为0 } printf("%d\n", semid); // 获取信号灯的值 printf("%d\n", semctl(semid, 0, GETVAL)); printf("%d\n", semctl(semid, 1, GETVAL)); // p操作(申请资源) struct sembuf buf = {0, -1, 0}; // 编号为0的信号灯进行p操作(申请资源) semop(semid, &buf, 1); // v操作(释放资源) buf.sem_num = 1; // 编号为1的信号灯进行v操作(释放资源) buf.sem_op = 1; buf.sem_flg = 0; semop(semid, &buf, 1); // 获取信号灯的值 printf("%d\n", semctl(semid, 0, GETVAL)); printf("%d\n", semctl(semid, 1, GETVAL)); // 删除信号灯集 semctl(semid, 0, IPC_RMID); return 0; }7. 消息队列
7.1. 特点
消息队列是IPC对象的一种,(活动在内核级别的一种进程间通信的工具)
1. 消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。
2. 消息队列可以按照类型来发送/接收消息
3. 在linux下消息队列的大小有限制。
● 消息队列个数最多为16个;
● 消息队列总容量最多为16384字节;
● 每个消息内容最多为8192字节。
消息是通过链表的方式依次进行添加,可以通过类型来区分添加的是那种类型的数据,同种类型的数据在读取的时候是按照队列的方式读取的,不同类型的数据是按照类型进行读取
7.2. 步骤
1. 创建key值
2. 创建或打开消息队列 msgget (message deque)
3. 添加消息 msgsnd (send)
4. 读取消息 msgrcv (recive)
5. 删除消息队列 msgctl
7.3. 操作命令
ipcs -q: 查看消息队列
ipcrm -q msgid: 删除消息队列
注意:有时候可能创建失败或者msgid为0,所以用命令看看,删了重新创建就可以了。
76.+
7.4. 函数接口
int msgget(key_t key, int flag); 功能:创建或打开一个消息队列 参数: key值 flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666 返回值:成功:msgid 失败:-1 int msgsnd(int msgid, const void *msgp, size_t size, int flag); 功能:添加消息 参数:msgid:消息队列的ID msgp:指向消息的指针。常用消息结构msgbuf如下: struct msgbuf{ long mtype; //消息类型 值>0 char mtext[N]}; //消息正文 } size:发送的消息正文的字节数 flag:IPC_NOWAIT消息没有发送完成函数也会立即返回 0:直到发送完成函数才返回 返回值:成功:0 失败:-1 使用:msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0) 注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。 int msgrcv(int msgid, void* msgp, size_t size, long msgtype, int flag); 功能:读取消息 参数:msgid:消息队列的ID msgp:存放读取消息的空间 size:接受的消息正文的字节数(sizeof(msgp)-sizeof(long)) msgtype: 0:接收消息队列中第一个消息。 大于0:接收消息队列中第一个类型为msgtyp的消息. 小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。 flag: 0:若无消息函数会一直阻塞 IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG 返回值:成功:接收到的消息的长度 失败:-1 int msgctl ( int msgqid, int cmd, struct msqid_ds *buf ); 功能:对消息队列的操作,删除消息队列 参数:msqid:消息队列的队列ID cmd: IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。 IPC_SET:设置消息队列的属性。这个值取自buf参数。 IPC_RMID:从系统中删除消息队列。 buf:消息队列缓冲区 返回值:成功:0 失败:-1 用法:msgctl(msgid, IPC_RMID, NULL);#include <stdio.h> #include <sys/msg.h> #include <sys/types.h> #include <sys/ipc.h> #include <errno.h> #include <string.h> struct msgbuf { long mtype; // 消息类型 char ch[32]; // 消息正文 int n; }; int main(int argc, char const *argv[]) { int msgid; key_t key; key = ftok("./app", 'a'); if (key < 0) { perror("ftok err"); } // 创建消息队列 msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666); if (msgid <= 0) { if (errno == EEXIST) { msgid = msgget(key, 0666); } else { perror("msgget err"); return -1; } } printf("msgid: %d\n", msgid); // 添加消息 struct msgbuf msg; msg.mtype = 1; strcpy(msg.ch, "hello"); msg.n=10; msgsnd(msgid, &msg, sizeof(msg)-sizeof(long), 0); msg.mtype = 2; strcpy(msg.ch, "world"); msg.n=20; msgsnd(msgid, &msg, sizeof(msg)-sizeof(long), 0); msg.mtype = 2; strcpy(msg.ch, "hahah"); msg.n=300; msgsnd(msgid, &msg, sizeof(msg)-sizeof(long), 0); // 读取消息 struct msgbuf m; msgrcv(msgid, &m, sizeof(m)-sizeof(long), 2, 0); printf("%s %d\n", m.ch, m.n); msgrcv(msgid, &m, sizeof(m)-sizeof(long), 2, 0); printf("%s %d\n", m.ch, m.n); // 删除消息队列 msgctl(msgid, IPC_RMID, NULL); return 0; }两个进程通信示例:
七、线程
1.概念
线程是一个轻量级的进程,为了提高系统的性能引入线程。
线程和进程都参与统一的调度。
在同一个进程中可以创建多个线程,并且共享进程资源。
2.进程和线程区别(面试题)
相同点:都为操作系统提供了并发执行的能力
不同点:
资源和调度:进程是系统资源分配的最小单位,线程是资源调度的最小单位
地址空间方面:每个进程都有独立的地址空间;同一个进程中的多个线程共享进程地址空间
通信方面:线程通信相对简单,只需要通过全局变量就可以实现,但是需要考虑临界资源访问的问题; 进程通信比较复杂,需要借助进程间的通信机制(3-4g的内核空间)。
安全性方面:线程安全性差一些,当进程结束时会导致所有线程退出; 进程相对安全。
面试题:程序什么时候该使用线程?什么时候用进程?(深圳棱镜空间智能科技有限公司)(北京明朝万达)
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高、速度快的高并发环境时,需要频繁创建、销毁或切换时,资源的保护管理要求不是很高时,使用多线程。
3.线程资源
共享的资源:可执行的指令、静态数据、进程中打开的文件描述符、信号处理函数、当前工作目录、用户ID、用户组ID
私有的资源:线程ID (TID)、PC(程序计数器)和相关寄存器、堆栈(局部变量, 返回地址)、错误号 (errno)、信号掩码和优先级、执行状态和属性
4.函数接口
4.1. 创建线程:pthread_create
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg); 功能:创建线程 参数:thread: 线程标识 attr:线程属性,NULL代表设置默认属性 start_routine:函数名:代表线程函数(自己写) arg:用来给前面函数传参 返回值:成功:0 失败:错误码 编译的时候需要加 -lpthread 链接动态库4.2.退出线程:pthread_exit
#include <pthread.h> void pthread_exit(void *retval); 功能:用于退出线程的执行 参数:retval:线程退出时返回的值#include <stdio.h> #include <pthread.h> #include <unistd.h> void *handler(void *arg) { printf("in the thread\n"); pthread_exit(NULL); // 让线程退出 while(1); return NULL; } int main(int argc, char const *argv[]) { pthread_t tid; if(pthread_create(&tid, NULL, handler, NULL) != 0) { perror("create thread err"); } // 主线程 printf("in the main\n"); while(1); return 0; }线程结束之后不是多线程状态了,此时只剩下一个主线程
4.3. 回收线程资源
#include <pthread.h> int pthread_join(pthread_t thread, void **retval); 功能:用于等待一个指定的线程结束,阻塞函数 参数:thread:创建的线程对象,线程ID retval:指针*value_ptr指向线程返回的参数,一般为NULL 返回值:成功:0 失败:errno int pthread_detach(pthread_t thread); 功能:让线程结束时自动回收线程资源,让线程和主线程分离,非阻塞函数 参数:thread:线程ID 非阻塞式的,例如主线程分离(detach)了线程T2,那么主线程不会阻塞在pthread_detach(),pthread_detach()会直接返回,线程T2终止后会被操作系统自动回收资源#include <stdio.h> #include <pthread.h> #include <unistd.h> void *handler(void *arg) { printf("in the thread\n"); sleep(2); pthread_exit(NULL); // 让线程退出 while(1); return NULL; } int main(int argc, char const *argv[]) { pthread_t tid; if(pthread_create(&tid, NULL, handler, NULL) != 0) { perror("create thread err"); } // pthread_join(tid, NULL); // 阻塞等待指定线程退出回收资源 pthread_detach(tid); // 非阻塞,让指定的线程为分离态,结束时自动回收资源 // 主线程 printf("in the main\n"); while(1); return 0; }4.4.练习:输入输出,quit结束
通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,当输入quit结束程序。
1) 全局变量
2) 加上标志位(flag),实现主线程输入一次修改标志位,从线程打印一次也修改标志位, int flag=0;
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <string.h> char buf[32] = {}; int flag = 0; void *handler(void *arg) { while (1) { if (flag == 1) { if (!strcmp(buf, "quit")) break; printf("%s\n", buf); flag = 0; } } return NULL; } int main(int argc, char const *argv[]) { pthread_t tid; if (pthread_create(&tid, NULL, handler, NULL) != 0) { perror("create thread err"); } while (1) { scanf("%s", buf); flag = 1; if (!strcmp(buf, "quit")) break; } pthread_join(tid, NULL); return 0; }在应用层中有这两个概念:
同步:现在有两件事都是你应该去做的事情,那么这两件事是不是有先后的顺序
异步:我在做一件事情的同时还可以去做另一件事情
5.线程同步
5.1. 概念:
多个线程(任务)按照约定的顺序相互配合完成一件事情
5.2. 同步机制:
通过信号量实现线程同步
信号量:通过信号量实现同步操作; 由信号量来决定线程继续运行还是阻塞等待。
信号量:代表一类资源,其值可以表示系统中该资源的数量。
信号量的值>0: 表示有资源可以用,可以申请到资源。
信号量的值<=0: 表示没有资源可以用,无法申请到资源,阻塞。
信号量:还是受保护的变量,只能通过三种操作来访问:初始化,P操作(申请资源),V操作(释放资源)
sem_init:信号量初始化
sem_wait:申请资源,P操作,如果没有资源可用则阻塞,否则就申请到资源 -1
sem_post:释放资源,V操作,非阻塞,+1
int sem_init(sem_t *sem, int pshared, unsigned int value) 功能:初始化信号量 参数:sem:初始化的信号量对象 pshared:信号量共享的范围(0: 线程间使用 非0:1进程间使用) value:信号量初值 返回值:成功 0 失败 -1 int sem_wait(sem_t *sem) 功能:申请资源 P操作 参数:sem:信号量对象 返回值:成功 0 失败 -1 注:此函数执行过程,当信号量的值大于0时,表示有资源可以用,则继续执行,同时对信号量减1;当信号量的值等于0时,表示没有资源可以使用,函数阻塞 int sem_post(sem_t *sem) 功能:释放资源 V操作 参数:sem:信号量对象 返回值:成功 0 失败 -1 注:释放一次信号量的值加1,函数不阻塞#include <stdio.h> #include <semaphore.h> int main(int argc, char const *argv[]) { sem_t sem; if(sem_init(&sem, 0, 1) < 0) { perror("sem init err"); return -1; } // 申请资源 -> P操作 sem_wait(&sem); printf("hello\n"); // 释放资源 sem_post(&sem); sem_wait(&sem); // 没有释放资源,会进入阻塞状态 printf("world\n"); return 0; }练习:通过信号量实现线程同步:主线程循环从终端输入字符串,子线程循环将字符串打印至终端,当输入"quit"时结束
6.线程互斥
6.1. 互斥概念
多个线程访问临界资源时,同一时间只能一个线程访问
临界资源:多个线程共同访问的数据,且一次仅允许一个线程所使用的资源
通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。
互斥锁的操作方式:初始化
申请锁(上锁) :阻塞
当申请不到锁时(表示:锁被其它线程占用),是阻塞的
释放锁(解锁) :非阻塞
注意:上锁和解锁需要成对存在
6.2. 函数接口
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr) 功能:初始化互斥锁 参数:mutex:互斥锁 attr: 互斥锁属性 // NULL表示缺省属性 返回值:成功 0 失败 -1 int pthread_mutex_lock(pthread_mutex_t *mutex) 功能:申请互斥锁 参数:mutex:互斥锁 返回值:成功 0 失败 -1 注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回 int pthread_mutex_unlock(pthread_mutex_t *mutex) 功能:释放互斥锁 参数:mutex:互斥锁 返回值:成功 0 失败 -1 int pthread_mutex_destroy(pthread_mutex_t *mutex) 功能:销毁互斥锁 参数:mutex:互斥锁6.3.实例
案例:两个线程,一个线程倒置全局数组中的数,另一个线程遍历数组中数据,每隔1s打印一次。 int a[10] = {1,2,3,4,5,6,7,8,9,0};
#include <stdio.h> #include <pthread.h> #include <unistd.h> int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; pthread_mutex_t lock; void *swap_handler(void *arg) { int t; while(1) { pthread_mutex_lock(&lock); for(int i = 0; i < 5; i++) { t = arr[i]; arr[i] = arr[9-i]; arr[9-i] = t; } pthread_mutex_unlock(&lock); } } void *print_handler(void *arg) { while(1) { pthread_mutex_lock(&lock); for(int i = 0; i <= 9; i++) { printf("%d ", arr[i]); } printf("\n"); pthread_mutex_unlock(&lock); sleep(1); } } int main(int argc, char const *argv[]) { pthread_t tid1, tid2; if (pthread_create(&tid1, NULL, swap_handler, NULL) != 0) { perror("create thread err"); } if (pthread_create(&tid2, NULL, print_handler, NULL) != 0) { perror("create thread err"); } if(pthread_mutex_init(&lock, NULL) != 0) { perror("mutex init err"); return -1; } pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }补充:死锁
是指两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁产生的四个必要条件
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
7.条件变量
7.1.概念
条件变量(cond)用于在线程之间传递信号,以便某些线程可以等待某些条件发生。当某些条件发生时,条件变量会发出信号,使等待该条件的线程可以恢复执行。
7.2. 函数接口
一般和互斥锁搭配使用,来实现同步机制:
pthread_cond_init(&cond,NULL); //初始化条件变量
使用前需要上锁:
pthread_mutex_lock(&lock); //上锁
一些逻辑
pthread_cond_wait(&cond, &lock); //阻塞等待条件产生,没有条件产生时阻塞,同时解锁,当条件产生时结束阻塞,再次上锁。
执行线程里面的逻辑
pthread_mutex_unlock(&lock); / /解锁
pthread_cond_signal(&cond); //产生条件,不阻塞
pthread_cond_destroy(&cond); //销毁条件变量
注意:必须保证让pthread_cond_wait先执行pthread_cond_sginal再产生条件。