2025/12/22 16:28:04
网站建设
项目流程
qq电脑版官方网站,华为erp企业管理系统软件,分析 网站,网站建设文献综述范文学习方法1. 重点放在函数的应用#xff0c;不需要关心底层逻辑#xff0c;不要太钻牛角尖太探究内核的东西#xff0c;把当下学习的内容都掌握了已经很好了#xff0c;可以以后有能力了再继续研究更底层的#xff0c;现在先把重点放在应用层。2. 好好理解概念#xff0c;…学习方法1. 重点放在函数的应用不需要关心底层逻辑不要太钻牛角尖太探究内核的东西把当下学习的内容都掌握了已经很好了可以以后有能力了再继续研究更底层的现在先把重点放在应用层。2. 好好理解概念但是需要记住的例如函数的功能必须要记住函数的参数记住重要的可以把代码多敲几遍自然而然就记住了但是要带着脑子敲代码。3. 练习题主要是记住逻辑不要死记硬背代码。大纲IOinput、output标准IO文件IO库Linux IO模型进程process进程基础进程间通信无名管道(pipe)、有名管道(fifo)、信号(signal)、共享内存(shared memory)、消息队列(message queue)、信号灯集(semphore set)线程(thread)、同步、互斥、条件变量一、标准IO1、什么是标准IO1.1 概念标准IO:在C库中定义的一组专门用来输入输出的函数1.2. 特点1)通过缓冲机制减少系统调用提高效率系统调用内核向上提供的一组接口2)围绕着流进行操作流FILE*来描述FILE代表的是结构体用 vi 的ctags索引先建立ctags索引密码11) vi -t 查找的名称输入序号回车2) 继续追踪将光标定位到要追踪的内容ctrl ]回退ctrl t3) 跳转到上次位置ctrl o跳转到下次位置ctrl iq退出vscode索引‘跳转到定义ctrl 左键前后跳转ctrl alt -ctrl shift -3)默认打开了三个流stdin(标准输入)、stdout(标准输出)、stderr(标准错误)1.3. 操作打开文件fopen关闭文件fclose读写操作fgets、fputs、fread、fwrite定位操作rewind、fseek、ftell2.缓存区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、fwrite3.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、fwritefread#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. 文件定位操作rewindvoid 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);ftelllong 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 #includestring.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; }二、文件IO1.什么是文件IO1.1. 概念又称系统IO是系统调用是操作系统提供的函数接口在posix(可移植操作系统接口)中定义的一组输入输出的函数1.2. 特点1. 没有缓冲机制每次操作都会经过系统调用效率比较低2. 围绕着文件描述符进行操作文件描述符是非负整数0、1、23. 默认打开三个文件描述符0(标准输入)、1(标准输出)、2(标准错误)4. 除目录外其他任意类型的文件都可以操作b、c、-、l、s、p问题打开三个文件描述符3、4、5关闭3以后重新打开文件描述符是几答还是3问题一个进程的文件描述符最大到几最多能打开多少个文件描述符最多能打开多少个文件?答一个进程的文件描述符最大到1023(0-1023),最多能打开1024个文件描述符, 最多能打开1024-31021个文件。1.3. 操作打开文件open关闭文件close读写操作read、write定位操作lseek2.函数接口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文件IOr 只读不会创建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和依次为01和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 n0; while((nread(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\stderr4. 只操作普通文件5. 可移植性相对较好1. 无缓冲区每次操作都引起系统调用2. 围绕文件描述符操作3. 默认打开三个文件描述符0\1\24. 除目录外其他文件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:dlllib库文件include:头文件2.库的分类静态库、动态库本质的区别是代码载入的时刻不同。2.1. 静态库静态库是在程序编译时会被复制到目标代码中以.a结尾优点程序运行时将不再需要该静态库运行时无需加载库运行速度更快缺点静态库中的代码复制到了程序中因此体积较大静态库升级后程序需要重新编译链接2.2. 动态库动态库是在程序运行时才会被载入到代码中。也叫共享库以.so结尾优点程序在执行时加载动态库代码体积小程序升级更简单不同应用程序如果调用相同的库那么在内存里只需要有一份该共享库的实例。缺点运行时还需要动态库的存在移植性较差3.库的制作3.1. 静态库的制作1-将源文件编译生成目标文件gcc -c xxx.c -o xxx.o2-创建静态库用ar命令它将很多.o转换成.aar crs libxxx.a xxx.o静态库文件名的命名规范是以lib为前缀紧接着跟静态库名扩展名为.a3-测试使用静态库gcc xxx.c -L. -l指定库名 // -L指定库的路径 -l指定库名执行./a.out3.2. 动态库的制作1-我们用gcc来创建共享库gcc -fPIC -c xxx.c -o xxx.o-fPIC 创建与地址无关的编译程序 (就是不和路径进行关联)gcc -shared -o libxxx.so xxx.o2-测试动态库使用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. select4.1.1.特点1. 一个进程最多只能监听1024个文件描述符2. select被唤醒之后要重新轮询效率相对低3. select每次都会清空未发生响应的文件描述符每次拷贝都需要从用户空间到内核空间效率低开销大4.1.2.编程步骤1. 先构造一张关于文件描述符的表2. 清空表 FD_ZERO3. 将关心的文件描述符添加到表中 FD_SET4. 调用select函数5. 判断是哪一个或者式哪些文件描述符产生了事件 FD_ISSET6. 做对应的逻辑处理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. pollint 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].reventsPOLLIN) {}#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 ret0; 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].fd0; //键盘 fds[0].eventsPOLLIN; //监听读事件 //fds[0].revents 是实际发生的事件 fds[1].fdfd_mouse; //鼠标 fds[1].eventsPOLLIN; //监听读事件 //3.保存数组最后一个有效元素下标 int last1; //4.调用poll函数监听循环判断是那个文件描述符产生了IO事件 while (1) { ret poll(fds,last1, 2000); if(ret0) { 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 nread(fd_mouse, buf, sizeof(buf)-1); buf[n]\0; printf(mouse:%s\n,buf); } } close(fd_mouse); return 0; }4.3. epoll特点1. 监听的最大的文件描述符没有个数限制2. 异步IOepoll当有事件产生被唤醒之后文件描述符主动调用callback函数回调函数直接拿到唤醒的文件描述符不需要轮询效率高3. epoll不需要重新构造文件描述符表只需要从用户空间拷贝到内核空间一次。IO多路复用总结selectpollepoll监听个数一个进程最多监听1024个文件描述符由程序员自己决定百万级方式每次都会被唤醒都需要重新轮询每次都会被唤醒都需要重新轮询红黑树内callback自动回调不需要轮询效率文件描述符数目越多轮询越多效率越低文件描述符数目越多轮询越多效率越低不轮询效率高原理每次使用select后都会清空表每次调用select都需要拷贝用户空间的表到内核空间内核空间负责轮询监视表内的文件描述符将发生事件的文件描述符拷贝到用户空间再次调用select如此循环不会清空结构体数组每次调用poll都需要拷贝用户空间的结构体到内核空间内核空间负责轮询监视结构体数组内的文件描述符将发生事件的文件描述符拷贝到用户空间再次调用poll如此循环不会清空表epoll中每个fd只会从用户空间到内核空间只拷贝一次上树时通过epoll_ctl将文件描述符交给内核监管一旦fd就绪内核就会采用callback的回调机制来激活该fdepoll_wait便可以收到通知内核空间到用户空间的拷贝特点一个进程最多能监听1024个文件描述符select每次被唤醒都要重新轮询表效率低select每次都清空未发生相应的文件描述符每次都要拷贝用户空间的表到内核空间优化文件描述符的个数限制poll每次被唤醒都要重新轮询效率比较低耗费cpupoll不需要构造文件描述符表也不需要清空表采用结构体数组每次也需要从用户空间拷贝到内核空间监听的文件描述符没有个数限制取决于自己的系统异步IOepoll当有事件产生被唤醒文件描述符会主动调用callback函数拿到唤醒的文件描述符不需要轮询效率高epoll不需要构造文件描述符的表只需要从用户空间拷贝到内核空间一次。结构数组数组红黑树就绪链表开发复杂度低低中五、进程Process1.什么是进程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_RUNNINGR指正在被CPU运行或者就绪的状态。这样的进程被成为runnning进程。2睡眠态(等待态)可中断睡眠态TASK_INTERRUPTIBLES处于等待状态中的进程一旦被该进程等待的资源被释放那么该进程就会进入运行状态。(只能通过特定的函数进行唤醒是不能随便去中断的)不可中断睡眠态TASK_UNINTERRUPTIBLED该状态的进程只能用wake_up()函数唤醒。3暂停态TASK_STOPPED:T当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。4死亡态进程结束 X5僵尸态Z 当进程已经终止运行但还占用系统资源要避免僵尸态的产生 高优先级N 低优先级s 会话组组长l 多线程 前台进程1.6. 进程状态切换图进程创建后进程进入就绪态当CPU调度到此进程时进入运行态当时间片用完时此进程会进入就绪态如果此进程正在执行一些IO操作(阻塞操作)会进入阻塞态完成IO操作阻塞结束后又可进入就绪态等待CPU的调度当进程运行结束即进入结束态1.7. 进程相关命令ps 查看系统中的进程 -aux -eftop 动态显示系统的进程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子进程退出状态 options0 阻塞 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 Communication1、进程间通信方式IPC1) 早期的进程间通信无名管道(pipe)、有名管道(fifo)、信号(signal)2) system V IPC对象共享内存(share memory)、信号灯集(semaphore)、消息队列(message queue)3) BSDsocket套接字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. 信号种类SIGINT2中断信号Ctrl-C 产生用于中断进程SIGQUIT3退出信号 Ctrl\ 产生用于退出进程并生成核心转储文件SIGKILL9终止信号用于强制终止进程。此信号不能被捕获或忽略。SIGALRM14闹钟信号当由 alarm() 函数设置的定时器超时时产生。SIGTERM15终止信号用于请求终止进程。此信号可以被捕获或忽略。terminationSIGCHLD17子进程状态改变信号当子进程停止或终止时产生。SIGCONT18继续执行信号用于恢复先前停止的进程。SIGSTOP19停止执行信号用于强制停止进程。此信号不能被捕获或忽略。SIGTSTP20键盘停止信号通常由用户按下 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信号司机打印lets gogogo2售票员捕捉SIGQUIT代表停车信号向司机发送SIGUSR2信号司机打印stop the bus3司机捕捉SIGTSTP代表到达终点站信号向售票员发送SIGUSR1信号售票员打印please get off the bus4司机等待售票员下车之后司机再下车。分析司机(父进程)、售票员(子进程)售票员捕捉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(lets 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值 ftok2. 创建或打开共享内存 shmget3. 映射共享内存到用户空间(拿到映射的地址后就可以操作共享内存) shmat4. 撤销映射 shmdt5. 删除共享内存 shmctl5.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位组合而成的。如0x61013096pathname只要是路径中存在的文件即可ls -i 查看文件inode号5.4.2. 创建共享内存#include sys/shm.h int shmget(key_t key, size_t size, int shmflg); 功能创建或打开共享内存 参数key 键值 size共享内存的大小 创建 检测错误 shmflgIPC_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那么由用户指定 shmflgSHM_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值ftok2. 创建或打开信号灯集: semget3. 初始化信号灯: semctl4. PV操作semop5. 删除信号灯集: semctl6.3. 操作命令ipcs -s查看信号灯集ipcrm -s semid删除信号灯集6.4. 函数接口6.4.1. 创建信号灯集#include sys/sem.h int semget(key_t key, int nsems, int semflg); 功能创建/打开信号灯 参数keyftok产生的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. 删除消息队列 msgctl7.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发送的消息正文的字节数 flagIPC_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.n10; msgsnd(msgid, msg, sizeof(msg)-sizeof(long), 0); msg.mtype 2; strcpy(msg.ch, world); msg.n20; msgsnd(msgid, msg, sizeof(msg)-sizeof(long), 0); msg.mtype 2; strcpy(msg.ch, hahah); msg.n300; 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 flag0;#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操作如果没有资源可用则阻塞否则就申请到资源 -1sem_post释放资源V操作非阻塞1int 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再产生条件。