大纲
IO(input、output)
标准IO、文件IO、库、Linux IO模型
进程:process
进程基础、进程间通信:无名管道(pipe)、有名管道(fifo)、信号(signal)、共享内存(shared memory)、消息队列(message queue)、信号灯集(semphore set)
线程(thread)、同步、互斥、条件变量
1. 标准IO
标准IO是C语言标准库中提供的一组用于输入输出操作的函数,定义在<stdio.h>头文件中。1通过缓冲机制提高效率,2并围绕流(FILE *)进行操作,3标准IO默认打开三个流:stdin(标准输入)、stdout(标准输出)和stderr(标准错误)。
特点:
1) 通过缓冲机制减少系统调用,提高效率
缓冲机制:在数据传输过程中,通过在内存中设置一定大小的缓冲区,将数据临时存储在缓冲区中,以减少系统调用的次数,提高数据处理的效率。
系统调用:应用层与内核层之间的一组接口。
2) 围绕着流进行操作
流(stream)的概念:它将数据的输入输出看作是数据的流入和流出。
操作系统用结构体FILE标识一个流,结构体包含了标准 I/O 库函数为管理文件所需要的所有信息。
(FILE数据类型的定义也在stdio.h中,是操作系统帮我们创建的)
我们用指向这个结构体的指针FILE * 来找到这个结构体的位置,进行操作;
当采用fopen 打开一个流时会返回一个指向流对象的指针,指针类型为FILE *
3) 标准IO默认打开三个流:stdin(标准输入)、stdout(标准输出)、stderr(标准错误)
他们默认在终端上输入和输出
1.1.缓冲机制(三种)
1) 全缓存:和文件相关
刷新缓存的条件:
1.程序正常结束 return(main)、exit
2.强制刷新:fflush(NULL)
3.缓冲区满
2) 行缓存:和终端相关
刷新缓存的条件:
1.程序正常结束 return(main)、exit
2.强制刷新:fflush(NULL)
3.缓冲区满
4.\n
3) 不缓存:标准错误的话不缓存
缓冲区大小一般为1kb,也就是1024个字节。
综上:当我们每次要打印数据时,并不是将数据直接发送给标准输出设备,也就是并直接发送给显示器,而是将要打印的数据先存放到缓存区,当缓冲存数据满时,或者遇到\n,或者程序结束时,或者手动刷新缓存区时,缓冲区才会把数据传输到标准输出设备中,也就是显示器中进行输出。
1.2. 标准IO的函数接口
通过流对文件或终端中的数据进行读写操作
打开文件:fopen
关闭文件:fclose
读写操作:fgets、fputs、fread、fwrite
定位操作:rewind、fseek、ftell
1.2.1. 打开文件
#include <stdio.h> FILE *fopen(const char *pathname, const char *mode); 功能:打开文件 参数: pathname: 打开的文件路径 mode: 打开的方式 r:只读,流被定位到文件开头 r+:可读可写,流被定位到文件开头 w:只写,文件不存在创建,文件存在清空,流被定位到文件开头 w+:可读可写,文件不存在创建,文件存在清空,流被定位到文件开头 a:只写,文件不存在创建存在追加,流被定位到文件末尾,从文章末尾开始写 a+:可读可写,文件不存在创建,存在追加,开始进行读时从头读,进行写时流被定位到文件末尾 返回值:成功:返回一个 FILE 指针==文件流 失败:NULL,并且设置全局变量 errno (错误码)来标识错误。r :只读方式打开文件 , 文件必须存在 ; 文件不存在打开失败 ;流在开头
w :打开只写文件 , 文件不存在创建文件 , 文件存在清空文件 ;流在开头
a :打开只写文件 , 文件不存在创建文件 , 文件存在追加文件 ;流在结尾
+ :读写方式打开文件 ;a+的读操作流在开头
1.2.2. 关闭文件
int fclose(FILE* stream); 功能:关闭文件 参数:stream:文件流 返回值:如果流成功关闭,则该方法返回零。如果失败,则返回 EOF。1.2.3. 读写文件
读字符串: #include <stdio.h> char *fgets(char *s, int size, FILE *stream); 功能:从文件中读取一个字符串 参数:s:存放所读字符串的数组的首地址 size:读取的大小 stream:文件流 返回值:成功:读取的字符串所在数组的首地址 失败读到文件末尾:NULL 特性:1. 一次调用最多读取一行数据,遇到\n或者达到文件末尾后不在继续下一行 2. 实际读到个数为size-1个,末尾自动添加\0 写字符串: #include <stdio.h> int fputs(const char *s, FILE *stream); 功能:向文件或终端中写一个字符串 参数:s:要写入的内容 stream:文件流 返回值:成功:非负整数 失败:EOF读二进制: #include <stdio.h> size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 功能:从文件流读取多个元素(将二进制数据从文件读出) 参数:ptr:用来存放读取元素(可以用来存放任意类型的数据) size:元素大小 sizeof(数据类型) nmemb: 读取的对象个数 stream:要读取的文件 返回值:成功:读取元素的个数 失败或读取到文件尾:0 写二进制: size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream); 功能:将二进制数据写入文件 参数:ptr :是一个指针,保存要输出数据的空间的地址。 size :要写入的字节数 sizeof(数据类型) nmemb : 要进行写入元素的个数 stream: 目标文件流指针 返回值:成功:写元素的个数 失败:-11.2.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、2ftell
long ftell(FILE *stream) 功能:获取当前的文件位置 参数:要检测的文件流 返回值:成功:当前位置指针位置 失败:-1练习:实现"head -h 文件名"命令的功能
./a.out -5 xx.c
atoi: "123" --> 123
argv[1]: "-5"
argv[1]+1: "5" //+1目的是去掉-
思想:循环打印,数换行,来一行行数+1,打印这行,知道换行的数量达到n就结束。
#include <stdio.h> #include <string.h> #include <stdlib.h> int main(int argc, char const *argv[]) { if (argc != 3) { printf("err\n"); return -1; } FILE *fp = fopen(argv[2], "r"); if (fp == NULL) { perror("fp err"); return -1; } char buf[32]={}; int num = atoi(argv[1] + 1); for (int i = 0; i < num; i++) { if (fgets(buf, 32, fp) != NULL) { if (buf[buflen(buf) - 1] == '\n') fputs(buf, stdout); } } fclose(fp); return 0; }2. 文件IO
又称系统IO,是系统调用,是操作系统提供的函数接口
在posix(可移植操作系统接口)中定义的一组输入输出的函数
特点
1.没有缓冲机制,每次操作都会经过系统调用,相比较标准IO效率更低
2.围绕文件描述符进行操作,文件描述符是非负整数
3.默认打开三个文件描述符,分别为0(标准输入)、1(标准输出)、2(标准错误)
4.除目录外其他任意类型的文件都可以操作 :b、c、-、l、s、p
问题:打开三个文件,描述符:3、4、5;关闭3以后,重新打开一个文件,描述符是几?
答:还是3
问题:一个进程的文件描述符最大到几?最多能打开多少个文件描述符?最多能打开多少个文件?
答:一个进程的文件描述符最大到1023(0-1023),最多能打开1024个文件描述符, 最多能打开1024-3=1021个文件。
2.1. 文件IO的函数接口
2.1.1. 打开文件
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); 功能:打开文件 参数:pathname: 文件路径名 flags:打开方式 O_RDONLY:只读 O_WRONLY:只写 O_RDWR:可读可写 O_CREAT:不存在创建 O_TRUNC:清空 O_APPEND:追加 返回值:成功:文件描述符 失败:-1 补充: 1.多种打开方式一起使用时,采用位或 | 来分隔两种打开方式(因为打开方式是二进制数) 2.当打开方式使用 O_CREAT(不存在创建)的时候,需要第三个参数:创建文件的权限 文件权限=权限值 &(~umask) umask: 0002 //文件权限掩码 0666 & (~umask) 110 110 110 & 111 111 101 || 110 110 100 = 0664 最终变化为其他用户没有写权限2.1.2. 关闭文件
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 fd = open("./test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);//写|创建|清空,权限664 if (fd < 0) { perror("fd err"); return -1; } printf("%d", fd);//打印一下看看文件描述符 close(fd); //关闭文件 return 0; }2.1.3. 读写文件
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); 功能:从一个已经打开的可读文件中读取数据 参数:fd 文件描述符 buf 读到数据的存放位置 count 期望的个数//指定多少字符就读取多少字符 返回值: 成功:实际读到的个数(小于期望的值,说明实际没这么多)//所见即所得 读到文件结尾:返回0 失败:返回值-1,并设置errno号 #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); 功能:向指定的文件描述符中,写入count个字节的数据 参数:fd 文件描述符 buf 要写的内容 count 期望写入的字节数 返回值:成功:实际写入数据的个数 失败:-1三种读的对比
fgets: NULL末尾或失败
fread: 0末尾或失败
read : 0末尾 -1失败
2.1.4. 定位操作
#include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence); 功能:设置文件的偏移位置 参数:fd 文件描述符 offset 偏移量 正数:向文件结尾位置移动 负数:向文件开始位置移动 whence 相对位置 或用0,1和2代替 SEEK_SET 开始位置 SEEK_CUR 当前位置 SEEK_END 结尾位置 返回值:成功:文件的当前位置 失败:-1练习:文件IO实现cp功能。cp 源文件 新文件名
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main(int argc, char const *argv[]) { char buf[32] = {}; int n; int fd = open(argv[1], O_RDONLY); if (fd < 0) { perror("fd err"); return -1; } int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd2 < 0) { perror("fd2 err"); return -1; } if (argc != 3) { printf("输入格式 err\n"); return -1; } while ((n = read(fd, buf, 32)) > 0) { write(fd2, buf, n); } close(fd); close(fd2); return 0; }2.2. 标准IO和文件IO的区别
标准IO | 文件IO | |
概念 | 在 C 库中定义的一组输入输出的函数 | 在 posix 中定义的一组输入输出的函数 |
特点 | 1. 有缓冲区,减少系统调用,提高效率 2. 围绕流操作,FILE * 3. 默认打开三个流:stdin\stdout\stderr 4. 只操作普通文件- 5.可移植性相对较好 | 1. 无缓冲区,每次操作都引起系统调用 2. 围绕文件描述符操作 3. 默认打开三个文件描述符:0\1\2 4. 除目录外其他文件bc-lsp 5.可移植性相对较差 |
函数 | 打开文件:fopen\freopen 关闭文件:fclose 读写文件:fgetc/fputc (读取写入字符) fgets/fputs fread/fwrite 文件定位:fseek/rewind/ftell | 打开文件:open 关闭文件:close 读写文件:read/write 文件定位:lseek |
3. 库
3.1. 库的定义
库文件是计算机上的一类文件,提供给使用者一些开箱即用的变量、函数或类。
库是一种可执行代码的二进制形式。
后缀:Linux:.so(动态库).a(静态库) Windows:.dell
不同系统的库互不兼容
lib:库文件 (实现函数的定义)
include:头文件(包含函数的声明)
3.2. 库的分类
静态库:在程序编译时会被复制到目标代码中,以.a结尾
优点:程序运行时将不再需要该静态库;运行时无需加载库,运行速度更快。
缺点:静态库中的代码复制到了程序中,因此体积较大;静态库升级后,程序需要重新编译链接。
动态库:在程序运行时才会被载入到代码中。也叫共享库,以.so结尾
优点:程序在执行时加载动态库,代码体积小;程序升级更简单;不同应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。
缺点:运行时需要动态库的存在,移植性较差。
静态库、动态库,本质的区别是代码载入的时刻不同
3.3. 库的制作
静态库的制作:
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
动态库的制作:
1-用gcc来创建共享库
gcc -fPIC -c xxx.c -o xxx.o
-fPIC 创建与地址无关的编译程序 (不和路径进行关联,可以移动)
gcc -shared -o libxxx.so xxx.o
2-把库拷贝到/usr/lib和/lib目录下。(此方法编译时不需要指定库的路径)
3-测试动态库使用 gcc main.c -l指定库名
当加载动态库时,系统会默认从/lib或/usr/lib路径下查找库文件,不用-L指定路径了
(除了拷贝库到/lib目录下之外也可以:在LD_LIBRARY_PATH环境变量中加上库所在路径。
或者 添加/etc/ld.so.conf.d/*.conf文件。把库所在的路径加到文件末尾,并执行ldconfig刷新
sudo vi xx.conf 添加动态库存在的路径使用绝对路径)
-L路径:指定库的路径
-l库名:指定链接的库名
-I(大写i) 路径:指定头文件的路径 默认查找的路径/usr/include
<> 代表从系统路径下查找
"" 代表从当前路径下查找如果没有再去系统路径下查找
ldd 可执行文件名:查看链接的动态库
3.4.总结静态库和动态库
静态库:编译阶段,以.a结尾,执行速度快,体积大,移植性好,升级麻烦。
动态库:运行节点,以.so结尾,执行速度慢,体积小,移植性差,升级简单。
4. Linux IO模型
阻塞IO、非阻塞IO、信号驱动IO(了解)、IO多路复用
4.1. 阻塞IO:最常见、效率低、不浪费CPU
4.2. 非阻塞IO:轮询、耗费CPU、可以同时处理多路IO
设置文件描述符的属性设置非阻塞IO
int flage = fcntl(0, F_GETFL); // 1. 用flage接收fcntl函数的返回值:文件描述符的属性信息 flage |= O_NONBLOCK; // 2. 添加非阻塞权限 fcntl(0, F_SETFL, flage); // 3. 将修改好的权限重新设置flag &= ~O_NONBLOCK; fcntl(0, F_SETFL, flag);4.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操作,对性能要求不高 | 高并发网络服务器,减少线程阻塞时间 | 实时性要求高的应用,避免轮询开销 |
4.4. IO多路复用
4.4.1. select
(1)特点:
1. 一个进程最多只能监听1024个文件描述符
2. select被唤醒之后要重新轮询,效率相对低
3. select每次都会清空未发生响应的文件描述符,每次拷贝都需要从用户空间到内核空间,效率低,开销大
(2)编程步骤(六步):
1. 先构造一张关于文件描述符的表
2. 清空表 FD_ZERO
3. 将关心的文件描述符添加到表中 FD_SET
4. 调用select函数
5. 判断是哪一个或者式哪些文件描述符产生了事件 FD_ISSET
6. 做对应的逻辑处理
(3)超时检测
避免进程在没有数据时无限制的阻塞;
select函数
#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的多路复用
参数:
1. nfds:关注的最大的文件描述符+1
2. readfds:关注的读表
3. writefds:关注的写表
4. exceptfds:关注的异常表
5. timeout:超时的设置
返回值:
准备好的文件描述符的个数
-1 :失败:
0 :超时检测时间到并且没有文件描述符准备好
#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.4.2.poll
(1)特点
1. 优化文件描述符的限制,文件描述符的限制取决于系统
2. poll被唤醒之后要重新轮询一遍,效率相对低
3. poll不需要重新构造表,采用结构体数组,每次都需要从用户空间拷贝到内核空间
(2)实现过程
1. 创建一个表,也就是一个结构体数组 struct pollfd fds[100];
2. 将关心的描述符添加到表中并赋予事件
3. 循环调用poll更新表while(1){poll();}
4. 逻辑判断 if(fds[i].revents==POLLIN) {}
4.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不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。 |
结构 | 数组 | 数组 | 红黑树+就绪链表 |
开发复杂度 | 低 | 低 | 中 |
5. 进程
程序:编译好的可执行文件
存放在磁盘上的指令和数据的有序集合(文件)
程序是静态的,没有任何执行的概念
进程:一个独立的可调度的任务
执行一个程序所分配资源的总称
进程是程序的一次执行过程
进程是动态的,包括创建、调度、执行和消亡
特点
1. 系统会为每个进程分配0-4g的虚拟空间,其中0-3g是用户空间,每个进程独有;3g-4g是内核空间,所有进程共享
2. 轮转调度:时间片,系统为每个进程分配时间片(几毫秒~几十毫秒),当一个进程时间片用完时,CPU调度另一个进程,从而实现进程调度的切换 (没有外界干预是随机调度)
5.1. 进程段
Linux中的进程大致包含三个段:
数据段:存放的是全局变量、常数以及动态数据分配的数据空间(如malloc函数取得的空间)等。
正文段:存放的是程序中的代码
堆栈段:存放的是函数的返回地址、函数的参数以及程序中的局部变量 (类比内存的栈区)
5.2. 进程分类
交互进程:该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行。该类进程经常与用户进行交互,需要等待用户的输入,当接收到用户的输入后,该类进程会立刻响应,典型的交互式进程有:shell命令进程、文本编辑器等
批处理进程:该类进程不属于某个终端,它被提交到一个队列中以便顺序执行。(目前接触不到)
守护进程:该类进程在后台运行。它一般在Linux启动时开始执行,系统关闭时才结束。
5.3. 进程状态
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 多线程
+ 前台进程
5.4. 进程状态切换图
进程创建后,进程进入就绪态,当CPU调度到此进程时进入运行态,当时间片用完时,此进程会进入就绪态,如果此进程正在执行一些IO操作(阻塞操作)会进入阻塞态,完成IO操作(阻塞结束)后又可进入就绪态,等待CPU的调度,当进程运行结束即进入结束态
5.5. 进程相关命令
ps 查看系统中的进程 -aux -ef
top 动态显示系统的进程
nice 按用户指定的优先级运行进程
renice 改变正在运行进程的优先级
kill 发送信号给进程
jobs查看当前终端的后台进程
bg 将进程切换到后台执行
fg 将进程切换到前台执行
5.6.进程函数
5.6.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) 父子进程有独立的地址空间,互不影响;当在相应的进程中改变全局变量、静态变量,都互不影响。
3) fork之前的代码会被复制但是不会被重新执行一遍,fork之后的代码会被复制,并且父子进程分别执行一遍。
4) fork之前打开的文件,fork之后会拿到同一个文件描述符,操作同一个文件指针。
5) 若父进程先结束,子进程成为孤儿进程,被init进程(进程号1)收养,子进程变成后台进程。
6) 若子进程先结束,父进程如果没有及时回收子进程,子进程变成僵尸进程(要避免僵尸进程产生)。
5.6.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; }5.6.3.结束进程
#include <stdlib.h> void exit(int status); 功能:结束进程,刷新缓存 #include <unistd.h> void _exit(int status); 功能:结束进程,不刷新缓存 参数:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。 通常0表示正常结束;其他数值表示错误结束;补充:
exit和return区别:
exit:不管在子函数还是主函数,都可以结束进程
return:当子函数中有return时返回到函数调用位置,并不结束进程
5.6.4.获取进程号
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); 功能:获取当前进程的进程号 pid_t getppid(void); 功能:获取当前进程的父进程号6. 进程间通信IPC
进程间通信方式
1) 早期的进程间通信:
无名管道(pipe)、有名管道(fifo)、信号(signal)
2) system V IPC对象
共享内存(share memory)、信号灯集(semaphore)、消息队列(message queue)
3) BSD
socket套接字
6.1. 无名管道
6.1.1. 特点
1) 只能用于具有亲缘关系的进程之间通信
2) 具有固定的读端和写端,半双工通信模式
单工:只能单向通信(广播)
半双工:可以双向通信,但是同一时间不可以同时发送(对讲机)
全双工:可以双向同时通信(电话)
3) 管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数
4) 管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1]。
其中fd[0]固定用于读管道,而fd[1]固定用于写管道。
6.1.2. 函数接口
int pipe(int fd[2]) 功能:创建无名管道 参数:文件描述符fd[0]:读端 fd[1]:写端 返回值:成功:0 失败:-16.2. 有名管道
6.2.1. 特点
1) 有名管道可以使互不相关的两个进程互相通信
2) 有名管道可以通过路径名来指出,并在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。
3) 进程通过文件IO来操作有名管道
4) 有名管道遵循先进先出规则
5) 不支持如lseek() 操作
6.2.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文件。
6.3. 信号
信号是进程通信方式中的唯一的一种异步的方式
同步:按照一定顺序去执行
异步:没有顺序的,它不要求先后顺序,它是来什么信号处理什么信号
6.3.1. 概念
● 信号是在软件层次上对中断机制的一种模拟,是一种 异步通信方式
● 信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
● 如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。、
6.3.2. 信号的响应方式
忽略信号:对信号不做任何的处理,但是有两个信号不能忽略:即:SIGKILL、SIGSTOP
捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数
执行默认(缺省)操作:Linux对每种信号都规定了默认操作
6.3.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 产生,用于请求停止进程。
6.3.4. 函数接口
6.3.4.1.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; }6.3.4.1.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; }6.3.4.1.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 <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> pid_t pid; void handler(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(SIGTSTP, SIG_IGN); signal(SIGINT, handler); signal(SIGQUIT, handler); signal(SIGUSR1, handler); } // 当返回值大于零的时候相当于在父进程中运行 else { signal(SIGINT, SIG_IGN); signal(SIGQUIT, SIG_IGN); signal(SIGUSR1, driver); signal(SIGUSR2, driver); signal(SIGTSTP, driver); } while (1) pause(); return 0; }6.4. 共享内存
共享内存指的是操作系统在物理内存中申请一块空间,应用程序可以映射到这块空间,进行直接读写操作
特点:
1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝
2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间
3)进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
4)由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等
6.4.1. 步骤
1. 创建唯一key值 ftok
2. 创建或打开共享内存 shmget
3. 映射共享内存到用户空间(拿到映射的地址后就可以操作共享内存) shmat
4. 撤销映射 shmdt
5. 删除共享内存 shmctl
6.4.2. 函数接口
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);//创建唯一key值 #include <sys/shm.h> int shmget(key_t key, size_t size, IPC_CREAT | IPC_EXCL | 0666);//创建或打开共享内存 //两种失败情况,errno==EEXIST时,shmid=shmget(key,1024,0666) #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);//映射共享内存到用户空间 #include <sys/shm.h> int shmdt(const void *shmaddr);//撤销映射 #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);// 删除共享内存#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id); 功能:创建key值 参数: pathname: 文件名 proj_id:取整数的低8位数据 返回值:成功:key值 失败:-1补充:
key值是根据pathname的inode号和proj_id的低8位组合而成的。如:0x61013096
pathname只要是路径中存在的文件即可
ls -i 查看文件inode号
6.4.2.1.创建共享内存
#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查看创建的共享内存的命令:ipcs -m
6.4.2.2.映射共享内存
#include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg); 功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问 参数:shmid:共享内存的id号 shmadd:一般为NULL,表示由系统自动完成映射 如果不为NULL,那么由用户指定 shmflg:SHM_RDONLYH就是对该共享内存进行只读操作 0 可读可写 返回值:成功:完成映射后的地址 出错:(void *)-1的地址6.4.2.3.取消映射
#include <sys/shm.h> int shmdt(const void *shmaddr); 功能:取消映射 参数:shmaddr:要取消映射的共享内存地址 返回值:成功:0 失败:-16.4.2.4.删除共享内存
#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 失败 -16.4.3.操作命令:
ipcs -m: 查看系统中的共享内存
ipcrm -m shmid:删除共享内存
ps: 可能不能直接删除掉还存在进程使用的共享内存。
这时候可以用ps -ef对进程进行查看,kill掉多余的进程后,再使用ipcs查看。
实例:
#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; }#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.5.信号灯集
6.5.1.特点
信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制;
System V信号灯集是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。
通过信号灯集实现共享内存的同步操作
6.5.2.步骤
1. 创建key值:ftok
2. 创建或打开信号灯集: semget
3. 初始化信号灯: semctl
4. PV操作:semop
5. 删除信号灯集: semctl
6.5.3.操作命令
ipcs -s:查看信号灯集
ipcrm -s semid:删除信号灯集
6.5.4.函数接口
6.5.4.1.创建信号灯集
#include <sys/sem.h> int semget(key_t key, int nsems, int semflg); 功能:创建/打开信号灯 参数:key:ftok产生的key值 nsems:信号灯集中包含的信号灯数目 semflg:信号灯集的访问权限,通常为IPC_CREAT | 0666 返回值:成功:信号灯集ID 失败:-1semid等于0时我们认为是错误的,可以手动去删除这个信号灯集:
ipcs -s :查看创建的信号灯集
ipcrm -s [semid]:删除信号灯集
6.5.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));
6.5.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; }6.6.消息队列
6.6.1.特点
消息队列是IPC对象的一种(活动在内核级别的一种进程间通信的工具)
1. 消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。
2. 消息队列可以按照类型来发送/接收消息
3. 在linux下消息队列的大小有限制。
● 消息队列个数最多为16个;
● 消息队列总容量最多为16384字节;
● 每个消息内容最多为8192字节。
消息是通过链表的方式依次进行添加,可以通过类型来区分添加的是那种类型的数据,同种类型的数据在读取的时候是按照队列的方式读取的,不同类型的数据是按照类型进行读取
6.6.2.步骤
1. 创建key值
2. 创建或打开消息队列 msgget (message deque)
3. 添加消息 msgsnd (send)
4. 读取消息 msgrcv (recive)
5. 删除消息队列 msgctl
6.6.3.操作命令
ipcs -q: 查看消息队列
ipcrm -q msgid: 删除消息队列
注意:有时候可能创建失败或者msgid为0,所以用命令看看,删了重新创建就可以了。
6.6.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; }7.线程 Thread
7.1.概念
线程是一个轻量级的进程,为了提高系统的性能引入线程。
线程和进程都参与统一的调度。
在同一个进程中可以创建多个线程,并且共享进程资源。
7.2.进程和线程区别(面试题)
相同点:都为操作系统提供了并发执行的能力
不同点:
资源和调度:进程是系统资源分配的最小单位,线程是资源调度的最小单位
地址空间方面:每个进程都有独立的地址空间;同一个进程中的多个线程共享进程地址空间
通信方面:线程通信相对简单,只需要通过全局变量就可以实现,但是需要考虑临界资源访问的问题; 进程通信比较复杂,需要借助进程间的通信机制(3-4g的内核空间)。
安全性方面:线程安全性差一些,当进程结束时会导致所有线程退出; 进程相对安全。
面试:程序什么时候该使用线程?什么时候用进程?(深圳棱镜空间智能科技有限公司、北京明朝万达)
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高、速度快的高并发环境时,需要频繁创建、销毁或切换时,资源的保护管理要求不是很高时,使用多线程。
7.3.线程资源
共享的资源:可执行的指令、静态数据、进程中打开的文件描述符、信号处理函数、当前工作目录、用户ID、用户组ID
私有的资源:线程ID (TID)、PC(程序计数器)和相关寄存器、堆栈(局部变量, 返回地址)、错误号 (errno)、信号掩码和优先级、执行状态和属性
7.4.函数接口
编译的时候需要加 -lpthread 链接动态库
7.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 失败:错误码7.4.2.退出线程:pthread_exit
#include <pthread.h> void pthread_exit(void *retval); 功能:用于退出线程的执行 参数:retval:线程退出时返回的值线程结束之后不是多线程状态了,此时只剩下一个主线程
7.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终止后会被操作系统自动回收资源练习:输入输出,quit结束
通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,当输入quit结束程序。
1) 全局变量
2) 加上标志位(flag),实现主线程输入一次修改标志位,从线程打印一次也修改标志位, int flag=0;
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <string.h> int flag = 0; // 全局标志 char buf[32] = {}; void *handler(void *arg) { while (1) { if (flag == 1) // 有新数据要打印 { printf("%s\n", buf); flag = 0; // 打印后重置标志 } else if (flag == 2) { pthread_exit(NULL); // 线程退出 } } return NULL; } int main() { pthread_t tid; if (pthread_create(&tid, NULL, handler, NULL) != 0) { perror("线程创建失败"); return 1; } while (1) { scanf("%s", buf); if (strcmp(buf, "quit") == 0) { flag = 2; // 退出 break; } flag = 1; // 标记有新数据 } pthread_join(tid, NULL); return 0; }7.5. 线程同步
7.5.1.概念:
多个线程(任务)按照约定的顺序相互配合完成一件事情
7.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,函数不阻塞练习:通过信号量实现线程同步:主线程循环从终端输入字符串,子线程循环将字符串打印至终端,当输入"quit"时结束
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <string.h> #include <semaphore.h> sem_t sem; sem_t sem1; char buf[32] = {}; void *handler(void *arg) { while (1) { sem_wait(&sem); if (!strcmp(buf, "quit")) break; printf("%s\n", buf); sem_post(&sem1); } return NULL; } int main() { pthread_t tid; if (pthread_create(&tid, NULL, handler, NULL) != 0) { perror("线程创建失败"); return 1; } if (sem_init(&sem, 0, 0) < 0) { perror("sem init err"); return -1; } if (sem_init(&sem1, 0, 1) < 0) { perror("sem1 init err"); return -1; } while (1) { sem_wait(&sem1); scanf("%s", buf); sem_post(&sem); if (strcmp(buf, "quit") == 0) { break; } } pthread_join(tid, NULL); return 0; }7.6.线程互斥
7.6.1.互斥概念
多个线程访问临界资源时,同一时间只能一个线程访问
临界资源:多个线程共同访问的数据,且一次仅允许一个线程所使用的资源
通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。
互斥锁的操作方式:初始化
申请锁(上锁) :阻塞
当申请不到锁时(表示:锁被其它线程占用),是阻塞的
释放锁(解锁) :非阻塞
注意:上锁和解锁需要成对存在
7.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:互斥锁案例:两个线程,一个线程倒置全局数组中的数,另一个线程遍历数组中数据,每隔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; }7.6.3. 补充:死锁
是指两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁产生的四个必要条件
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
7.6.4. 条件变量
条件变量(cond)用于在线程之间传递信号,以便某些线程可以等待某些条件发生。当某些条件发生时,条件变量会发出信号,使等待该条件的线程可以恢复执行。
函数接口
一般和互斥锁搭配使用,来实现同步机制:
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再产生条件。