该文为学习笔记,仅作学习参考,如有错误,望指正!
一、标准IO
1. IO
- IO:input、output(读写操作——文件);
- IO的方式:标准IO、文件IO;
- 标准IO:间接采用系统调用(采用库函数)的方式对文件实现读写操作;
- 文件IO: 直接采用系统调用的方式对文件(设备)实现读写操作;
- 系统调用层:内核维护的接口层,用户程序可以通过该接口层实现各种功能;
- 封装函数(func)-----》系统调用接口(中断)------》syscall(编号)-----》sys_func(实现功能);
- CPU的状态切换 用户态–》内核态;
- 每一个系统调用的函数接口都会对应一个编号,即syscall;
- 参数传递:将参数传递给cpu内部的寄存器,运行sys_func之前将参数从寄存器中传到内核进程的堆栈区;
2. 标准IO
- 标准IO: 当对文件进行操作时,首先操作缓冲区,等到缓冲区满足一定的条件(缓冲区满或刷新缓冲区)时,然后再去执行系统调用,实现对文件的操作;
- 标准I/O库的所有操作都是围绕流(stream)来进行的,在标准I/O中,流用
FILE *
来描述。 - FILE指针:每个被使用的文件都存在;
- 内存中开辟一个区域,用来存放文件的有关信息,这些信息是保存在一个结构体类型的变量中,该结构体类型是由系统定义的,取名为 FILE;
char* _IO_buf_base;
/* Start of reserve area. */ 缓冲区的开始;char* _IO_buf_end;
/* End of reserve area. */ 缓冲区的末尾;- 缓冲区的大小:
sizeof(_IO_buf_end)-sizeoof(_IO_buf_base)
#include <stdio.h>
int main(int argc, const char *argv[])
{
FILE *fp;
fp = fopen("test.txt", "w");
fputc('a', fp);
printf("size = %d\n", fp->_IO_buf_end - fp->_IO_buf_base);
getchar();
printf("size = %d\n", stdin->_IO_buf_end - stdin->_IO_buf_base);
printf("size = %d\n", stdout->_IO_buf_end - stdout->_IO_buf_base);
printf("size = %d\n", stderr->_IO_buf_end - stderr->_IO_buf_base);
return 0;
}
[root@192 learn]# ./a.out
size = 4096
w
size = 1024
size = 1024
size = 0
2.1. fputc函数原型
函数原型:int fputc(int c, FILE *stream);
- 功能:向文件写入一个字符;
- 参数:c 写入的字符;stream 流指针;
- 返回值:成功返回已经写入的字符(强转);失败 EOF;
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
FILE *fp;
int i;
if((fp = fopen("test.txt", "w")) == NULL){
printf("fopen error:%s\n", strerror(errno));
return -1;
}
for(i = 0; i < 5; i++){
if(fputc(99, fp) == EOF){ //fputc为存入字符,99以ASCII码形式存入
perror("fputc error");
return -1;
}
}
return 0;
}
2.2. fgetc函数原型
函数原型:int fgetc(FILE *stream);
- 功能:从指定的文件中读取一个字符;
- 参数:stream 文件流指针;
- 返回值:成功 读取的字符;失败 EOF;
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(){
FILE *fp;
char ch;
if ((fp = fopen("test.txt", "r")) == NULL)
{
printf("fopen error:%s\n", strerror(errno));
return -1;
}
if ((ch = (fgetc(fp))) == EOF) // fgetc 直接读取文件流指针
{
perror("fgetc error");
return -1;
}
printf("ch = %c\n", ch);
return 0;
}
2.3. fgets函数原型
实现对文件操作:采用字符形式、采用字符串的形式、采用二进制的形式。
函数原型:FILE *fgets (char *s, int size, FILE *stream );
-
功能:从指定的文件中读取一个字符串,并保存到字符数组中;
-
参数:s 字符数组;size 需要读取的字符数目;stream 文件流指针;
-
返回值:读取成功 返回字符数组首地址,也即 s;读取失败 返回 NULL;
如果开始读取时文件内部指针已经指向了文件末尾,那么将读取不到任何字符,也返回 NULL;
2.4. fputs函数原型
函数原型:FILE *fgets (char *s, FILE *stream );
- 功能:字符串写入到指定的流 stream 中,但不包括空字符。;
- 参数:s 字符数组,包含了要写入的以空字符终止的字符序列;stream 文件流指针;
- 返回值:成功 函数返回一个非负值,错误 则返回 EOF。
2.5. fopen函数原型
函数原型:FILE *fopen(const char path, const char mode);
-
功能:打开文件;
-
参数:
path 文件名;
mode 进程打开文件的方式;
- r:以只读的方式打开文件,如果文件不存在,错误;
- r+:以读写的方式打开文件,如果文件不存在,错误;
- w:以只写的方式打开文件,如果文件不存在,自动创建;如果文件已存在,清空文件中的数据;
- w+:以读写的方式打开文件,如果文件不存在,自动创建;如果文件已存在,清空文件中的数据;
- a:以只写的方式打开文件,如果文件不存在,自动创建;如果文件已存在,写入文件的末尾;
- a+:以读写的方式打开文件,如果文件不存在,自动创建;如果文件已存在,追加到文件的末尾;
-
返回值:成功 流指针; 失败 NULL;
实例:若文件打开错误输出报错信息
#include <stdio.h>
#include <errno.h> //printf函数输出调用strerror需要加头文件
#include <string.h> //printf函数输出调用stderr需要加头文件
int main(int argc, char *argv[])
{
FILE *fp;
if((fp = fopen("test.txt", "r")) == NULL)
{
// perror("fopen error");
printf("fopen error:%s\n", strerror(errno));
fprintf(stderr, "fopen error\n");
return -1;
}
return 0;
}
实例:file内容每4个一行输出
#include <stdio.h>
#define Size 5
int main(){
FILE *fp;
char buf[Size] = "";
if ((fp = fopen("test.txt", "r")) == NULL)
{
perror("fopen error!");
return -1;
}
while ((fgets(buf, Size, fp)) != NULL)
{
printf("buf = %s\n", buf);
}
return 0;
}
实例:hello world内容赋值给text.txt
#include <stdio.h>
int main(){
FILE *fp;
char buf[] = "Hello World!";
if ((fp = fopen("test.txt", "w")) == NULL)
{
perror("fopen error!");
return -1;
}
if ((fputs(buf, fp)) == EOF) //向文件里写入字符串时不用规定大小,每次写入遇到'\n'或size-1个字符结束
{
perror("fputs error!");
return -1;
}
return 0;
}
实例:输出文件行数
#include <stdio.h>
#include <string.h>
int GetLine(FILE *fp){
int count = 1;
char ch = fgetc(fp);
if (ch == EOF)
count = 0;
while ((ch = (fgetc(fp))) != EOF)
{
if (ch == '\n')
count++;
}
return count;
}
int main(){
FILE *fp;
if ((fp = fopen("test.txt", "r")) == NULL)
{
perror("fopen error!");
return -1;
}
int line = GetLine(fp);
printf("line = %d\n", line);
return 0;
}
实例:命令行传参,求出任意一个文件行数
#include <stdio.h>
#include <string.h>
int GetLine(FILE *fp){
int count = 1;
char ch = fgetc(fp);
if (ch == EOF)
count = 0;
while ((ch = (fgetc(fp))) != EOF)
{
if (ch == '\n')
count++;
}
return count;
}
int main(int argc, char *argv[]){
FILE *fp;
if ((fp = fopen(argv[1], "r")) == NULL)
{
perror("fopen error!");
return -1;
}
int line = GetLine(fp);
printf("line = %d\n", line);
return 0;
}
2.6. fseek函数原型
-
函数原型:
int fseek(FILE *stream, long offset, int whence);
-
功能:定位当前的读写位置;
-
参数:
stream 文件流指针;
offset为偏移量,在第三个参数定位的基础上产生位置的偏移;
whence SEEK_SET 将文件的读写位置定位到文件的开始处,SEEK_CUR将文件的读写位置定位到文件当前位置,SEEK_END将文件的读写位置定位到文件的末尾处;
-
返回值:成功 返回 0,失败 返回 -1;
2.7. ftell函数原型
函数原型:long ftell(FILE *stream);
- 功能:获取当前读写位置的值;
- 参数:stream 文件流指针;
- 返回值:成功 返回位置的值(偏移量-相对于文件开始处的偏移量),失败 返回 -1;
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
FILE *fp;
int ch;
if((fp = fopen("test.txt", "w+")) == NULL){
printf("fopen error:%s\n", strerror(errno));
return -1;
}
fputc('a', fp); //从0位置输入‘a’后,当前位置为1
fputc('b', fp); //当前位置为2
fputc('c', fp); //当前位置为3
fputc('d', fp); //当前位置为4
fseek(fp, -4, SEEK_CUR); //向左偏移4个单位后为0
long offset;
offset = ftell(fp);
printf("offset1 = %ld\n", offset);
fputc('e', fp); //e覆盖a的位置,光标向后移动一位,当前位置为1
fseek(fp, 2, SEEK_CUR); //向右移动2个单位,当前位置为3
offset = ftell(fp);
printf("offset2 = %ld\n", offset);
return 0;
}
[root@192 learn]# ./a.out
offset1 = 0
offset2 = 3
2.8. fwrite函数原型
函数原型: size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
- 功能:向文件中写入nmemb个数据单元;
- 参数:ptr 数据的来源,size 每个单元数据的大小,nmemb 写入文件的单元数据的个数,stream 流指针;
- 返回值:成功 返回写入的数据单元的个数;失败 返回3负数 ;
2.9. fread函数原型
函数原型:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
- 功能:从文件中读取nmemb个数据单元;
- 参数:ptr 保存读取的数据,size 数据单元的大小,nmemb 读取数据单元的个数,stream 流指针;
- 返回值:成功为读取单元的个数;失败为负数;
3. 预定义流指针
标准I/O预定义3个流,他们可以自动地为进程所使用(实现终端的操作)
- stdin 标准输入;
- stdout 标准输出;
- stderr 标准错误输出;
#include <stdio.h>
int main(int argc, char *argv[])
{
int ch;
while((ch = getchar()) != EOF){ //缓存区中存数据的时候,遇见‘\n’或缓冲区存满才刷新缓存区
putchar(ch); // 缓存区中取数据的时候也是一样
}
return 0;
}
[root@192 learn]# ./a.out
1
1
EOF
EOF
^C
#include <stdio.h>
int main(int argc, char *argv[])
{
int ch;
while((ch = fgetc(stdin)) != EOF){
fputc(ch, stdout);
}
return 0;
}
[root@192 learn]# ./a.out
1
1
2
2
^C
4. 缓冲区
-
标准IO缓冲区分为三种:全缓冲区、行缓冲区、不缓冲;
-
全缓冲区:当流指针和文件相关联的时候,此时访问的缓冲区为全缓冲区(当使用标准IO函数接口操作文件时,此时访问为全缓冲区),缓冲区的大小:
4096 byte
;缓冲区的刷新:程序正常退出、缓冲区满、
fflush()
; -
行缓冲区:当流指针和终端相关联的时候,此时访问的缓冲区为行缓冲区(当使用标准IO函数接口操作终端时,此时访问的行缓冲区),
stdin、stdout getchar putchar printf
,缓冲区的大小:1024 byte
,。缓冲区的刷新:程序正常退出,缓冲区满:
fflush()
;\n
; -
不缓冲: stderr;
二、文件IO
1. 文件IO
- 文件IO:直接系统调用;
- 标准IO的操作的核心:流指针;
- 文件IO的操作的核心:文件描述符,文件描述符就是内核当中 fd_array 数组的下标,是一个正整数。
0、1、2系统自动打开文件描述符(实现操作终端)
- 0 标准输入;
- 1 标准输出;
- 2 标准错误输出;
2.1. open函数原型
函数原型:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
-
功能:打开一个文件或者设备(硬件);
-
参数:pathname 路径名或文件名;flags 进程对该文件的执行权限;mode:该文件所属用户对文件的执行权限(如果第二个参数不指定O_CREAT,第三个参数忽略),默认:
0664, 0755,mode & ~umask(位掩码0002)
;参数 说明 O_RDONLY
以只读的方式打开文件 O_WRONLY
以只写的方式打开文件 O_RDWR
以读写的方式打开文件 O_APPEND
以追加的方式打开文件 O_CREAT
如果文件不存在,创建文件 O_TRUNC
如果文件已存在,清空文件中的数据 对比:标准IO与文件IO对比。
标准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_WRONLY|O_CREAT|O_APPEND
读写,文件不存在会自动创建,文件存在则追加内容 -
返回值:成功 大于等于0的整数(即文件描述符);失败 -1;
2.2. write函数原型
函数原型:ssize_t write(int fd, const void *buf, size_t count);
-
功能:向文件中写入count个字节的数据(根据数据的大小进行写入,不论数据的类型);终端输入字符串,追加写入文件中;
-
参数:
fd:文件描述符;
buf:需要写入文件的数据;
count:需要写入的字节数;
-
返回值:成功 实际写入的字节数;失败 -1;
2.3. read函数原型
函数原型: ssize_t read(int fd, void *buf, size_t count);
-
功能:从文件中读取数据;
-
参数:
fd:文件描述符;
buf:保存读取的数据;
count:期望读取的字节数;
-
返回值:成功 实际读取的字节数;失败 -1;0 (文件为空, 当前的读写位置处于文件的末尾);
2.4. lseek函数原型
函数原型: off_t lseek(int fd, off_t offset, int whence);
-
功能:读写位置定位;
-
参数:
fd:文件描述符;
offset:偏移量;
whence:SEEK_SET、SEEK_CU、SEEK_END;
-
返回值:成功 返回定位的读写位置相对于文件开始处的偏移量;失败 返回 -1;
2.5. opendir函数原型
函数原型:DIR *opendir(const char *name);
-
功能:打开一个目录;
-
参数:
name:目录名;
-
返回值:成功 目录流指针;失败 NULL;
2.6. readdir函数原型
函数原型:struct dirent *readdir(DIR *dirp);
- 功能:遍历指定目录路径下的所有文件;
- 参数:目录流指针;
- 返回值:失败 NULL;成功返回结构体(需定义结构体变量接收) ;
代码:
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported by all file system types */
char d_name[256]; /* filename */
};
2.7. stat函数原型
函数原型:int stat(const char *path, struct stat *buf);
-
功能:获取文件的属性信息;
-
参数:
path:路径名或文件名;
buf:保存文件的属性信息;
-
返回值:成功 0;失败 -1;
代码
struct stat {
dev_t st_dev; /* ID of device containing file */ 设备号
ino_t st_ino; /* inode number */ 索引号
mode_t st_mode; /* protection */ 用户的执行权限
nlink_t st_nlink; /* number of hard links */ 硬链接数
uid_t st_uid; /* user ID of owner */ 用户的ID
gid_t st_gid; /* group ID of owner */ 组ID
dev_t st_rdev; /* device ID (if special file) */ 设备号
off_t st_size; /* total size, in bytes */ 文件的大小
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */ 上一次打开的时间
time_t st_mtime; /* time of last modification */ 上一次修改的时间
time_t st_ctime; /* time of last status change */ 上一次状态改变的时间
};
2. 库文件
- 静态库:在程序编译时,链接库文件,程序执行时,不需要库文件;
.a
后缀; - 动态库(共享库): 在程序编译时,不需要链接库文件,在程序执行时,链接库文件;
.so
后缀;
库函数名及其源码如下:hello.c hello.h main.c
hello.c
:
#include "stdio.h"
int hello(void){
printf("Hello Lib!");
return 0;
}
hello.h
:
#ifndef __HELLO_H
#define __HELLO_H
int hello(void);
#endif
main.c
:
#include "hello.h"
int main(){
hello();
}
2.1. 静态库的打包及使用
-
将
hello.c
编译为目标文件(即二进制文件);[root@192 source]# gcc -c hello.c -o hello.o
-
将
.o
文件打包成静态库,生成libhello.a
文件;[root@192 source]# ar crs libhello.a hello.o
-
使用静态库,因为静态库是在编译的时候一起打包进程序的,所以如果编译的时候没有静态库文件,则无法编译,因为编译器找不到
hello()
的实现代码,如下所示:[root@192 source]# gcc main.c /usr/bin/ld: /tmp/cccOFTDo.o: in function `main': main.c:(.text+0x5): undefined reference to `hello' collect2: error: ld returned 1 exit status
所以,在编译的过程中要加入库引用:
[root@192 source]# gcc -c main.c -L. -lhello -o a.out -l 指定库的名字,第三方库 -L 指定库路径
-L<路径>
:引用自定义库的路径,如果调用系统库就不用-L.
表示当前文件夹。-lxxxx
:指定库的名字,第三方库,这里libhello.a
只要写hello
就可以。
2.2. 动态库的打包及使用
-
将
hello.c
编译为目标文件(即二进制文件),如果这里没有加-fpic
下一步就会提示你重新用-fpic
编译。[root@192 source]# gcc -c -fpic hello.c
-
将函数功能实现的代码,制作为动态库:
-shared
是生成动态库;-fpic
生成位置无关代码,默认加。[root@192 source]# gcc -shared -fpic -o libhello.so hello.c
-
使用动态库:
[root@192 source]# gcc -c main.c -L. -lhello -o a.out
-
动态库生效:
-
将动态库复制当前系统存放动态库的位置:
[root@192 source]# cp libxxxx.so /usr/lib
-
配置环境变量(直接对全局变量进行赋值,指定库的路径):
[root@192 source]# export LD_LIBRARY_PATH=<动态库所在的绝对路径>
-
修改配置脚本:
在
/etc/ld.so.conf
添加动态库所在路径;刷新配置文件:
[root@192 source]# sudo ldconfig
-
三、进程
1. 进程简介
进程简介:
-
进程:进程是一个程序的一次执行的过程;
-
进程和程序的区别;
程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;
进程是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡;
-
创建:当程序执行(或者说进程创建时),系统会为每一个进程分配一个大小为4G的虚拟地址空间,用来存储进程所需的程序代码;使用变量,开辟内存的空间,生成
task_struct
(进程控制块)用来描述当前进程的状态信息; -
调度:cpu调度,每一个进程都有属于自己的时间片,时间片用来设置当前进程从获取到CPU的执行权,到放弃执行权的时间
-
进程的上下文切换(上文:保存进程状态;下文:恢复进程上一次中断的状态);
-
退出:将进程使用的有限资源全部释放;
虚拟地址与物理地址:
- 虚拟地址空间(虚拟内存):虚设,本质实际不存在;
- 物理地址空间(物理内存):真实的,可以被CPU取址的内存区域;
- 虚拟地址空间,实际上是不存在,只是当前系统为每一个进程虚设的内存区域,内存区域的地址称之为虚拟地址;
- 如果进程需要属于自己虚拟地址空间,只需要对实际物理内存建立映射关系即可;
- 映射的本质:将物理地址转换为虚拟地址;
- 系统将虚拟地址空间分割为大小为4K内存单元(页单元),实际物理地址空间也被分割为大小为4K的内存单元;
- 虚拟地址和物理地址映射之后对应关系,由页映射表记录(页映射表由页表条目组成,每一个条目记录对应关系 0x200(物)-0x100(虚拟);
2. 进程的内存布局
-
程序文本段(程序段:Text):程序代码在内存中的映射,存放函数体的二进制代码。
-
数据段(已初始化数据段,未初始化数据段):存储的是当前进程使用的全局变量,静态变量;
- 初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。
- 未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。
-
堆区(Heap):存储动态内存分配,需要程序员手工分配,手工释放。注意它与数据结构中的堆是两回事,分配方式类似于链表。
-
栈区(Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。
3. 进程分类
- 进程:前台进程, 后台进程;
- 前台进程:可以与用户进行交互;
- 后台进程:不与用户进行交互,不受终端的控制;
- 守护进程(系统进程,属于后台进程的一种,系统开启时,创建该进程,系统退出,进程退出);
#include <stdio.h>
int main(int argc, const char *argv[])
{
while(1){
sleep(1);
puts("zzzzzzzzzz");
}
return 0;
}
4. 进程状态切换
-
查看进程状态信息指令:
ps axj aux
PPID PID PGID SID TTY STAT UID TIME COMMAND
父进程 进程ID 进程组ID 会话组ID 终端 进程名字
-
大部分的子进程都是父进程创建,少部分不是;创建子进程为了响应系统中的各种任务;
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
pid_t pid;
int a = 3;
pid = fork();
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
//child
int a = 5;
printf("In the child process, %d %p\n", a, &a);
}
else{
//parent
printf("In the parent process %d %p\n", a, &a);
}
return 0;
}
- D uninterruptible sleep (usually IO) 不可中断睡眠态
- R running or runnable (on run queue) 运行态
- S interruptible sleep (waiting for an event to complete) 可中断睡眠态
- T stopped, either by a job control signal or because it is being traced. 停止态(不运行,不释放资源)
- X dead (should never be seen) 死亡态(进程退出)
- Z defunct (“zombie”) process, terminated but not reaped by its parent.(僵尸态)
- For BSD formats and when the stat keyword is used, additional characters may be displayed:
- < high-priority (not nice to other users) 高优先级
- N low-priority (nice to other users) 低优先级
- L has pages locked into memory (for real-time and custom IO)
- s is a session leader 会话组组长
- l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do) 进程创建线程
- + is in the foreground process group. 前台执行
相关命令如下:
-
ctrl z
:将前台正在执行的命令放到后台,并且暂停; -
ctrl c
、ctrl \
:终止一个进程 ; -
bg
:将进程放到后台运行 《====》./a.out &
; -
fg
:将进程放到前台执行; -
top
:动态查看(自动刷新); -
jobs
:查看当前有多少在后台运行的命令; -
shift >
:向下翻页;shift <
:向上翻页; -
PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
-
NI: -20 ~ 0;PR = NI + 20(NI值越小,优先级越高)
-
优先级 nice值
-
renice:修改已经运行的进程的NICE:
sudo renice -n -10(修改值) PID
-
nice:对即将执行的进程修改NICE值:
sudo nice -n -10 ./a.out
5. 子进程创建
5.1 getpid函数原型
函数原型:pid_t getpid(void);
- 功能:获取当前进程的 ID 号;
- 参数:无;
- 返回值:目前进程的进程 ID 号;
5.2 getppid函数原型
函数原型:pid_t getppid(void);
- 功能:获取当前进程父进程的ID号;
- 参数:无;
- 返回值:目前进程的父进程的ID号;
5.3 fork函数原型
函数原型:pid_t fork(void);
,头文件:<unistd.h>
;
- 功能:创建新进程,通过复制调用进程生成,将新进程称为子进程,将调用进程称为父进程;
- 参数:无;
- 返回值:成功 父进程的代码中返回值为子进程的 ID,子进程的代码中返回值为 0。失败 父进程返回值 -1,子进程创建失败;
子进程通过复制父进程得来,子进程复制父进程的所使用的虚拟地址空间,子进程拥有和父进程相同的程序文本段,数据段,堆栈区;
pid = fork();
父进程运行 fork 开始创建子进程,在变量 pid 接受 fork 的返回值之前,子进程创建结束;- 父进程的变量 pid 获取到子进程的 ID 号,表示子进程创建成功,子进程的变量 pid 获取到返回值是0;
- 父进程的变量 Pid 获取的值为 -1,表示子进程创建失败;
- 子进程不执行fork函数,并且不执行 fork 以上所有的执行代码;
- 子进程复制父进程使用缓冲区;
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid < 0){
// fork()创建子进程失败
perror("fork error");
return -1;
}else if(pid == 0){
//child
printf("In the child process, child = %d parent = %d\n", getpid(), getppid());
}else if(pid > 0){
//parent
printf("In the parent process, child = %d parent = %d\n", pid, getpid());
}
return 0;
}
6. 孤儿进程
孤儿进程:父进程优先于子进程结束。
创建子进程成功,父进程退出,子进程不退出,子进程是孤儿进程,则init
进程作为子进程的父进程;
子进程失去父进程之后,会认为1号进程(即init
进程:负责初始化硬件,回收资源,回收和管理子进程)是自己的父进程。如果很多子进程都认为1号进程是自己的父进程,那么1号进程的负担会很大。所以,在编写代码时尽量让父进程回收完子进程的资源之后再结束。
孤儿进程是没有危害的。
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid < 0){
perror("fork error");
return -1;
}else if(pid == 0){
printf("In the child process, child = %d parent = %d\n", getpid(), getppid());
while(1);
}else if(pid > 0){
printf("In the parent process, child = %d parent = %d\n", pid, getpid());
}
return 0;
}
7. 僵尸进程
僵尸进程:子进程优先于父进程结束,但父进程没有回收子进程的资源。换句话说,子进程的任务已经完成了,但资源得不到回收,子进程就变成了僵尸进程。
- 僵尸态:进程退出,无法唤醒,进程资源不释放;
- 父进程退出,回收子进程的资源;
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc,const char *argv[])
{
pid_t pid = fork();
if (-1 == pid) {
perror("fork");
return -1;
}else if(0 == pid) {
//子进程结束,父进程未结束,资源没有被回收,变成僵尸进程
exit(0);
}else if(pid > 0) {
//父进程
while(1) {
sleep(1);
}
}
return 0;
}
僵尸进程是有危害的。回收僵尸进程的方法:
-
阻塞回收任意一个子进程资源 wait :父进程调用 wait 来回收子进程的资源,会阻塞等待回收,降低父进程的工作效率。
函数原型:
pid_t wait(int *status);
- 功能:阻塞等待子进程的状态发生改变,如果子进程状态未改变,函数阻塞,直到子进程状态改变,如果子进程状态是退出,wait函数回收子进程的资源;
- 参数:status 状态值;
wait()
会暂时停止目前进程的执行, 即阻塞父进程,等待子进程结束或者其他信号;- 如果在调用
wait()
时子进程已经结束, 则wait()
会立即返回子进程结束状态值; - 子进程的结束状态值会由参数 status 返回, 而子进程的进程识别码也会一并返回.;
- 如果不考虑结束状态值, 则参数 status 可以设成 NULL;
- 返回值:成功 获取子进程的ID号;失败 -1;
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> int main(int argc,const char *argv[]) { pid_t pid = fork(); if (-1 == pid) { perror("fork"); return -1; }else if(0 == pid) { exit(0); }else if(pid > 0) { sleep(6); // 此时子进程为僵尸进程 wait(NULL); // 子进程资源被回收 } return 0; }
-
非阻塞回收任意一个子进程资源 waitpid:父进程调用 waitpid 来回收子进程资源,需要频繁调用函数去查看子进程是否结束。
函数原型:
pid_t waitpid(pid_t pid, int *status, int options);
-
功能:回收子进程的资源;
-
参数:
-
pid
pid < -1
:等待进程组 ID 等于 pid 的绝对值的进程组下的任一子进程;pid = -1
:等待任何一个子进程退出,此时和wait()
作用一样;pid = 0
:等待与父进程同组的任何一个子进程;pid > 0
:指定进程号执行等待; -
status
:状态值同 wait; -
options
:0:同
wait()
,阻塞父进程,等待子进程退出;WNOHANG
:非阻塞函数,不论子进程是否退出,立刻返回;WUNTRACED
:若 pid 指定进程已被暂停,且其状态自暂停以来还未报告过,则返回其状态 ;
-
-
返回值
options 0
:成功 子进程的ID号否则阻塞;失败 -1WNOHANG
:成功 子进程退出,返回子进程的ID号;子进程不退出,返回0。失败 -1
#include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> int main(int argc,const char *argv[]) { pid_t pid = fork(); if (-1 == pid) { perror("fork"); return -1; }else if(0 == pid) { exit(0); }else if(pid > 0) { while(1){ sleep(1); waitpid(-1, NULL, WNOHANG); } } return 0; }
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, const char *argv[]) { pid_t pid = fork();112 if(pid < 0){ perror("fork error"); return -1; }else if(pid == 0){ sleep(10); printf("In the child process, child = %d parent = %d\n", getpid(), getppid()); }else{ pid_t ret; while((ret = waitpid(-1, NULL, WNOHANG)) == 0){ sleep(1); } printf("ret = %d\n", ret); printf("In the parent process, child = %d parent = %d\n", pid, getpid()); while(1); } return 0; }
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, const char *argv[]) { pid_t pid = fork(); if(pid < 0){ perror("fork error"); return -1; }else if(pid == 0){ printf("In the child process, child = %d parent = %d\n", getpid(), getppid()); while(1); }else{ pid_t ret = waitpid(-1, NULL, WNOHANG); printf("ret = %d\n", ret); printf("In the parent process, child = %d parent = %d\n", pid, getpid()); while(1); } return 0; }
-
-
信号 signal:使用信号回收僵尸进程优于 wait 函数和 waitpid 函数。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <signal.h> void myfun(int signum){ pid_t pid = wait(NULL); } int main(int argc, const char *argv[]) { if(SIG_ERR == signal(SIGCHLD, myfun)){ perror("signal"); return -1; } pid_t pid = fork(); if(-1 == pid){ perror("fork error"); return -1; }else if(0 == pid){ sleep(6); exit(0); }else if(pid > 0){ while(1){ sleep(1); } } return 0; }
8. 守护进程
8.1 定义及作用
守护进程(后台进程):由系统自动创建,系统开启,进程自动创建,系统关闭,进程自动退;响应任务,但可能不执行任务;
Linux 系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括:
- 系统日志进程
syslogd
; - web服务器
httpd
; - 邮件服务器
sendmail
; - 数据库服务器
mysqld
等。
8.2 创建流程
守护进程的创建:
-
fork()
创建子进程,父进程exit()
退出。子进程成为孤儿进程,摆脱父进程,init进程成为其父进程。
由于守护进程是脱离控制终端的,因此,完成第一步后就会在 Shell 终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在 Shell 终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。
-
子进程中调用
setsid()
函数创建新的会话。在调用了
fork()
函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而setsid()
函数能够使进程完全独立出来。 -
再次
fork()
一个孙进程并让子进程退出。为什么要再次fork呢,假定有这样一种情况,之前的父进程fork出子进程以后还有别的事情要做,在做事情的过程中因为某种原因阻塞了,而此时的子进程因为某些非正常原因要退出的话,就会形成僵尸进程,所以由子进程fork出一个孙进程以后立即退出,孙进程作为守护进程会被init接管,此时无论父进程想做什么都随它了。
-
在孙进程中调用
chdir()
函数,让根目录"/"
成为孙进程的工作目录(守护进程的默认工作目录是根目录)。这一步也是必要的步骤,使用
fork
创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如/mnt/usb
)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"
作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp
,改变工作目录的常见函数是chdir
。int chdir(const char *path);
- 参数:路径名(改变目录) —— “/”;
-
在孙进程中调用
umask()
函数,设置进程的文件权限掩码为 0。umask(mode_t mask);
参数:修改的值;文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是
umask
。在这里,通常的使用方法为umask(0)
。 -
在孙进程中关闭任何不需要的文件描述符。
同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。
在上面的第2)步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。
int getdtablesize(void);
功能:获取最大文件描述符
for(fd = 0; fd < getdtablesize;fd++){ close(fd); }
-
守护进程退出程序。
当用户需要外部停止守护进程运行时,往往会使用
kill
命令停止该守护进程。所以,守护进程中需要编码来实现kill
发出的signal
信号处理,达到进程的正常退出。
实例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
void creat_daemon();
int main(int argc, const char argv[]){
int fd;
// 创建守护进程
creat_daemon();
return 0;
}
void creat_daemon(){
// 创建子进程并退出父进程;
pid_t pid = fork();
if (pid == -1)
{
printf("fork error!\n");
exit(1);
}else if (pid)
{
exit(0);
}
// 在子进程中创建新的会话
if (-1 == setsid())
{
printf("setsid error!\n");
exit(1);
}
// 在子进程中创建孙进程
pid = fork();
if (pid == -1)
{
printf("fork error!\n");
exit(1);
}else if (pid)
{
// 退出孙进程的父进程
exit(0);
}
// 改变孙进程的工作目录
chdir("/");
// 设置进程的文件权限掩码
umask(0);
// 关闭不需要的文件描述符
for (int i = 0; i < 3; i++)
{
close(i);
}
return;
}
9. exec函数族
我们用fork
函数创建新进程后,经常会在新进程中调用exec
函数去执行另外一个程序。当进程调用exec
函数时,该进程被完全替换为新程序。因为调用exec
函数并不创建新进程,所以前后进程的ID
并没有改变。
函数族:exec函数族分别是execl
,execlp
,execle
,execv
,execvp
,execvpe
。
函数原型:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
-
功能:运行新进程,替换到调用进程,并且新进程占有调用进程的地址空间;
-
参数:
path:可执行文件的路径名;
file:如果参数 file 中包含
/
,则将其视为路径名,否则按PATH
环境变量,在它所指的各目录中搜索可执行文件。arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且 arg 必须以
NULL
结束; -
返回值:exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
使用实例见:(115条消息) linux进程—exec族函数(execl, execlp, execle, execv, execvp, execvpe)_云英的博客-CSDN博客
四、线程
1. 线程的定义
线程:进程中的一个实体,是CPU调度和分派的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。线程在运行中呈现间断性。
线程由进程创建,创建的线程共享该进程的地址空间(线程与线程之间,共享进程的数据段、程序文本段、堆区, 每一个线程享有自己独立的栈区,保存自己使用的局部变量,参数,返回值),在同一个进程中创建的线程共享该进程的地址空间;;
进程的两个基本属性:
- 进程是一个可拥有资源的独立单位;
- 进程是一个可以独立调度和分派的基本单位。
多线程与子进程:
-
子进程拥有自己独立的地址空间,如果实现进程间数据的交互,需要引入进程间通信机制;
线程与线程之间通信,不需要引入通信机制,线程间是共享进程的数据段(全局变量,静态变量),因此线程间需要进行通信直接操作共享进程的数据段;
将并发执行的任务(线程)访问同一个数据,称之为竞态;
线程间进行通信,需要同步互斥机制,保证任何一个时刻,只有一个任务(线程)在访问共享数据(保证数据的原子性)(任何一个时刻,只有一个线程进入自己的临界区);
临界区:将任务(线程)访问共享数据的操作代码;
-
多线程可以相应多个任务,相应多任务,也可以通过创建子进程来完成,但是频繁创建子进程带来系统消耗;
创建子进程需要父进程的所有属性,但是创建线程不需要复制,线程共享进程的属性;
2. 线程的创建与退出
函数原型:线程创建。
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
-
功能:在进程中创建新线程;
-
参数:
thread:指向线程标识符的指针;
attr:设置线程的属性,默认为
NULL
;start_routine:线程运行函数起始地址;
arg:运行函数的参数,它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。;
-
返回值:成功 0;失败 错误码;
函数原型:线程退出。
#include <pthread.h>
noreturn void pthread_exit(void *retval);
- 功能:终止调用线程;通常情况下,
pthread_exit()
函数是在线程完成工作后无需继续存在时被调用。如果main()
是在它所创建的线程之前结束,并通过pthread_exit()
退出,那么其他线程将继续执行。否则,它们将在main()
结束时自动被终止。 - 参数:retval 退出时,设置的状态值;
- 返回值:无返回值;
代码实例:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define NUMBER_OF_THREADS 10
void *PrintHelloWorld(void * tid);
int main()
{
// 定义线程描述符,多个文件描述符可用数组
pthread_t thread[NUMBER_OF_THREADS];
for (int i = 0; i < NUMBER_OF_THREADS; i++)
{
printf("Main here. Creating thread %d\n", i);
// 创建线程,传参分别为(指向线程标识符的指针,线程属性NULL,线程运行的打印函数,线程运行函数参数)
int status = pthread_create(&thread[i], NULL, PrintHelloWorld, (void *)i);
if (status)
{
printf("pthread_create return error code %d!\n", status);
exit(-1);
}
}
// 退出线程
pthread_exit(NULL);
return 0;
}
// 线程运行函数
void *PrintHelloWorld(void * tid)
{
printf("Hello World %d.\n", tid);
pthread_exit(0);
}
Main here. Creating thread 0
Main here. Creating thread 1
Hello World 0.
Main here. Creating thread 2
Hello World 1.
Hello World 2.
Main here. Creating thread 3
Main here. Creating thread 4
Hello World 3.
Main here. Creating thread 5
Hello World 4.
Main here. Creating thread 6
Hello World 5.
Main here. Creating thread 7
Hello World 6.
Main here. Creating thread 8
Hello World 7.
Main here. Creating thread 9
Hello World 8.
Hello World 9.
由于没有在主线程中等待我们创建出来的10个线程执行完毕,所以创建出来的子线程可能还没来得及执行完成,就因为主线程(main函数)执行完毕而终止整个进程,导致子线程没法运行。因此printf
得到的Hello world
不是10个,其数量是无法预知的,其顺序也是无法预知的。
3. 阻塞等待线程回收资源
函数原型:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
-
功能:
- 阻塞等待其他线程结束:当调用
pthread_join()
时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。 - 对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用
pthread_join()
的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。
- 阻塞等待其他线程结束:当调用
-
参数:
thread:线程标识符,即线程 ID,标识唯一线程(用户自定义);
retval:用户定义的指针,用来存储被等待线程的返回值;
-
返回值:成功 0;失败 错误码;
代码实例:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define NUMBER_OF_THREADS 10
void *PrintHelloWorld(void *tid);
int main()
{
pthread_t thread[NUMBER_OF_THREADS];
for (int i = 0; i < NUMBER_OF_THREADS; i++)
{
printf("Main here. Creating thread %d\n", i);
int status = pthread_create(&thread[i], NULL, PrintHelloWorld, (void *)i);
if (status)
{
printf("pthread_create return error code %d!\n", status);
exit(-1);
}
// 阻塞等待进程结束回收资源
pthread_join(thread[i], NULL);
}
exit(0);
}
void *PrintHelloWorld(void *tid)
{
printf("Hello World %d.\n", tid);
pthread_exit(0);
}
Main here. Creating thread 0
Hello World 0.
Main here. Creating thread 1
Hello World 1.
Main here. Creating thread 2
Hello World 2.
Main here. Creating thread 3
Hello World 3.
Main here. Creating thread 4
Hello World 4.
Main here. Creating thread 5
Hello World 5.
Main here. Creating thread 6
Hello World 6.
Main here. Creating thread 7
Hello World 7.
Main here. Creating thread 8
Hello World 8.
Main here. Creating thread 9
Hello World 9.
4. 取消线程
函数原型:
/* Cancel THREAD immediately or at the next possibility. */
extern int pthread_cancel (pthread_t __th);
-
功能:向线程发送一个取消请求,将线程退出;
-
参数:thread 线程标识符;
-
返回值:成功 0;失败 错误码;
函数原型:
/* Test for pending cancellation for the current thread and terminate
the thread as per pthread_exit(PTHREAD_CANCELED) if it has been
cancelled. */
extern void pthread_testcancel (void);
-
功能:测试当前线程是否取消,如果线程已被取消,则按照pthread_exit(PTHREAD_CANCELED)终止线程;
-
参数:无;
-
返回值:无;
代码实例:
#include <stdio.h>
#include <pthread.h>
void *child(void *arg){
int i = 0;
while (1)
{
printf("child running: %d\n", i++);
pthread_testcancel();
}
}
int main(){
pthread_t pid;
pthread_create(&pid, NULL, child, NULL);
sleep(1);
pthread_cancel(pid);
pthread_join(pid, NULL);
}
5. 线程的同步互斥机制
当多线程在访问全局数据段,实现通信,产生竞态,引入同步互斥机制,保证任何一个时刻只有一个线程在访问共享数据段。
5.1 锁
多线程访问共享资源的时候,避免不了资源竞争而导致数据错乱的问题,所以我们通常为了解决这一问题,都会在访问共享资源之前加锁。
最常用的就是互斥锁,当然还有很多种不同的锁,比如自旋锁、读写锁、乐观锁等,不同种类的锁自然适用于不同的场景。
如果选择了错误的锁,那么在一些高并发的场景下,可能会降低系统的性能,这样用户体验就会非常差了。
所以,为了选择合适的锁,我们不仅需要清楚知道加锁的成本开销有多大,还需要分析业务场景中访问的共享资源的方式,再来还要考虑并发访问共享资源时的冲突概率。
对症下药,才能减少锁对高并发性能的影响。
5.1.1 互斥锁
互斥锁代表是一种锁资源,互斥锁的工作原理:保证对共享资源操作的原子性(完整性);
当多线程在对同一个资源进行访问,在访问之前,执行申请锁操作,如果某一个线程获取到该锁,开始执行访问数据;如果线程在申请锁时,锁资源已经被占用,该线程就会执行等待,直到锁资源被释放;
互斥锁不能保证线程(任务)的执行先后;
当访问共享数据的任务采用了互斥锁,其他任务在访问同一个数据时,必须采用互斥锁机制,如果某一个任务不执行锁操作,锁资源无效;
初始化锁
-
静态创建:用宏
PTHREAD_MUTEX_INITIALIZER
来静态的初始化锁,采用这种方式比较容易理解,互斥锁是pthread_mutex_t
的结构体,而这个宏是一个结构常量。静态初始化锁函数原型:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-
动态创建函数原型:
/* Initialize a mutex. */ extern int pthread_mutex_init (pthread_mutex_t *__mutex, const pthread_mutexattr_t *__mutexattr)
-
功能:初始化互斥锁;
-
参数:
mutex:互斥锁的标识符;
attr:互斥锁的属性;
-
返回值:成功 0;失败 错误码;
-
销毁锁/释放锁
函数原型:
/* Destroy a mutex. */
extern int pthread_mutex_destroy (pthread_mutex_t *__mutex)
-
功能:释放锁资源;
-
参数:
mutex:互斥锁的标识符;
-
返回值:成功 0;失败 错误码;
尝试加锁/申请互斥锁
函数原型:
/* Try locking a mutex. */
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex)
-
功能:执行上锁操作(非阻塞);
-
参数:
mutex:互斥锁的标识符;
-
返回值:成功 0;失败 错误码;
加锁/申请互斥锁
函数原型:
/* Lock a mutex. */
extern int pthread_mutex_lock (pthread_mutex_t *__mutex)
-
功能:执行上锁操作(阻塞);
-
参数:
mutex:互斥锁的标识符;
-
返回值:成功 0;失败 错误码;
解锁
函数原型:
/* Unlock a mutex. */
extern int pthread_mutex_unlock (pthread_mutex_t *__mutex)
-
功能:执行解锁操作;
-
参数:mutex 互斥锁的标识符;
-
返回值:成功 0;失败 错误码;
互斥锁死锁理解不深
产生死锁的方式:
- 互斥锁交叉嵌套;
- 同一个互斥锁嵌套使用;
- 占有所资源的任务异常退出,锁资源未释放;
学习:(115条消息) Linux环境下,C语言预防死锁的方法、产生死锁的实际情况(参考APUE的11章以及12章)_c语言死锁_ySh_ppp的博客-CSDN博客
5.1.2 读写锁
为了解决性能问题,引入读写锁,特点是:读共享,写独享,写优先级高,读写之间也是互斥的。适合读线程较多的场景。
🌴🌴 注意:在A线程加锁状态下,当B线程请求读锁,然后C线程请求写锁时,在A释放锁的情况下,C请求的写锁先获取到。
函数原型:
//初始化
pthread_rwlock_t mutex = PTHREAD_RWLOCK_INITIALIZER;
//或者
int pthread_rwlock_init(pthread_rwlock_t * __restrict,
const pthread_rwlockattr_t * _Nullable __restrict);
//加锁(读写)
int pthread_rwlock_rdlock(pthread_rwlock_t *);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *)
int pthread_rwlock_wrlock(pthread_rwlock_t *);
int pthread_rwlock_trywrlock(pthread_rwlock_t *);
//解锁
pthread_rwlock_unlock();
//释放锁
int pthread_rwlock_destroy(pthread_rwlock_t * )
5.2. 信号量
使用信号量可以实现协同确定任务的执行先后,保证任何一个时刻只有一个任务进入临界区;
信号量的工作原理:信号量声明信号量的值;
对于访问共享资源,都需要先访问信号量的值:申请信号量:P操作 -1;释放信号量:V操作 +1
当执行任务申请信号量时,如果当前信号量的值大于0,申请成功;反之信号量的值为 -1;如果此时信号量的值为0,申请就会阻塞,直到其他任务释放信号量;
-
初始化信号量的值,函数原型:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);
-
功能:初始化信号量;
-
参数:
sem:信号量的标识符;
pashred:0 用于线程间,非0 用于进程间;
value:信号量的初始值;
-
返回值:成功 0;失败 -1;
-
-
申请信号量(加锁),函数原型:
int sem_wait(sem_t *sem);
#include <semaphore.h> int sem_wait(sem_t *sem); // 信号量 < 0,函数阻塞等待(阻塞) int sem_trywait(sem_t *sem); // 非阻塞 int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict abs_timeout);
-
功能:申请信号量,信号量的值 -1;
-
参数:
sem:信号量的标识符;
-
返回值:成功 0;失败 -1;
-
-
释放信号量(解锁),函数原型:
#include <semaphore.h> int sem_post(sem_t *sem);
-
功能:释放信号量信号量的值 +1;
-
参数:
sem:信号量的标识符;
-
返回值:成功 0;失败 -1;
-
-
摧毁信号量,函数原型:
#include <semaphore.h> int sem_destroy(sem_t *sem);
-
功能:销毁信号量;
-
参数:
sem:信号量的标识符;
-
返回值:成功 0;失败 -1;
-
-
获取当前信号量的值,函数原型:
#include <semaphore.h> int sem_getvalue(sem_t *restrict sem, int *restrict sval);
-
功能:获取当前信号量的值;
-
参数:
sem:信号量的标识符;
sval:保存当前信号量的值;
-
返回值:成功 0;失败 -1;
-
代码实例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define NUMBER_OF_THREADS 2
// 定义信号量标识符
sem_t sem;
void *func1(void *arg){
// 申请信号量,信号量值 -1
sem_wait(&sem);
int *running=arg;
printf("pthread2 running\n");
printf("%d\n",*running);
}
void *func2(void *arg){
// 释放信号量,信号量 +1
sem_post(&sem);
printf("pthread2 running\n");
}
void pthread_log(int status){
if(status){
printf("pthread_create return error code!\n", status);
exit(-1);
}
}
int main(){
// 初始化信号量,线程间通信且设置信号量初始值为0;
sem_init(&sem, 0, 0);
// 定义线程描述符2个
pthread_t thread[NUMBER_OF_THREADS];
int status, a = 5, i = 0, value = 0;
// 创建线程1
status = pthread_create(&thread[0], NULL, func1, (void *)&a);
pthread_log(status);
printf("main thread1 running!\n");
sleep(5);
// 创建线程2
status = pthread_create(&thread[1], NULL, func2, (void *)&a);
pthread_log(status);
printf("main thread2 running!\n");
// 获取当前信号量的值
sem_getvalue(&sem, &value);
printf("sem = %d\n", value);
// 回收资源
pthread_join(thread[0], NULL);
pthread_join(thread[1], NULL);
// 销毁信号量集
sem_destroy(&sem);
return 0;
}
5.3. 条件变量
条件变量工作原理:对当前不访问共享资源的任务,直接执行睡眠处理,如果此时需要某个任务访问资源,直接将该任务唤醒 ;
- 条件变量类似异步通信,操作的核心:睡眠、唤醒;
- 条件变量:如果唤醒操作发生在睡眠之前,唤醒操作无效,睡眠处理一定发生在唤醒之前;
条件变量本质不是锁,通常与互斥锁配合使用。当条件不满足时,阻塞线程。当条件满足时,通知线程解除阻塞。
-
初始化条件变量:
-
方法一:静态初始化条件变量;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
-
方法二:动态初始化条件变量,函数原型:
#include <pthread.h> int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
-
功能:初始化条件变量;
-
参数:
cond:条件变量标识符;
addr:属性;
-
返回值:成功 0,失败 错误码;
-
-
-
条件不足阻塞,函数原型:
#include <pthread.h> int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
-
功能:主动挂起等待信号的到来;
每个条件变量总是有一个互斥锁与之关联,当
pthread_cond_wait
运行后,会原子地执行2个动作;- 给互斥锁mptr解锁,让出这把锁的使用权,把调用线程投入睡眠;
- 直到另外某个线程就本条件变量调用
pthread_cond_signal
,pthread_cond_wait
返回前重新给mptr上锁;
-
参数:
cond:条件变量标识符;
mutex:互斥锁标识符;
-
返回值:成功 0,失败 错误码;
-
-
条件满足则唤醒,函数原型:
#include <pthread.h> int pthread_cond_signal(pthread_cond_t *cond);
-
功能:发送一个条件变量成立的信号,解除阻塞,执行唤醒操作;
-
参数:cond 条件变量标识符;
-
返回值:成功 0,失败 错误码;
-
-
释放条件变量,函数原型:
#include <pthread.h> int pthread_cond_destroy(pthread_cond_t *cond);
-
功能:摧毁条件变量;
-
参数:cond 条件变量标识符;
-
返回值:成功 0,失败 错误码;
-
代码实例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#define N 128
char buf[N] = "";
pthread_mutex_t lock;
pthread_cond_t cond;
void *thread_handler1(void *arg){
while(1){
// 从终端向buf存入N个字节
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
// 唤醒cond
pthread_cond_signal(&cond);
}
// 线程退出
pthread_exit(0);
}
void *thread_handler2(void *arg){
while(1){
sleep(3);
printf("thread2 running!\n");
// 加锁并挂起等待信号
pthread_mutex_lock(&lock);
pthread_cond_wait(&cond, &lock);
printf("buf = %s\n", buf);
// 解锁
pthread_mutex_unlock(&lock);
}
pthread_exit(0);
}
int main(int argc, const char *argv[]){
pthread_t thread1, thread2;
// 初始化互斥锁和条件变量
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&cond, NULL);
// 创建线程
if(pthread_create(&thread1, NULL, thread_handler1, NULL) != 0){
perror("pthread_create error!");
exit(-1);
}
if(pthread_create(&thread2, NULL, thread_handler2, NULL) != 0){
perror("pthread_create error!");
exit(-1);
}
// 阻塞等待回收资源
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 释放锁
pthread_mutex_destroy(&lock);
// 释放条件变量
pthread_cond_destroy(&cond);
return 0;
}
五、通信机制
1. 进程的通信机制
2. 无名管道
无名管道:在进程访问的共有的内核空间上创建一个特殊的文件,称为管道;
无名管道属性:
- 只能用于具有亲缘关系的进程之间的通信,也就是说,匿名管道只能用于父子进程之间的通信;
- 半双工的通信模式,具有固定的读端和写端;
- 管道可以看成是一种特殊的文件,对于它的读写可以使用文件 IO 如 read、write 函数;
- 无名管道的操作属于一次性操作,如果对无名管道执行读操作,数据就会被读走;
- 无名管道管道的大小是固定的,管道一旦写满,写操作就会阻塞,管道的大小为
64K
; - 当管道中无数据,执行读操作,读操作阻塞;
- 无名管道写满,写操作阻塞,如果管道中有大于
4K
的空间,写操作可以继续,每次最多写入4K的整倍数; - 无名管道不保证操作的原子性,如果当前管道,满足读写条件,读写可以并发;
- 向无名管道中写数据,将读端关闭,管道损坏,进程收到信号(SIGPIPE),将进程退出;
- 当管道中有数据,将写端关闭,读操作可以执行,之后数据读完,可以继续读取(非阻塞);
创建无名管道,函数原型:
#include <unistd.h>
int pipe(int pipefd[2]);
-
功能:创建无名管道;
-
参数: 一个存储空间为 2 的文件描述符数组,
pipefd[0]:管道的读端;
pipefd[1]:管道的写端;
pipefd[2] :保存的是操作管道的两个文件描述符;
-
返回值:成功 0,失败 -1;
实例:实现一个进程向另一个进程传输文件内容
代码实例:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define Size 128
int main(int argc, const char *argv[]){
int fd[2];
char buf[Size] = {0};
// 建立管道
if (pipe(fd) != 0){
printf("create pipe failed!\n");
return -1;
}
int pid = fork();
if (pid < 0){
printf("fork failed!\n");
return -1;
}else if (pid == 0){
printf("child!\n");
// 关闭子进程写端,操作读端,从管道内读取128字节数据
close(fd[1]);
read(fd[0], buf, 128);
printf("child printf message from parent: %s!\n", buf);
}else{
printf("parent!\n");
sleep(3);
// 关闭父进程的读端,操作写端,向管道写入"hello from parent"
close(fd[0]);
write(fd[1], "hello from parent", strlen("hello from parent"));
}
return 0;
}
无名管道缺点:无名管道由于没有名字,只能用于父子进程间的通信。为了克服这个缺点,提出了有名管道;
3. 有名管道
有名管道可以使互不相关的两个进程互相通信。有名管道可以通过路径名来指出,并且在文件系统中可见;
进程通过文件IO来操作有名管道;遵循先进先出规则;不支持如lseek()
操作 ;
创建有名管道,函数原型:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
-
功能:创建有名管道;
-
参数:
pathname:路径名(管道名);
mode:文件所属用户的执行权限;
-
返回值:成功 0,失败 -1;
代码实例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#define N 128
int main(int argc, const char *argv[]){
int fd;
char buf[N] = "";
if(mkfifo("fifo", 0775) != 0){
if(errno == EEXIST){
fd = open("fifo", O_RDWR);
printf("fifo exit,fd = %d\n", fd);
}else{
perror("mkfifo error!");
return -1;
}
}else{
fd = open("fifo", O_RDWR);
printf("fd = %d\n", fd);
}
while(1){
read(fd, buf, N);
printf("buf:%s\n", buf);
}
return 0;
}
有名管道缺点:管道这种进程通信方式虽然使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。
4. system V IPC
System V引入了三种高级进程间的通信机制:消息队列、共享内存和信号灯(信号量集)。
4.1 消息队列
消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。
消息队列中消息本身由消息类型和消息数据组成,通常使用如下结构:
struct msgbuf {
long mtype; /* message type, must be > 0 */ 消息的类型(选择性的读取)
char mtext[1]; /* message data */ 消息的正文(数组,变量,结构体)
};
消息队列工作原理:在内核空间上创建队列,信息发送者将发送信息打包成节点添加到队列中,信息的接受者选择性从队列上读取想要的节点;
ipcs -q
:查看系统中使用消息队列的情况;ipcrm -q + msqid
:删除消息队列;
消息队列:创建队列,向队列中添加信息,从队列移除信息,实现队列的控制(获取队列的属性,设置队列的属性,删除不使用队列);
-
生成 key 值(键值),确保消息队列的唯一性,函数原型:
#include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
-
功能:生成 key 值;
-
参数:
pathname:路径名,用户给定,必须真实存在;
proj_id:传字符;可以根据自己的约定,随意设置。这个数字,有的称之为 project ID; 在UNIX 系统上,它的取值是 1 到 255;
-
返回值:成功 key值 随机数;失败 -1;
-
-
创建消息队列,函数原型:
#include <sys/msg.h> int msgget(key_t key, int msgflg);
-
功能:创建消息队列,或访问一个已经存在的消息队列。;
-
参数:
key值:0或非0,key值确保消息队列的唯一性;
msgflg:标志位。
IPC_CREAT|IPC_EXCL|0664
:创建并打开消息队列(如果消息队列不存在,自动创建,如果已存在,返回 EEXIST)访问权限为0664; -
返回值:成功 消息队列标识符;失败 -1;
-
-
消息添加到消息队列末尾,函数原型:
#include <sys/msg.h> int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
-
功能:将消息添加到队列的末尾;
-
参数:
msqid:消息队列的标识符;
msgp:发送的消息;
msgsz:消息正文的大小
sizeof(struct msgbuf) - sizeof(long)
;msgflg 0:消息队列没空间,写操作阻塞;
-
返回值:成功 0;失败 -1;
-
-
将消息从队列中移除,函数原型:
#include <sys/msg.h> ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
-
功能:将消息从队列中移除;
-
参数:
msqid:消息队列标识符;
msgp:保存接受的消息(定义和写入时保持一致);
msgsz:消息正文的大小;
msgtyp:消息的类型
- =0:读取消息队列中的第一个消息;
- >0:读取消息队列中类型为 msgtype 的第一个消息;
- <0:读取消息队列中不小于 msgtyp 的绝对值,且类型最小的第一个消息;
msgflg 0:消息队列中无数据,读操作阻塞;
-
返回值:成功 消息正文的大小;失败 -1;
-
-
对消息队列的控制,函数原型:
#include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf);
-
功能:实现对消息队列的控制;
-
参数:
msqid:消息队列的标识符;
cmd:指定消息队列的操作处理;
IPC_STAT
从第三个参数读取消息队列的属性信息,IPC_SET
设置消息队列的属性;IPC_RMID
删除消息队列;buf:描述消息队列属性的结构体;
struct msqid_ds { struct ipc_perm msg_perm; /* Ownership and permissions */ 消息队列的权限 time_t msg_stime; /* Time of last msgsnd(2) */ 消息队列发送消息的时间 time_t msg_rtime; /* Time of last msgrcv(2) */ 消息队列接受消息的时间 time_t msg_ctime; /* Time of last change */ 消息队列改变的时间 unsigned long__msg_cbytes; /* Current number of bytes in queue (nonstandard) */消息队列当前的字节数 msgqnum_t msg_qnum;/* Current number of messages in queue */消息队列当前消息的个数 msglen_t msg_qbytes;/* Maximum number of bytes allowed in queue */消息队列最大的字节数 pid_t msg_lspid; /* PID of last msgsnd(2) */ 发送消息的用户ID pid_t msg_lrpid; /* PID of last msgrcv(2) */ 接收消息的用户ID };
-
返回值:返回值:成功 0;失败 -1;
-
代码实例:实现两个终端数据交互;
-
终端1:
#include <stdio.h> #include <sys/ipc.h> #include <sys/msg.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <errno.h> #include <signal.h> #define N 128 #define TYPE1 100 #define TYPE2 200 // 消息正文的大小 #define SIZE (sizeof(struct msgbuf) - sizeof(long)) // 定义消息队列结构体 struct msgbuf{ long mtype; char mtext[N]; }; int main(int argc, const char *argv[]){ struct msgbuf msg_snd, msg_rcv; key_t key; // 生成key值 if((key = ftok(".", 'a')) == -1){ perror("ftok error!"); return -1; } int msgid; // 创建或打开消息队列,获取消息队列标识符 if((msgid = msgget(key, IPC_CREAT|IPC_EXCL|0664)) == -1){ if(errno != EEXIST){ perror("msgget error!"); return -1; }else{ msgid = msgget(key, 0664); } } // 创建子进程 pid_t pid = fork(); if(pid < 0){ perror("fork error!"); return -1; }else if(pid == 0){ while(1){ // 子进程发送消息 msg_snd.mtype = TYPE1; fgets(msg_snd.mtext, N, stdin); msg_snd.mtext[strlen(msg_snd.mtext) - 1] = '\0'; if(strncmp(msg_snd.mtext, "quit", 4) == 0){ kill(getppid(), SIGKILL); break; } // 将消息添加到消息队列末尾 msgsnd(msgid, &msg_snd, SIZE, 0); } }else{ while(1){ // 父进程接收消息 msgrcv(msgid, &msg_rcv, SIZE, TYPE2, 0); if(strncmp(msg_rcv.mtext, "quit", 4) == 0){ kill(pid, SIGKILL); goto err; } printf("msg_tty2:%s\n", msg_rcv.mtext); } } return 0; err: msgctl(msgid, IPC_RMID, NULL); }
-
终端2:
#include <stdio.h> #include <sys/ipc.h> #include <sys/msg.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <errno.h> #include <signal.h> #include <string.h> #define N 128 #define TYPE1 100 #define TYPE2 200 #define SIZE (sizeof(struct msgbuf) - sizeof(long)) struct msgbuf{ long mtype; char mtext[N]; }; int main(int argc, const char *argv[]){ struct msgbuf msg_snd, msg_rcv; key_t key; // 生成key值 if((key = ftok(".", 'a')) == -1){ perror("ftok error!"); return -1; } int msgid; // 创建或打开消息队列 if((msgid = msgget(key, IPC_CREAT|IPC_EXCL|0664)) == -1){ if(errno != EEXIST){ perror("msgget error!"); return -1; }else{ msgid = msgget(key, 0664); } } // 创建子进程 pid_t pid = fork(); if(pid < 0){ perror("fork error!"); return -1; }else if(pid == 0){ while(1){ // 子进程发送消息 msg_snd.mtype = TYPE2; fgets(msg_snd.mtext, N, stdin); msg_snd.mtext[strlen(msg_snd.mtext) - 1] = '\0'; if(strncmp(msg_snd.mtext, "quit", 4) == 0){ kill(getppid(), SIGKILL); break; } // 将消息添加到消息队列末尾 msgsnd(msgid, &msg_snd, SIZE, 0); } }else{ while(1){ // 父进程接收消息 msgrcv(msgid, &msg_rcv, SIZE, TYPE1, 0); if(strncmp(msg_rcv.mtext, "quit", 4) == 0){ kill(pid, SIGKILL); goto err; } printf("msg_tty1:%s\n", msg_rcv.mtext); } } return 0; err: msgctl(msgid, IPC_RMID, NULL); }
4.2 共享内存
工作原理:系统为每一个进程创建4G
的虚拟地址空间,共享内存开辟一块实际的物理内存区域(共享内存区域),将该物理内存映射给每一个进程;
本质:多进程实际都是在访问同一块物理内存区域;
通信效率最高,适用场合:实现实时数据传输;
多进程访问同一块物理内存区域
容易产生竞态,共享内存结合同步互斥机制,保证数据的正确性,任何一个时刻只有一个任务在访问共享的内存区域;
-
共享内存的创建,函数原型:
#include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
功能 开辟实际物理内存区域; 参数 key 标识共享内存的键值: 0/IPC_PRIVATE。通常此值来源于 ftok 返回的IPC键值; size 物理内存大小,以字节为单位; shmflg IPC_CREAT|IPC_EXCL|0664:创建并打开物理内存(内存不存在,开辟;已存在,报错),0664 权限打开物理内存; 返回值 成功 返回共享内存的标识符; 失败 返回 -1,错误原因存于error中; 错误代码 EINVAL 参数size小于SHMMIN或大于SHMMAX; EEXIST 预建立 key 所指的共享内存,但已经存在; EIDRM 参数 key 所指的共享内存已经删除; ENOSPC 超过了系统允许建立的共享内存的最大值 (SHMALL); ENOENT 参数 key 所指的共享内存不存在,而参数 shmflg 未设 IPC_CREAT 位; EACCES 没有权限; ENOMEM 核心内存不足; 在 Linux 环境中,对开始申请的共享内存空间进行了初始化,初始值为
0x00
。 -
将物理内存映射到进程的虚拟地址空间(进程只需要操作属于自己的虚拟地址空间,本质访问实际物理内存),函数原型:
#include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
功能 将物理内存区域映射到进程的虚拟地址空间(将物理地址转换为虚拟地址); 参数 shmid 共享内存标识符; shmaddr 将物理内存映射进程一个合理的位置(未使用,且内存足够大区域); shmflg 一般为 0,对于共享内存段,可以实现读写; 返回值 成功 返回链接物理内存的虚拟地址; 失败 返回 -1; -
将物理内存与进程虚拟内存的映射关系断开,函数原型:
#include <sys/shm.h> int shmdt(const void *shmaddr);
功能 将物理内存与进程的虚拟内存的映射关系断开; 参数 shmaddr 映射的虚拟地址值; 返回值 成功 返回 0; 失败 返回 -1; -
释放物理内存,函数原型:
#include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能 实现对共享内存的控制; 参数 shmid 共享内存标识符; cmd 指定对共享内存的操作;
IPC_STAT:从第三个参数中读取共享内存的属性信息;
IPC_SET:设置共享内存的属性信息;
IPC_RMID:删除共享内存;buf 描述共享内存属性的结构;
struct shmid_ds { struct ipc_perm shm_perm; /* Ownership and permissions(访问的权限) */ size_t shm_segsz; /* Size of segment (bytes)(共享内存的大小) */ time_t shm_atime; /* Last attach time(上一次被映射的时间) */ time_t shm_dtime; /* Last detach time(上一次被断开映射的时间) */ time_t shm_ctime; /* Last change time(上一次改变的时间) */ pid_t shm_cpid; /* PID of creator(创建共享内存的用户ID) */ pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2)(上一次执行映射和断开映射的进程ID) */ shmatt_t shm_nattch; /* No. of current attaches(被映射的次数(编号)) */ ... };
返回值 成功 返回 0; 失败 返回 -1;
代码实例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/shm.h>
int main(int argc, const char argv[]){
// 创建共享内存标识符
int shmid = shmget(IPC_PRIVATE, 1000, IPC_CREAT|0600);
if(shmid == -1){
perror("shmget error!\n");
exit(1);
}
printf("create shared memory OK. shmid = %d\n", shmid);
// 物理内存与虚拟内存建立联系后写数据
char * shmaddr = (char *)shmat(shmid, NULL, 0);
if((int)shmaddr == -1){
perror("shmat error!\n");
exit(1);
}
strcpy(shmaddr, "hello world");
shmdt(shmaddr);
// 物理内存与虚拟内存建立联系后读数据
shmaddr = (char *)shmat(shmid, NULL, 0);
if((int)shmaddr == -1){
perror("shmat error!\n");
exit(1);
}
printf("%s\n", shmaddr);
// 删除
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
4.3 信号量集
Posix 信号量采用计数信号量,System V 信号量在此基础上增加了一级复杂度,它采用计数信号量集,计数信号量集是由一个或多个计数信号量构成的集合。
对于系统中的每个信号量集,内核都维护一个struct semid_ds
信息结构,它定义在sys/sem.h
头文件中。
struct semid_ds
{
struct ipc_perm sem_perm;
struct sem *sem_base; //指向信号量集的指针
ushort sem_nsems; //信号量集中的信号量个数
time_t sem_otime; //上一次调用semop的时间
time_t sem_ctime; //创建时间或上一次以IPC_SET调用semctl的时间
};
其中,sem_base 是指向信号量集的指针,信号量集中的每个成员都对应一个 struct sem 结构:
struct sem
{
ushort_t semval; //信号量的值
short sempid; //上一次成功调用semop,或以SETVAL、SETALL调用semctl的进程ID
ushort_t semncnt; //等待semval变为大于当前值的线程数
ushort_t semzcnt; //等待semval变为0的线程数
};
-
生成 key 值(键值),函数原型:
#include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
功能 生成键值; 参数 pathname 路径名,用户给定,必须真实存在; proj_id 传字符;可以根据自己的约定,随意设置。这个数字,有的称之为 project ID; 在UNIX 系统上,它的取值是 1 到 255; 返回值 成功 返回 key 值(随机数); 失败 返回 -1; -
信号量级的创建,函数原型:
#include <sys/sem.h> int semget(key_t key, int nsems, int semflg);
功能 用于创建一个新的信号量集或打开一个已存在的信号量集; 参数 key ftok 返回值; nsems 指定了信号量集中的元素个数(该参数只在创建信号量时有效); semflg sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误; 返回值 成功 返回信号量集的标识符; 失败 返回 -1,错误原因存于error中; 错误代码 EACCESS 没有权限; EEXIST 信号量集已经存在,无法创建; EIDRM 信号量集已经删除; ENOENT 信号量集不存在,同时 semflg 没有设置 IPC_CREAT 标志; ENOMEM 没有足够的内存创建新的信号量集; ENOSPC 超出限制; -
改变信号量的值,函数原型:
#include <sys/sem.h> int semop(int semid, struct sembuf *sops, size_t nsops); int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timespec *timeout);
功能 完成对信号量的 P 操作或 V 操作; 参数 semid 信号量集标识符; sops 此结构体为: struct sembuf { unsigned short sem_num; /* 数组中的信号量下标 */ short sem_op; /* 信号量操作 */ short sem_flg; /* 操作标识 */ };
成员解读:
sem_num:信号量集的个数,单个信号量设置为0;
sem_op(PV操作):
1)>0:增加信号量的值(发送操作);
2)<0:减小信号量的值(等待操作);
3)=0:表示对信号量当前值进行是否为 0 的测试;
sem_flg:
1)IPC_NOWAIT:如果不能对信号量集进行操作,则立即返回;
2)SEM_UNDO:当进程退出后,该进程对 sem 进行的操作将撤销;nsops 操作信号量的个数,即 sops 结构变量的个数; 返回值 成功 返回 key 值(随机数); 失败 返回 -1; -
对信号量集的控制,函数原型:
#include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...);
功能 常用于设置信号量的初始值和销毁信号量; 参数 semid 信号量标识符; sem_num 信号量集数组上的下标,表示某一个信号量,填 0; cmd 对信号量命令操作:
1. IPC_RMID:销毁信号量,不需要第四个参数;
2. SETVAL:初始化信号量的值(信号量成功创建后,需要设置初始值),这个值由第四个参数决定。第四参数是一个自定义的共同体,如下(相当于要对信号量设置属性的一个描述信息,将这个描述信息作为参数传给 semctl 函数);
3. GETVAL:获取信号量值;
// 用于信号灯操作的共同体。 union semun { int val; struct semid_ds *buf; unsigned short *arry; };
返回值 成功 情况略为复杂,见 API 手册; 失败 返回 -1;
代码实例:两个进程 PA.C(设置信号量的值,每隔 3s semval 的值减一)PB.C(每隔 1s 获取信号量的值)
-
PA.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/sem.h> typedef union semun{ int val; }semun_t; int main(int argc, const char argv[]){ struct sembuf sb = {0, -1, IPC_NOWAIT}; // 生成key值 key_t key = ftok(".", 31); if(key == -1){ perror("ftok error!\n"); exit(1); } // 使用键值获取信号量集ID int semid = semget(key, 1, IPC_CREAT|0664); if(semid == -1){ perror("semget error!\n"); exit(1); } // 使用信号量集ID设置信号量的初值 semun_t arg; arg.val = 5; int ctl = semctl(semid, 0, SETVAL, arg); if(ctl == -1){ perror("semctl error!\n"); exit(1); } // 每隔3s第一个信号量的值-1 while(1){ sleep(3); // 改变信号量的值 int op = semop(semid, &sb, 1); if(op == -1){ perror("semop error, number < 0!\n"); return -1; } } return 0; }
-
PB.c:
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int main(int argc, const char *argv[]){ // 获取键值 key_t key = ftok(".", 31); if(key == -1){ perror("ftok error!\n"); return -1; } // 使用键值获取信号量集的ID int semid = semget(key, 1, IPC_CREAT|0664); if(semid == -1){ perror("semget error!\n"); return -1; } while(1){ sleep(1); int val = semctl(semid, 0, GETVAL); if(val == -1){ perror("semctl error!\n"); return -1; } printf("semval = %d\n", val); } return 0; }