APUE阅读笔记 囊括了文件IO,信号机制,线程控制,进程控制,网络编程入门,高级IO入门等等内容。APUE这本书实在太厚了,因此这个笔记中只涵盖了关键部分
基础知识
文件与目录
unix下的所有文件和目录本质上都是文件。
输入与输出
带缓冲的输出/输入与不带缓冲的输出/输入。
进程与线程
进程处理函数 fork wait exec
线程处理 pthread
出错处理
通过引入头文件errno.h,可以通过该头文件中的接口来获知具体的异常情况
用户标识
常用的两种用户标识为用户id与组id。分别由getuid(),getgid()得到。
信号
通过引入signal.h头文件,信号是指向进程通知发生了某些异常,从而让进程产生响应。一般有三种响应信号的方法。
- 忽略
- 默认处理
- 自定义函数处理 signal(信号type,处理函数)
时间值
衡量进程时间的三个标准
- 墙上时间 进程运行的时间总量 = 阻塞时间 + 就绪时间 + 运行时间 (进程生命周期总时间)
- 用户时间 进程运行在用户态的时间
- 系统时间 进程运行内核态(进行系统调用)花的时间
系统调用与库函数
虽然两者在用户调用的角度来看没差别,但是实际上前者工作在kernel态,后者则在用户态,完全不一样。
文件I/O
本节描述的函数被称为不带缓冲的函数。
需要头文件
unistd.h
fcntl.h
FOPEN_MAX 表示了最多能被打开的文件数量,用来作文件描述符差错判断用的。
文件描述符
打开/创建文件时,会返回一个最小未定的文件描述符,可以传递给read/write函数,其定义在unistd.h中。常用描述符如下:
- STDIN_FILENO 标准输入 0
- STDOUT_FILENO 标准输出 1
- STDERR_FILENO 标准错误 2
读写标志位的头文件 fcntl.h
open系列
open(const char* path,int oflag,...)
openat(int fd,const char* path,int oflag,...)
oflag表示读写标志位 常用的如下
- O_RDONLY
- O_WRONLY
- O_RDWR
- O_EXEC
上述四个oflag必须指定一个,其余的都是可选项
- O_APPEND
- O_CREAT
- O_TRUNC
在设置了O_CREAT的情况下,还需要设置mode,常用mode如下
- S_IRWXG 该文件组用户 可读可写可执行
- S_IRWXU 该文件用户 可读可写可执行
返回文件描述符
判断文件是否存在函数
#include<unistd.h>
uint16_t access(const char* file,int mode);//如果文件不存在则返回-1,mode:F_OK
if(access(file,F_OK) != -1)//判断文件存在
creat
用来创建文件的函数,这个函数相当于只写(O_WRONLY),截断(O_TRUNC),创建(O_CREAT)并且指定mode的open函数。因此如果要创建文件完毕后,再读该文件,就不得不关闭后,再通过O_RDONLY打开文件再读。
现在创建文件优先使用
open(path,O_RDWR|O_TRUNC|O_CREAT,S_IRWXG);
close
关闭文件
int close(int fd);//fd表示文件描述符
lseek
用来计算文件的当前偏移量,指定文件描述符的文件,并且根据whence的值去设置其偏移量,返回新的偏移量
off_t lseek(int fd,off_t offset,int whence)
whence设置参数
- SEEK_CUR 当前位置设置offset
- SEEK_SET 设置为从文件开始处偏移offset
- SEEK_END 从末尾开始设置,这里的末尾指的是字符串末尾的\0,也就意味着,如果要获取文件的最后一个字节,应该需要SEEK_END-1
eg 判断当前文件偏移量的用法
lseek(fd,0,SEEK_CUR);//得到文件当前偏移量
lseek(fd,0,SEEK_SET);//将文件偏移量置为文件头部
如果返回-1,那么表示文件无法设置偏移量。(比如网络套接字)
文件偏移量是可以超出文件尾部的。从而会形成一个空洞。空洞在访问时会显式成/0
一个简单的修改文件偏移并且读写的例子
#include<iostream>
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
using namespace std;
int main(int argc,char* argv[]){
char str[]="hello";
const int size=strlen(str);
int fd=open("./txt",O_RDWR|O_CREAT,S_IRWXG);
//偏移到文件的最后一个字节开始(也就是\0前面一个字节开始)
int pos=lseek(fd,-1,SEEK_END);
printf("current pos %d\n",pos);
if(write(fd,str,size)!=size)
{
printf("write bug\n");
return 0;
}
char *ch=(char*)malloc(sizeof(char));
//写入完后,文件描述符移动到了\0,需要重新设置才能从头开始读文件
//如果形成空洞文件,那么遇到当中的/0那么就停止读了
lseek(fd,0,SEEK_SET);
//不能以null的方式取io
while(read(fd,ch,sizeof(char))>0)
printf("%c",*ch);
printf("\n");
close(fd);
return 0;
}
read
读文件,返回读到大小,如果读到末尾(EOF)就返回0
size_t read(int fd,void* buf,size_t nbytes)//描述符,缓冲区,要求读取字节数
//常用读到文件尾判断
read(fd,ch,sizeof(char))>0
对于一些情况的说明
- 如果要求读的大小已经超过了目前文件偏移到文件尾的距离,那么能读多少读多少,下一次读返回0
- 对于从网络套接字中读取数据,可能由于阻塞而减少读到的量。
write
ssize_t write(int fd,const void* buf,size_t nbytes)
从当前文件偏移量位置,开始写文件。返回写入的大小。
一个简单的读写文件例子
#include<iostream>
#include<stdlib.h>//malloc所需要头文件
#include<string.h>//字符串处理所需要头文件
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
using namespace std;
int main(int argc,char* argv[]){
char str[]="hello";
const int size=strlen(str);//strlen字符串长度 sizeof字符串大小
int fd=creat("./txt",S_IRWXG);//open("./txt",O_RDWR|O_APPEND,S_IRWXU);
int count=write(fd,str,size);
printf("%d bytes writed\n",count);
close(fd);
int fd2=open("./txt",O_RDONLY);
if(fd2==-1)
{
printf("read bug\n");
return 0;
}
char *ch=(char*)malloc(sizeof(char));
//有一个技巧read(fd,pointer,size)>0,那么表示还有字符能够读到
while(read(fd2,ch,1)>0)
printf("%c\n",*ch);
close(fd);
return 0;
}
上述这些函数都是无缓冲的 并且使用读写函数,都是会修改文件描述符位置的,需要通过lseek去重置。
文件结构
一共有三个结构
- 每个打开文件都有一个v结点信息,包括了文件类型,操作文件的各种函数指针,v结点还包含了i结点信息,包括了文件基本信息(文件长度,所有者等),以及磁盘上文件存储位置的指针信息。
- kernel为所有打开文件维护了一张打开文件表,包含了文件状态标志(当前文件偏移量),以及指向v结点的指针。
- 每个进程在其进程结构中有一张打开文件表,每一项包含了文件描述符和指向内核打开文件表的指针。
三层结构 文件描述符项->内核打开文件表项->v结点。
不同进程打开同一个文件,操作系统会给出不同的打开文件表项,因为每个进程的当前文件偏移量不同,但是会指向同一个v结点。如果一个进程,多次打开同一文件,并且不关闭描述符,那么就会出现一个进程中多个描述符项,指向多个文件表项,并且共享同一个V节点的情况。
文件描述符相关操作函数
用来复制一个现有的文件描述符,生成一个新的可用的描述符(相当于在文件表表项里面新建了一项)。新描述符和原本的描述符共享一个内核文件表项。
int dup(int fd);//当前可用描述符中的最小项
int dup2(int fd,int fd2);//指定返回描述符fd2,如果fd2已经被打开,那么先将其关闭,这意味着可能在调用dup2的过程中,文件描述符会产生变化
一个简单的例子
int new_fd=dup(fd);
while(read(new_fd,buf,size)>0){};//可以使用新的描述符去访问文件信息
由于dup后的文件描述符和原本的文件描述符共享同一个打开文件表项,因此修改dup后的文件描述符的当前位置会影响之前文件描述符。
系统内置描述符值
0:标准输入
1:标准输出
2:错误输出
文件的原子操作
多进程同时写操作可能会带来问题。A进程与B进程在同一个偏移量位置开始写,A写完后,B再写,会覆盖A写的内容,造成错误。
unistd.h提供的原子操作pread和pwrite
ssize_t pread(int fd,void *buf,size_t bytes,off_t offset);//成功读返回读到数据个数
ssize_t pwrite(int fd,void *buf,size_t bytes,off_t offset);//成功写返回已写字节数
与read和write的区别在于:
上述两个函数offset的操作和i/o操作是原子的,先定位再i/o且不可分。其次,上述操作不改变描述符位置的。
所谓原子操作:不可再分的最小操作,要么做,要么就不做
文件缓冲区
内核会将所有需要写入的数据,先放入缓冲区(内核缓冲区)中,接着排入写队列,再写入磁盘。有些函数可以用来进行缓冲区与磁盘的IO同步。(等待写磁盘执行完毕才返回)
调用write函数就只是写缓冲区(页缓冲),需要经过os调度才会写入磁盘,在写完缓冲区后就返回了,如果在写缓冲区后和写入磁盘前这个时间段内产生错误,会使得数据并没有被写入磁盘,持久化失败。
因此引入fsync,该函数会指定文件描述符,确保写入磁盘后再返回,在写入磁盘完成前,进程就一直是阻塞状态,确保正确持久化。不过fsync除了同步文件修改信息外,还有同步一次文件状态信息(比如文件修改时间等等)。fdatasync解决了这个问题,只同步文件修改信息。
int fsync(int fd);//该函数只能确保对一个fd指定的文件起效
int fdatasync(int fd);//使用方法同上
void sync();//不起同步作用,只是将缓冲区数据排入写队列
//使用例子如下,在做写操作前使用即可,返回0表示成功
int fd=open("path",O_RDWR);
fsync(fd);
write(fd,buffer,size);
文件属性
fcntl函数
int fcntl(int fd,int cmd,...)
可以修改已有文件属性,具体修改那个信息看cmd标志位,…表示可选参数。出错返回0,其余返回值和cmd相关。
常用cmd
- F_DUPFD|F_DUPFD_CLOEXEC 复制文件描述符 类似于dup,可知指定新描述的值作为第三个参数
- F_GETFD 获取描述符属性
- F_SETFD 设置新描述符属性
- F_GETFL 得到标识符 (注意标识符和描述符是有区别的)像O_RDONLY,O_WRONLY这些就是标识符 设置O_NONBLOCK就用这个
- F_SETFL 设置标识符
ioctl
通用文件操作函数
stat
用来获取文件大小等文件常用属性
struct stat info;
int error=stat("file",&info);//通过stat结构体和stat函数,来获取文件信息
info.st_size;
标准I/O
上述的文件IO都是根据文件描述符来执行的,而标准IO是使用流来进行的,具体来说是使用文件指针进行操作的。需要头文件如下:
- stdio.h
- stdlib.h
流定向
所谓流定向,是指这个流是使用单个字节进行IO还是使用多个字节进行IO。下述函数可以修改打开标志:
int fwide(FILE* tp,int mode);//根据mode是正还是负去修改,负数是字节定向,正数是宽字节定向
三个标准文件指针
- stdin
- stdout
- stderr
缓冲
与open,read,write不同,标准IO是自带缓冲的IO,最明显的区别是,不带缓冲的话,调接口立马会写入文件,而对于带缓冲的,只有用了fflush才会写入文件。一共有三种类型的缓冲
- 全缓冲 流只有填满了缓冲区才会进行IO操作
- 行缓冲 缓冲区遇到了换行符就进行IO操作
- 不带缓冲 不进行缓冲直接IO操作 unistd.h提供的IO
在IO背景下的flush是指将缓冲区上存在的全部数据全部写入磁盘(搞一波io)。下述函数可以自行设置流的缓冲。标准IO的缓冲区是在空后内存空间的,数据会先读到内核缓冲区,再从内核缓冲区读取到用户的IO缓冲区。
void setbuf(FILE *restrict fp,char *restrict buf);//指定buf下,以全缓冲形式去设置缓冲区,如果buf=NULL,则关闭缓冲区,这里的buf大小必须为BUFSIZE(定义在了stdio.h中)
//使用setvbuf可以显式指定缓冲区大小
//mode _IOFBF全缓冲 _IOLBF 行缓冲 _IONBF 不带缓冲
void setvbuf(FILE *restrict fp,char *restrict buf,int mode,size_t size);
可以使用**fflush(FILE*)**来强行将数据全部写入到磁盘中。(强行冲洗输出缓冲区),由于标准io是带缓冲的,如果不用fflush去强行刷新的话,那么就必须等到os自行操作缓冲,才会被写入磁盘。
这就意味着有实时读写需求的话,最好就不要用标准io?
基本操作
打开流
指定路径和type就可以打开一个流
//指定文件,返回指向该文件的文件指针
FILE* fopen(const char *restrict path,char *restrict type);
//在指定流file上打开文件,如果流已经打开,那么先关闭再打开,如果已经定向,那么清楚定向
FILE* freopen(const char *restrict path,char *restrict type,FILE* file);
//常用于网络通信中,将文件描述符和流关联起来
FILE* fdopen(int fd,const char *type);
type常用属性
- r O_RDONLY
- w O_WRONLY|O_CREAT|O_TRUNC 这里的O_TRUNC表示会复写源文件,注意所谓复写指接下来的write操作会全部会在清空后的原文件里面写,而不是每写一次都清空
- a O_WRONLY|O_CREAT|O_APPEND
- r+ O_RDWR
- w+ O_RDWR|O_CREAT|O_TRUNC
- a+ O_RDWR|O_CREAT|O_APPEND
使用流的方式创建文件没有办法设置文件权限位。
文件按上述类型打开流**,默认是全缓冲的**,如果引用终端设备,那么是行缓冲的。在打开流与执行操作前,可以使用setbuf和setvbuf去设置缓冲区类型。
//关闭流
int fclose(FILE* file);
如果调用exit函数(往往exit(0)),那么会直接fflush缓冲区,并且关闭流。
读写
一共有三种类型的流式读写方法
- 按字节
- 按行
- fread/fwrite两者皆可,常用于读写二进制
按字节
//读操作会将读的下一个字符强行转换成int类型返回,因为要与EOF(-1)作比较,区别如下
//getc是宏定义,最好不要用
int getc(FILE* fp);
//fget是函数
int fgetc(FILE* fp);
//从标准输入读数据
int getchar(void);
//一个简单的例子
while((ch=fgetc(fp))!=EOF)
...
可以通过与EOF作比较来判断有无读到文件末尾。需要注意,不管读文件是出错还是读到文件尾,都会返回EOF,因此需要用下面两个函数作比较
//判断是error还是eof,返回0则是false,非0是true
int ferror(FILE* file);
int feof(FILE* file);
输出函数与输入函数对应
int putc(int c,FILE* fp);
int fputc(int c,FILE* fp);
按行
读取函数fgets gets
读到换行符为止,读取到的缓冲区以null结尾。
//指定一次读取一行的n-1个字符,返回buf一样的值,或者空指针(出错或者读到文件末尾)
//如果一行的个数超过了n-1,那么会先保存到buf中,接下来继续读取这一行的字符直到\n
char* fgets(char* buf,int n,FILE* fp);
//不指定缓冲区大小的读函数,由于读取到的数据最终可能会超过buf大小从而造成缓冲区溢出,因此最好不用
char* gets(char *buf);
//一个例子
while(fgets(buf,SIZE,fp)!=NULL)
...
//fgets需要事先设置buf大小,而getline可以获取任意大小的行,并且通过malloc来自行分配空间
uint16_t getline(char** buf,size_t *n,FILE* stream);//返回读取字符/字节个数
//如下使用
char* buf;
size_t n=0;
while(getline(&buf,&n,stream)>0);
\n的处理 fgets读到\n是会将其加入缓冲区,gets不会。
写一行函数
注意,写入的字符串最后一个字符不需要非得是\n,不需要非得一行行写入
返回写入个数,如果返回EOF,则表示写入有问题
int fputs(char* str,FILE* fp);
//打印到标准输出
int puts(char* str);
fgetc与fgets在判断文件尾时是有差异的,前者用EOF,后者用NULL
使用fgetc/fputc以及fgets/fputs也是会改变文件流,需要使用fseek等函数进行重置
二进制读写
以任意长度来读写文件,两者返回读写单位数。这两个函数是按照某一结构来读写二进制文件,并且假设所有数据都以同一格式存在二进制文件中
//两者参数含义相同,如下所示:size要读写的单个元素的大小,nobj读写元素个数
//如果读到的数据!=nobj,要么读到文件末尾,要么出错
size_t fread(void* ptr,size_t size,size_t nobj,File* fp);
//对于fwrite,如果返回的值小于nobj,那么出错
size_t fwrite(void* ptr,size_t size,size_t nobj,File* fp);
使用这两个函数,读写的数据文件需要在同一系统下,因为异构系统可能会产生问题?
流式读写的例子:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main() {
//C语言的字符串其最后一个字符是/0
char str[]="hello\n";
printf("len:%ld size:%ld\n",strlen(str),sizeof(str));
FILE* fp=fopen("./txt","a+");
for(int i=0;i<sizeof(str);i++)
fwrite(str+i,sizeof(char),1,fp);
fseek(fp,0,SEEK_SET);
char ch;
while(fread(&ch,sizeof(char),1,fp)>0)
printf("%c",ch);
fclose(fp);
return 0;
}
如果要读写数组的话(一次读写多个结构,那么ptr给出起始指针,然后用nobj来指定读写个数即可)
性能比较
虽然流在使用上,用户时间要比描述符慢很多,但是由于流式读写的缓冲区是os已经帮我们设置好的,所以用起来其实反而会方便不少。
定位流
IO流在读写文件时也是会发生变化的。常用的
long ftell(FILE* tp);//返回从起点开始的文件偏移量
void rewind(FILE* tp);//重置流,将文件流定位到起始位置
int fseek(FILE* tp,long offset,int whence);//设置方式与返回值和lseek相同
ISO C的两个函数fgetpos与fsetpos可以见apue。
格式化I/O
输出
输出到字符串,文件流,文件描述符
printf(char*,);
fprintf(FILE* fp,char*,...);//输出到文件流
//fprintf(fp,"%s - %d","We",2020);
//类似的格式化输出函数还有sprintf将数据格式化输出到一个字符串中
//eg. sprintf(str,"%d %d %d",0,1,2); str必须是已经分配了空间的 其与snprintf的区别在于后者会设置写入字符串的字节个数
dprintf(int fp,);//输出到文件描述符
输入
返回输入项数,如果出错或者到达文件尾,那么返回EOF
scanf(char*,...);//从标准输入读入
fscanf(FILE* fp,char*,...);//除了从标准IO流输入外,其余和scanf一样
//fscanf(fp,"%s","%d",str1,val1);
内存流
默认使用内存作为缓冲区,可以使用setbuf,setvbuf设置自己的缓冲区。内存流只能读写主存,不访问其他文件。
//type与fopen的type是一样的,buf是缓冲区,size是缓冲区大小,返回指向内存区域的指针
FILE* fmemopen(void* buf,size_t size,const char* type);
对内存流使用fflush可以将数据全部冲进缓冲区
一个使用内存流的例子
char buf[SIZE];
FILE* fp=fmemopen(buf,SIZE,"a+");
fprintf(fp,"hello world");
fflush(fp);
printf("%s\n",buf);//可以打印得到hello world
字符串处理
头文件string.h
#include<string.h>
char* strstr(char* str1,char* str2);//在str1中查找有无str2,如果有,那么返回首地址,否则返回NULL
char* strcpy(char* str1,char* str2);//str2复制到str1,必须有足够空间
char* strncpy(char*str1,char* str2,size_t bytes);//复制bytes字节从str2给到str1
//连接操作尤其需要注意str1必须有足够的空间,可以容纳str2
char* strcat(char* str1,char* str2);//将str2连接到str1
#include<stdlib.h>
int atoi(char*);//字符串转数字
char* itoa(int);//数字转字符串
进程环境
stdlib.h unistd.h
每个C程序都存在一张环境表,存储各环境变量
进程退出
一个C程序的终止处理方式(正常情况下)
int exit(int state);//这个函数会调用fflush,并对每个流调用fclose,在进行缓冲区刷新前,会先调用一系列终止处理程序,可以通过向atexit传递函数指针来设置FILO运行顺序的终止处理程序
_exit(status);
_Exit(status);
return 0;
还有两种与多线程相关
- 在进程中的最后一个线程返回0
- 在进程中的最后一个线程返回pthread_exit
子父进程退出关系
每个子进程在终结后,都将设置一个退出状态,并在父进程终止前或者获取终止状态信息前,沦为僵死进程(僵死进程的大部分资源都被释放了,但是pid,cpu时间等一部分信息依然保存着)。由内核将子进程的退出状态转换为终止状态。
如果父进程先于子进程结束,那么内核会将子进程的父进程ID改为1。(由init进程去收养子进程)如果一个init收养的子进程终止了,它不会沦为僵死进程,会直接wait获取终止状态后释放掉。
当父进程调用wait,waitpid等一系列函数获取到僵死进程的终止状态时,会将僵死进程占用的资源彻底释放掉。
C/C++存储空间布局
代码段(正文段)
全局数据段(静态段/常量段)
栈段(自动变量)
堆段(动态变量)
共享库
对于一些公共的库函数,程序只有在用到他们时才会将其加载到内存,所有进程都可以使用存储区中保存该库例程的一个副本,而不需要在编译时一起编译,从而节省了内存空间(不会每个进程都有一个共享库实例在内存中了),同时减少了可执行文件大小。
char* getenv(const char* name);//可以通过类似键值对的方式来取环境变量
//所有设置环境变量相关的操作只能对当前进程及其子进程产生影响,0成功,-1出错
int putenv(char* name,char* value);
int setenv(char* name,char* value,int rewrite);//rewrite如果为0,那么不能修改,否则可
int unsetenv(char* name);
存储空间分配
C++ new delete 运算符
C malloc free系列 启动了系统调用的库函数
分配空间
成功则非空,返回指向分配空间的指针(void*类型),根据具体类型需要强制转换
malloc(size_t size);//分配一块连续空间,并且初始化值是不确定的
calloc(size_t nobj,size_t size);//分配固定元素大小和长度的空间吗,和malloc相比,其空间中的每一位都被初始化为了0
realloc(void* ptr,size_t new_size);//为ptr重新分配空间,大小为new_size,并且原本空间中的内容是会被复制过去的(在堆中内存没法继续顺着增长的时候)
memset(void* data,int c,int n);//为data数据指针开始的n个字节全部设置为c
free(void* ptr);//释放空间
内存泄漏
如果在alloc后忘记了free,那么持续增长的进程mem会占满进程空间,使得进程页抖动,降低性能。每个进程都有一个页表
进程控制
相关头文件
unistd.h
sys/wait.h
sys/types.h
进程标识
每个进程唯一的标识符,根据延迟复用算法去分配。
系统相关进程(以linux系统为例)
- 0号进程用于进程调度算法,跑在kernel下
- 1号进程用于启动内核系统,Init 跑在user下,拥有超用户权限。
- 2号线程用于管理系统的内核级线程 顾名思义,运行在内核模式下的线程是内核级线程。
获得进程标识的相关函数
int getpid();//当前进程pid
int getppid();//得到父进程的pid
进程创建
fork
可以创建一个子进程,子进程会跟着fork后的内容执行。子进程返回值为0,父进程返回值为子进程id。
pid_t fork();
子父进程并不共享存储空间!!!使用这种方式,子进程会获得父进程虚拟内存的副本(写时复制),包括了父进程缓冲区中的内容,因此当做标准IO的时候(标准IO是带缓冲的),由于缓冲区被复制了,最终IO的结果可能会有多份!!!
文件共享问题
当一个子进程被创建,它会得到父进程文件描述符项,这些子进程中的文件描述符项会指向和父进程相同的内核打开文件表项,从而会有相同的文件偏移量。
在共享文件描述符下的常用操作情况
- 父进程等待子进程操作文件完后再执行 (wait)
- 父进程和子进程执行不同的程序段
fork的常用两种方法
- 父进程复制自己执行不同的代码段
- 执行不同的代码段 fork后立马调用exec
进程等待
sys/wait
wait与watpid
在父进程调用这两个函数的功能
- 如果所有子进程在运行,则阻塞父进程
- 如果一个子进程终止,那么父进程取回其终止状态,并且立刻返回。
- 没有子进程就出错。
如果有多个子进程,那么在其中某一个终止,进入僵死状态后,wait函数就会立刻获取其终止状态信息,继续运行父进程。
函数原型
#include<sys/wait.h>
pid_t wait(int *status);//status表示进程结束的终止状态信息,可以指定一个status用来存储,并通过传递给位于sys/wait.h的四个宏
pid_t waitpid(pid_t id,int *status,int option);//option的话,就是不阻塞
都返回进程id。
两者区别
- waitpid 可以指定阻塞的进程 wait阻塞任意进程
- waitpid可以只获取进程信息而不阻塞 option=WNOHANG
- waitpid可以支持作业控制 ???
调用两次fork可以避免父进程强行等待子进程的同时,子进程沦为僵死进程。通过让子进程的子进程执行子进程代码。然后直接exit子进程,这样第二子进程的父进程就是init进程,当他运行完后就直接被init回收了,不会沦为原父进程的僵死进程。
直接跑父进程,子进程的话,如果子进程比父进程先运行完,会沦为僵死进程,使用双子进程方法能避免上述情况。
waitid
和上述两个wait相比,waitid可以选择要等待的进程 or 进程组,并进行控制 。详见apue
wait3 & wait4
这两个函数和wait相比,可以指定rusage*来获取进程的资源使用情况信息。原型如下:
pit_t wait3(int* status,struct rusage* rusage);
pit_t wait4(pid_t pid,int* status,int option,struct rusage* rusage);
竞争条件
如果某一处代码依赖于进程运行的先后顺序,那么就会产生竞争条件。有一个技巧来避免这个问题
轮询法
让进程等待其父进程运行结束,使init回收它后,再执行后续代码。
while(getppid()!=1)
sleep(1);
为了控制进程之间的运行,采用进程间通信,阻塞一个进程,让其等待另外一个进程的运行结果(因为需要用到,比如g-net中的nf_start就必须先等待nflib中先加载gpu_rule),等待另外一个进程执行完某些代码后,再通知原本的进程运行。
dpdk中,通过共享内存以及rte_ring传递的描述符,可以来进行进程间的通信。
linux中,可以使用
- 信号
- 共享内存
- 管道
- 套接字编程
exec
使用exec后,会由新的进程代替原本的进程,并且从main开始执行,exec后进程的id是不改变的。
exec后进程会从父进程继承如下常用的内容:
- 进程ID和父进程ID
- 进程组ID
- 会话ID
- 未处理信号
- 当前工作目录/根目录
- 进程信号屏蔽
对于打开文件描述符来说,除非使用了fcntl函数设置了close-on-exec标识(FD_CLOEXEC),否则exec后的进程会继承原本进程的打开文件描述符。(依旧开着)
exec 7函数原型
所有传递给新替换进程的参数,必须都以**(char*)0**指针结尾
//pathname 文件路径,filename 文件名称,char* args,...可变参数列表
int execl(char* pathname,const char* args,...);
int execlp(char* filename,const char* args,...);
int execle(char* pathname,const char* args,...);//..,(char*)0,char* envp[]
int execv(char* pathname,const char* args[]);
int execvp(char* filename,const char* args[]);
int execve(char* pathname,const char* args[],char* envp[]);
各参数区别的说明
-
l 后缀表示使用可变参数列表 传递参数给新替换的进程需要用(char*)0结尾 与v参数互斥
-
v 后缀表示使用参数数组,原本传可变参数列表,现在传一个参数数组
-
p 后缀表示使用filename而不是使用pathname,会在path指定的路径下寻找可执行文件,如果没有可执行文件,就会被解释成shell。也就是说,可以直接使用shell命令
-
e 后缀表示使用数组,设置新的环境变量 以下是一个例子
char* envp[]={"user=root","cuad=/usr/local/cuda-11.0"};
所有的组合 l , lp , le , v , vp , ve
进程调度
unix系统上,本质使用基于调度优先级的粗粒度的控制。可以使用nice函数,来降低或者获取进程的优先级。nice值越低,优先度越高 (-1,20)
int nice(int incr);//用来修改调用进程
int getprority(int which,id_t who);//在指定which下,设置它的优先级值
int setprority(int which,id_t,int value);//用来设置进程优先级值
上述的三个函数返回nice值。其中的参数incr表示当前nice值的改变量。
进程时间
墙上时钟时间:表示进程开始运行到结束的时间,包括了阻塞时间
用户CPU时间:表示进程运行在用户态CPU经过的时间。
系统CPU时间:表示进程运行在内核态经过的时间。
tms计时
可用于多线程计时
#include<sys/times.h>
//记录时间的结构体,u前缀表示cpu时间,s表示系统时间,c表示子进程表示
//这个结构可以记录下不同线程阻塞后的时间
struct tms{
clock_t tms_utime;
clock_t tms_stime;
clock_t tms_cutime;
clock_t tms_cstime;
}
clock_t times(struct tms* buf);//参数为cpu,系统时间,返回值是墙上时钟时间
//通过sysconf(_SC_CLK_TCK)来将上述时间转换成秒
一个例子
struct tms start,end;
long start=times(&start);
//do something
long end=times(&end);
//read time is
//_SC_CLK_TCK头文件是unistd.h
double readTime=(end-start)/sysconf(_SC_CLK_TCK);
进程调度和进程时间结合的demo,有个问题,设置parent(nice = 20),此时parent的优先级应该低于chiid(nice = 0),但是还是parent的计数次数多,这是为什么。
#include<sys/wait.h>
#include<sys/times.h>
#include<stdio.h>
#include<unistd.h>
#include<stdint.h>
#include<stdlib.h>
#include<string.h>
int main(int argc,char* argv[])
{
int incr = 0;
int count = 0;
if(argc == 2)
incr = atoi(argv[1]);
else
incr = 0;
clock_t start=0;
clock_t end=0;
struct tms start_t;
struct tms end_t;
int ret=fork();
if(ret==0)
{
printf("child pid %d priority %d\n",getpid(),nice(0));
start = times(&start_t);
do{
count++;
end = times(&end_t);
}while(((end-start)/sysconf(_SC_CLK_TCK))<5);
printf("child cost time %.4f count is %d\n",((double)end-start)/sysconf(_SC_CLK_TCK),count);
exit(0);
}
else
{
printf("parent pid %d priority %d\n",getpid(),nice(incr));
start = times(&start_t);
do{
count++;
end = times(&end_t);
}while(((end-start)/sysconf(_SC_CLK_TCK))<5);
printf("parent cost time %.4f count is %d\n",((double)end-start)/sysconf(_SC_CLK_TCK),count);
}
//wait(NULL);
return 0;
}
timespec计时
可用多线程计时
#include<time.h>
//用于计时的结构,前者是秒,后者是纳秒
struct timespec ts;
ts.tv_sec;
ts.tv_nsec;
//CLOCK_REALTIME获取的是从系统启动开始的时间
clock_gettime(CLOCK_REALTIME,&ts);
struct timespec start;
struct timespec end;
clock_gettime(CLOCK_REALTIME,&start);
clock_gettime(CLOCK_REALTIME,&end);
cout<<"times(s)"<<start.tv_sec - end.tv_sec<<endl;
进程亲和性
相关头文件
#define __USE_GNU
#include <sched.h>
#include <ctype.h>
#incldue <string.h>
#include <pthread.h>
进程绑定在一个或者几个cpu上运行,从而提高性能(减少在不同cpu上运行的上下文切换能力。)通过设置cpu_set_t中的cpu掩码信息,来为其分配指定运行的cpu。
相关数据结构和函数
struct cpu_set_t;
//为指定线程设置cpu亲和性,如果返回值为-1,那么bug
sched_setaffinity(pid_t,sizeof(cpu_set_t),cpu_set_t*);
//获取线程设置cpu亲和性,如果返回值为-1,那么bug
sched_getaffinity(pid_t,sizeof(cpu_set_t),cpu_set_t*);
信号机制
基础
头文件 #include <signal.h>
一种软件中断,以异步的方式告知程序事件的发生,程序一般有三种处理信号的方式
- 忽视 SIGKILL SIGSTOP是不能被忽略
- 信号处理函数 信号处理函数会由内核插入到用户态调用栈上,然后切换回用户态后执行,最后返回现场。信号处理函数是运行在用户态上的
- 系统默认处理 不用信号处理的函数就执行系统默认操作,对于大部分信号来说就是ignore
简而言之:发信号让处理器停下当前工作,去执行信号处理
信号状态
- 信号抵达 程序接受到信号,并执行了处理。信号忽略也是信号抵达后程序选择了忽略
- 信号未决 介于信号产生和信号抵达之间的状态
- 信号阻塞 被阻塞的信号会保持在未决状态,直到进程取消对信号的阻塞
常见信号产生方式
- 终端组合键产生中断信号 ctrl+C
- 硬件异常
- 某些软件条件发生 (比如SIGURG,SIGPIPE)
常用的信号 (p251)
- SIGINT
- SIGSTOP
- SIGKILL
- SIGSEGV
- SIGALRM 计时器触发
signal函数
将信号绑定到指定的信号处理函数上,返回信号处理函数指针,unix系统提供了三种默认的信号处理函数指针
- SIG_IGN
- SIG_DFL
- SIG_ERR signal绑定信号处理函数的错误判断,可以用这个
void (*signal(int sign,void (*func)(int)));
if(signal(SIGALRM,handle)==SIG_ERR)
return -1;
每个进程都保留了一个信号屏蔽字,用来表示信号哪些部分是要忽略的。
函数名就指代了函数的首地址。
多进程对信号的影响
- 使用exec替换当前进程后,所有信号的处理将被重置为系统默认的方式
- 使用fork创建的子进程会继承父进程的信号处理方式
一个简单的信号处理例子
#include<signal.h>
#include<sys/times.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
static int flag = 0;
static void handler(int signal)
{
printf("signal:%d\n",signal);
flag = 1;
}
int main(void)
{
signal(SIGINT,handler);
struct tms start;
struct tms end;
long t_start = 0;
long t_end = 0;
t_start = times(&start);
while(flag == 0);
t_end = times(&end);
printf("signal handle time is %.5f",((double)t_end-(double)t_start)/sysconf(_SC_CLK_TCK));
exit(0);
}
中断系统调用
对于低速系统调用来说,当其被信号中断后,将终止并且返回错误码,进程继续获取资源并且运行。使用goto语句的返回,来重新调用被中断的系统调用
unix下提供了一些可以自重启的系统调用。详见(p261)
可重入函数
如果在函数执行过程中接受到了信号,转而调用信号处理函数,可能会产生错误。(比如调malloc,printf)。linux提供了一系列信号安全的函数,确保函数在执行时有信号也不会出问题。这类函数被称为可重入的,多个线程/任务调用同一函数,并不会相互影响就被称之为是可重入的。
比如malloc就是不可重入的,在调用过程中,如果产生了中断,并再中断处理程序中又调用了malloc(相当于异步调用了malloc),那么会对原本malloc的结果产生影响。
errno变量
每个进程都有一个唯一的errno变量,#inclulde<erron.h> 中定义的,可以查看进程运行是否出错?
常见errno变量的定义
22 //参数错误
111 //连接拒绝
SIGCLD语义
表示子进程状态改变,比如子进程运行结束后,状态改变,就会触发这个信号(绑定这个信号后,内核会立刻检查是否有子进程准备好被等待)。需要注意,用signal二次绑定SLGCLD信号时,需要先通过wait获取终止的子状态进程,否则会递归爆栈。
信号传递函数
int kill(pid_t pid,int signo);//可以向指定进程发送信号,不一定就发送kill信号
int raise(int signo);//向进程自身发送信号
int alarm(int seconds);//为进程设置闹钟时间,到时间后触发SIGALRM信号,默认处理方式是终止调用alarm的进程
int pause(void);//挂起进程,直到一个信号处理程序返回
一个简单的sleep函数的例子
signal(SIGALRM,handle);
alarm(seconds);
pause();//pause会在seconds接收到来自handle的信号处理程序的返回,并最终继续运行
信号集合
用一种数据结构来表示允许进程响应的信号,通过结构中的每一位来表示一个信号是否可以被响应(是否被阻塞),1表示可以被响应。(实现类似于g-net中的hint)
//操作与控制的是信号的集合
struct sigset_t*
//将信号集清空
int sigemptyset(sigset_t* set);
//使信号集包含所有信号
int sigfillset(sigset_t* set);
int sigaddset(sigset_t* set,int sig);
int sigdelset(sigset_t* set,int sig);
int sigismember(sigset_t* set,int sig);
复杂信号处理
基于 sigaction结构体和函数去实现,高级版的signal
//action中包含了sa_handler(int)|sa_sigaction(int,siginfo_t*,void*) 前者只能收到信号,后者可以获取信号附带的参数
//sa_flags 如果用sa_handler取0 用sa_sigaction取SA_SIGINFO
//sa_mask 信号屏蔽量
struct sigaction action;
//通过sigaction函数进行信号处理绑定 这与signal有啥区别
int sigation(int sig,struct sigaction* act,struct sigaction* old_act);
一共有两种写法,如下:
//只接收信号,不接受信号附带参数
struct sigaction action;
sigemptyset(&action.sa_mask);
action.sa_handler = func;//void func(int) (__sighandler_t)SIG_IGN
action.sa_flags = 0;
sigaction(SIGHUP,&action,NULL);
//接收信号附带参数
struct sigaction action;
//将mask
sigemptyset(&action.sa_mask);
action.sa_handler = func;//void func(int,struct siginfo*,void*)
action.sa_flags = SA_SIGINFO;
sigaction(SIGHUP,&action,NULL);
一个简单的计时器例子
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int tag = 0;
void handler(int status){
tag = 1;
}
int main(void)
{
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_handler = handler;
sa.sa_flags = 0;
sigaction(SIGALARM,&sa,NULL);
alarm(5);
while(tag == 0);
printf("Finish\n");
return 0;
}
线程
使用由POSIX提供的线程需要使用-lpthread来作为库使用
线程号
线程号由pthread_t这一结构体组成(创建线程也用这个),相关结构如下
int pthread_equal(pthread_t p1,pthread_t p2);//如果相等,则返回非0,否则返回0
pthread_t pthread_self(void);//返回自身线程号
线程创建与阻塞
pthread_create & pthread_join
#include<pthread.h>
//创建线程,当创建一个线程后,这个线程就会执行了,可以传递任意函数给线程,因为这里的函数指针参数返回值和形参都是void* 创建成果就返回0 注意不是返回线程号
int pthread_create(pthread_t* thread,const pthread_attr_t* attr,
void* (*func)(void*),
void* args);//如果传入的参数有一个以上,那么需要把这些参数搞成一个结构体,传入结构体地址
//使用上述方法建立的子线程,如果晚于主线程结束,那么会在主线程结束时强行回收。需要使用join来进行阻塞 这个函数还会返回对应线程函数的返回值
pthread_join(pthread_t p,void** result);
一个线程求和的例子,该例子中需要说明的内容如下:
- pthread_join中想要得到返回值,必须传入一个指向指针的指针,因此必须传void*的指针给它,具体用什么数据再转换
- 线程当中的返回值一定不能是线程局部变量,因为当线程指向完毕后,这些变量的存储空间就都被释放掉了。如果线程中有数据需要返回的,就用malloc/new去开辟在堆上的空间。
代码如下
#include<pthread.h>
#include<stdio.h>
#include<stdint.h>
#include<string.h>
#include<stdlib.h>
void* Add(void* args)
{
uint32_t number = *((uint32_t*)args);
//必须用malloc能获取返回值
uint32_t* sum = malloc(sizeof(uint32_t));
memset(sum,0,sizeof(uint32_t));
for(uint32_t i = 0 ; i<number ; i++)
*sum += i;
printf("add number:%d\n",*sum);
pthread_exit((void*)sum);
}
int main(int argc,char* argv[])
{
pthread_t p;
int number = 10;
if(argc > 1)
number = atoi(argv[1]);
printf("%d\n",number);
printf("res: %d\n",pthread_create(&p,NULL,Add,&number));
void* result;
//传递的参数必须是指向指针的指针
pthread_join(p,(void**)&result);
printf("pthread finish result is %d\n",*(uint32_t*)result);
return 0;
}
注意,通过pthread_create获得的线程id pthread_t是不能安全使用的,可能会出现问题。
线程终止
线程如果直接运行exit系列函数(exit,_Exit)会直接导致进程终止,因此无法直接使用这个。需要如下三种形式安全退出线程:
- 直接return
- 由其他线程来终止
- 调用pthread_exit
//可以用来返回线程中的值,一定注意rval指向的地址必须在线程栈销毁后依旧有效
void pthread_exit(void* rval)
//见上面内容
int pthread_join(pthread_t pthread_id,void** rval);
//可以向其他线程发出终止请求,注意,这里只是发出终止请求,并不是说请求的线程就立马结束运行了
int pthread_cancel(pthread_t pid);
与进程类似,可以通过pthread_cleaup_push & pthread_cleanup_pop的方式来添加线程终止处理函数,通过FILO的方式来调用依次添加的函数,触发条件有
- 调用pthread_exit
- 调用参数不为0 的pthread_cleanup_pop
- 响应其他线程的pthread_cancel
如果单纯return是无法启动这些终止处理函数的,具体使用接口如下
//这两个函数要成对使用
void pthread_cleanup_push(void (*rtn)(void*),void* args);
void pthread_cleanup_pop(int code);
分离线程
所谓分离线程是指线程终止后,立即释放资源的线程。而不像一般线程要等到pthread_join后再释放线程。分离线程如果用pthread_join重复释放资源会出现问题。
int pthread_detach(pthread_t pid)
线程同步问题
多个线程同时读写或者同时写一个变量就会出现问题。A,B线程同时写一个数据,可能在线程A还没写回时,B就开始写数据,导致两者实际上只修改了一次数据。
互斥量
一种sleep-wait的锁,可以确保数据的同步访问,当线程A访问被锁了的资源后,操作系统会进行上下文切换,把其置入就绪态,去调用其他线程。(而自旋锁就不会进行上下文切换,线程A会自旋等待直到获得锁资源)
#include<pthread.h>
//动态互斥量生成后,需要使用pthread_mutex_destroy来销毁,再调用free清空内存
//用init动态初始化的锁,必须确保是malloc过的
int pthread_mutex_init(pthread_mutex_t* mutex,pthread_mutexattr_t* attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
//静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
下述三种加锁解锁方式
int pthread_mutex_lock(pthread_t* mutex);
int pthread_mutex_unlock(pthread_t* mutex);
//try_lock和上述的区别在于,如果没法获取锁,那么不会阻塞线程运行
int pthread_mutex_trylock(pthread_t* mutex);
以下是一个极简例子
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
//对于加锁结构来说,往往可以声明在结构体内部
typedef struct object_s
{
uint32_t counter;
pthread_mutex_t* mutex;
}object_t;
object_t object;
static void init(){
object.counter = 0;
object.mutex = malloc(sizeof(pthread_mutex_t));
printf("Start init\n");
pthread_mutex_init(object.mutex,NULL);
}
void* worker(void* args)
{
for(uint32_t i = 0;i<1000000;i++)
{
pthread_mutex_lock(object.mutex);
object.counter++;
pthread_mutex_unlock(object.mutex);
}
pthread_exit(&object.counter);
}
int main()
{
init();
pthread_t p1;
void* rtn1;
pthread_t p2;
void* rtn2;
pthread_create(&p1,NULL,worker,NULL);
pthread_create(&p2,NULL,worker,NULL);
pthread_join(p1,&rtn1);
pthread_join(p2,&rtn2);
printf("thread1 return data:%d\n",*(uint32_t*)rtn1);
printf("thread2 return data:%d\n",*(uint32_t*)rtn2);
printf("thread return data:%d\n",object.counter);
pthread_mutex_destroy(object.mutex);
free(object.mutex);
exit(0);
}
死锁问题
多互斥量多线程持有并且循环等待会造成死锁。有两种解决方法
- 控制加锁顺序全序 也就是对锁线程的加锁顺序保持一致
- 可以试着先释放掉已经占有的锁,过一段时间再获取 往往用pthread_mutex_trylock
线程安全链表的双锁例子
对整个链表加锁是粗粒度锁,而对链表中的元素访问加锁则是细粒度 粗细粒度如何设计?
读写锁
#include<pthread.h>
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;使用读写锁竟然会出错???
//动态分配读写锁的方式与之前mutex是一致的
pthread_rwlock_init(pthread_rwlock_t* rwlock,pthread_rwlockattr_t* attr);
pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
//不管是读锁还是写锁,都可以使用pthread_rwlock_unlock解锁
pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
//尝试加锁,不成功继续
pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
只有当读取操作的频次远大于修改操作时,读写锁才能起效。
条件变量
条件变量配合mutex可以用一种无竞争的方法来等待/阻塞线程。之所以一定要用条件变量,是为了避免多线程访问临界区时候,一个线程为了访问临界资源等待条件加锁了,而另一个修改条件的线程始终无法获取这个加锁了的条件,从而造成死锁的情况
使用了条件变量后,当临界区被锁住的资源不符合条件后,锁会被释放(但是阻塞还是阻塞着的),由生产者获取锁后,修改条件变量,使得消费者满足条件后,获取锁再进一步执行。
#include<pthread.h>
//初始化与消除方式同mutex rwlock
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t* attr);
pthread_cond_destroy(pthread_cond_t *cond);
//这个函数在条件未被激活的情况下会先释放mutex,让mutex被其他线程激活,激活后,mutex再被加锁,这个函数也有timewait版本
pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
//激活条件变量对象
pthread_cond_signal(pthread_cond_t* cond);
pthread_cond_broadcast(pthread_cond_t* cond);
一个使用条件变量+mutex的多生产者,单消费者的stack例子
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#include<stdint.h>
#include<stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//这个玩意的逻辑本质上是个栈
typedef struct node_s{
struct node_s *next;
uint32_t num;
} node_t;
node_t* header = NULL;
void* consumer(void* args)
{
node_t *node;
while(1)
{
pthread_mutex_lock(&mutex);
while(header == NULL)
pthread_cond_wait(&cond,&mutex);
printf("thread %d\n",header->num);
node = header;
header = header->next;
pthread_mutex_unlock(&mutex);
free(node);
}
exit(0);
}
void* processer(void* args)
{
uint32_t threadId = *((uint32_t*)args);
node_t *node = NULL;
while(1)
{
node = malloc(sizeof(node_t));
pthread_mutex_lock(&mutex);
node->next = header;
node->num = threadId;
header = node;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);
sleep(rand()%2);
}
exit(0);
}
int main(int argc,char* argv[])
{
pthread_t c;
pthread_t p[2];
uint32_t c_tid = 0;
uint32_t p_tid[2] = {1,2};
pthread_create(&c,NULL,consumer,&c_tid);
pthread_create(&p[0],NULL,processer,&p_tid[0]);
pthread_create(&p[1],NULL,processer,&p_tid[1]);
pthread_join(c,NULL);
pthread_join(p[0],NULL);
pthread_join(p[1],NULL);
return 0;
}
条件变量的使用方法
- 在pthread_cond_wait前先对互斥量加锁
- 在某一条件下调用pthread_cond_wait(这个函数会释放锁并阻塞)
- 在另一线程获取互斥量资源的情况下,修改等待变量,并且pthread_cond_signal激活条件,继续运行
- 注意每个线程处理完资源后都需要pthread_mutex_unlock
自旋锁
不会引起上下文切换。适合用于持有锁时间短的情况。不常用,详见apue p336
屏障
协调多个工作线程并行的同步机制。eg.pthread_join(pthread_t tid,void** args);
设置屏障后,可以使得主线程同时等待多个线程运行完毕后,再继续工作,接口如下
#include<pthread.h>
//如果要动态分配的话,也是malloc init work destroy free的过程
pthread_barrier_t barrier;
//restrict关键字,指定完了后,只能通过指针方式去访问这个指针
//count表示等待的线程数
int pthread_barrier_init(pthread_barrier_t* barrier,pthread_barrierattr_t* attr,
unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t* barrier);
//对每一个处理线程,调用屏障等待,当线程完成调用后,屏障内置计数 + 1,达到屏障阻塞数后继续运行
int pthread_barrier_wait(pthread_barrier_t* barrier);
大数据排序的例子
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<time.h>
#include<sys/times.h>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
#define THREADS 8
#define NUM 80000000
vector<int> array;
struct tms start_t;
struct tms end_t;
pthread_t tid;
pthread_barrier_t* barrier;
//技巧,可以直接用void*作为long来传递数据
static void* sort(void* args)
{
long offset = (long)args;
sort(array.begin() + offset , array.begin() + offset + NUM/THREADS);
//每个执行线程调用一遍wait就相当于屏障的内置计数器 + 1,当屏障的内部计时器满时,则取消阻塞
pthread_barrier_wait(barrier);
return nullptr;
}
int main(void)
{
barrier = (pthread_barrier_t*)malloc(sizeof(pthread_barrier_t));
//之所以THREADS+1是因为还要考虑主线程
pthread_barrier_init(barrier,NULL,THREADS + 1);
for(uint32_t i = 0;i < NUM;i++)
array.push_back(rand() % NUM);
long start_time = times(&start_t);
for(int idx = 0;idx < THREADS;idx++)
{
pthread_create(&tid,NULL,sort,(void*)(idx * (NUM/THREADS)));
}
pthread_barrier_wait(barrier);
//这里合并后算法用冒泡会比自带的快排块很多,因为局部有序
sort(array.begin(),array.end());
long end_time = times(&end_t);
cout<<"time:"<<(end_time - start_time)/sysconf(_SC_CLK_TCK) <<"(s)"<<endl;
pthread_barrier_destroy(barrier);
free(barrier);
return 0;
}
线程属性
标准线程属性
pthread_attr_t attr;//表示线程属性
pthread_attr_init(pthread_attr_t* attr);//初始化线程属性
//设置具体的线程属性
pthread_attr_destroy(pthread_attr_t* attr);//清空线程
线程信息
- detach joinable
- 线程栈地址
互斥量属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(pthread_mutexattr_t* attr);//初始化线程属性
pthread_mutexattr_destroy(pthread_mutexattr_t* attr);//清空线程
-
进程共享 该互斥量能否被用于进程间数据同步
//PTHREAD_PROCESS_SHARED | PTHREAD_PROCESS_PRIVATE int pthread_mutexattr_getshared(pthread_mutexattr_t* attr,int *pshared); int pthread_mutexattr_setshared(pthread_mutexattr_t* attr,int pshared);
-
健壮属性 指的是这个线程在多进程使用条件下,在线程结束后,是否会采取其他动作,默认情况下是未定义行为,在设置了PTHREAD_MUTEX_ROBUST下,则会告知其他试图lock的线程,这个互斥量需要进行恢复,可以通过pthread_mutex_consistent进行恢复
//PTHREAD_MUTEX_ROBUST | PTHREAD_MUTEX_STALLED int pthread_mutexattr_getrobust(pthread_mutexattr_t* attr,int *probust); int pthread_mutexattr_setrobust(pthread_mutexattr_t* attr,int probust); int pthread_mutex_consistent(pthread_mutex_t* mutex);
-
类型 是否可以递归加锁 是否进行错误检测等等
//PTHREAD_MUTEX_RECURSIVE | PTHREAD_MUTEX_ERRORCHECK int pthread_mutexattr_gettype(pthread_mutexattr_t* attr,int* type); int pthread_mutexattr_settype(pthread_mutexattr_t* attr,int type);
递归加锁:
所谓递归加锁,是指在一个互斥量解锁前,可以多次加锁,直到解锁相应的加锁次数后,才能再释放这个锁资源。但是只有第一个对其加锁的线程可以加多次锁
递归锁使用情形
- 对条件进行互斥量加锁后,后续同一线程还需要进行条件操作,又需要一轮加锁 apue上面的那个例子是不用递归互斥锁会死锁
读写锁属性
读写锁只支持进程共享属性,同上述
条件变量属性
- 进程共享
- 时钟类型
屏障属性
屏障也只有进程共享属性
自旋锁属性
自旋锁也只有进程共享属性
上述的五个线程控制属性其接口都类似
可重入
表示函数可以安全地被多个线程调用
如果函数可以安全地被异步信号处理程序调用,那么称函数是异步线程安全的
文件加锁
可以通过线程安全的方式管理FILE(标准流式IO文件)
#include<stdio.h>
int ftrylockfile(FILE* tp);
void flockfile(FILE* tp);
void funlockfile(FILE* tp);
可重入不代表异步信号安全,因为会中断当前线程运行,从而无法释放锁,造成问题。可以通过递归锁解决
线程独立变量
多线程可以使用同一个key,来获取独立存储在线程栈上的数据。多个线程,同一key,独立数据
pthread_key_t
pthread_key_t key;
//这个析构函数的参数是key指向的value地址
pthread_key_create(pthread_key_t* key,void (*destructor)(void*));
//根据key获取值地址
void* pthread_getspecific(pthread_key_t key);
//根据key设置数据的指针
int pthread_setspecific(pthread_key_t key,void* data);
key在多线程使用过程中,只被初始化一次,否则其数据地址会被置为null,在每个线程自己的局部栈中再绑定value
pthread_once_t
一定要声明成全局变量,指定的绑定函数只执行一次。为了避免多线程多次生成key,可以使用
pthread_once_t once = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t* initflag,void (*initfn)(void));
//demo
//once必须为全部变量
pthread_once_t once = PTHREAD_ONCE_INIT;
pthread_key_t key;
void init(void)
{
pthread_key_create(&key,NULL);
···
}
void thread_func(void)
{
pthread_once_init(&once,init);
···
}
可取消属性
指的是线程接受到pthread_cancel(pthread_t pid)后的反应属性,有
- 可取消状态 表示是否可以取消该线程。
- 可取消类型 推迟取消:遇到取消点后才取消 异步取消:可以在任何位置取消线程。
多线程读写操作
这两个函数将文件描述符位置和读写操作绑定到一起,这里的偏移量是从零开始的。
ssize_t pread(int fd,void *buf,size_t bytes,off_t offset);//成功读返回读到数据个数
ssize_t pwrite(int fd,void *buf,size_t bytes,off_t offset);//成功写返回已写字节数
守护进程
运行在后台无命令行与控制终端,守护其他进程运行的进程,称之为守护进程。通过如下命令获取:
ps -ajx
守护进程分为内核守护进程和用户态守护进程。前者的父进程是kthreadd,后者的父进程是init。守护进程一般都运行在超用户权限下。
编程规则
进程组
一组进程的集合。可以通过对组长进程(可以管理进程组的进程,进程组id就是组成进程的pid),发信号,来影响进程组中的所有进程。进程组中的所有进程共享终端信号。
linux会话
本质上,会话是进程组的集合。当一个用户连接上系统后,就建立了一个会话。eg. ssh方式链接会话 会话是基于链接的 建立该会话的进程就是会话首进程,**会话首进程不能是组长进程 **会话中的所有进程会共享一个控制终端。核心接口如下:
#include<unistd.h>
//返回首进程组id 创建新进程
//调用setsid可以有如下三个效果
//1. 成为新会话的首进程
//2. 成为调用进程组的组长进程
//3. 脱离当前控制终端
pid_t setsid(void);
//返回会话首进程进程组id
pid_t getsid(pid_t pid);
以ssh链接为例,给出会话,进程组,进程创建过程:
ssh链接,起一个会话,验证完成后,启动一个shell终端,这个终端会作为该会话的首进程,该进程又会成为其进程组中的首进程。当会话退出时,会向其leader发送一个SIGHUP,leader又把其发送给其下的所有子进程(包括leader进程组中的子进程和该session下其他进程组中的子进程),子进程收到信号后终止。(守护进程除外)
守护进程创建流程
-
umask创建文件模式屏蔽字 umask(0) gertrlimit获取限制资源信息
-
fork后父进程exit 这样可以让子进程不作进程组组长
-
子进程调用setsid启动后续三个流程(会话首进程,进程组长,脱离终端)这一步完成后,当前进程为会话首进程,可能在后续调用open的过程中,由系统又分配了控制终端,因此可以再次开一个子进程作为守护进程(并其关闭父进程),从而使得守护进程不是会话首进程。这样就分配不到终端了
在再次开辟一个子进程之前,先将SIGHUP信号绑定到SIG_IGN,让当前进程组中进程可以忽略HUP信号
-
chdir更改当前目录为工作目录 (也不一定非得当前目录)
-
关闭不需要的文件描述符 因为子进程会继承父进程的文件描述符 因此需要重置
-
根据需要将输入输出都重定向到/dev/null中
一个守护进程的例子 daemonize函数
/*
struct rlimit r1;//系统限制资源大小,比如文件描述符个数 最大进程个数 最大线程个数等等
resource
RLIMIT_STACK 最大进程堆栈
RLIMIT_NOFILE 最大文件描述符
int getrlimit(int resource,struct rlimit* r);
int setrlimit(int resource,struct rlimit* r);
*/
完成这个例子后,由ps ajx查看数据,我们可以看到,守护进程不是新会话和进程组的首进程,从而不会分配得到控制终端,这是由二次fork实现的。
日志系统
使用日志系统syslog来记录错误,一种有三种方法使用syslog:
- 修改内核例程
- 调用syslog函数
- 通过udp协议向端口514发数据
目前ubuntu系统中都是用的时rsyslog,其是syslog的多线程增强版 其配置文件位于
/etc/rsyslog.conf
这个文件分为modules(用来配置日志监听情况,比如监听udp,tcp信息等等),以tcp为例格式如下
module(load="imtcp")
input(type="imtcp" port="514")
Global devices和Rules 这两个有啥用
各类型日志文件输出目录定义于
/etc/rsyslog.d/50-default.conf
默认的日志文件目录在/varr/log下 使用syslog的默认记录位置在/var/log/syslog中(上面那个守护进程的例子,其syslog就记录在这个文件中)
如何把指定的守护进程信息写入到指定的log文件中
系统接口
#include<syslog.h>
//option取
//LOG_CONS能写入日志文件就日志文件,否则console
//LOG_PID 给出日志信息的时候给出进程PID option是可以或级联的
//facility表示是什么性质的程序记录日志
//LOG_DAEMON 守护进程
//
void openlog(char* ident,int option,int facility);
//priority常取
//LOG_ERR LOG_INFO LOG_DEBUG
//format表示需要格式化输出到log的字符串
void syslog(int priority,char* format,...);
void closelog(void);
单实例守护进程
同一时刻只允许守护进程的一个副本运行,往往用于守护进程需要互斥访问硬件资源的情况下(比如访问/var/run下的文件)。使用文件和记录锁解决这个问题。具体步骤如下:
- 守护进程打开一个固定文件名称的文件 *.pid
- 访问前对其加锁
- 访问完成后直接将锁销毁
守护进程规则
- 如果使用文件和记录锁,则文件保存于/var/run中以*.pid结尾
- 如果守护进程要读取配置文件,那么配置文件放在/etc中
- 守护进程往往在系统启动时就会调用,往往在/etc/rc*.d或者/etc/init.d目录中的shell脚本中给出进程调用
进程间通讯
- 异步与同步
- 单向与双向
- 相关与不相干 是否是子进程关系
- 网络间通讯
进程间通讯本身就是在内核态中进行的
基础
linux下所有的IPC方式有 8种
- 信号
- 信号量
- 共享内存
- 消息队列
- 管道
- 命名管道
- 双向通信管道
- 套接字
相关命令
ipcs -a #查看共享内存 消息队列 信号量 三者IPC的使用情况
ipcrm -m|-q|-s #删除一个消息对象
XSI IPC
信号量 共享内存 消息队列三者都是XSI IPC方案。三者都会在内核中维护一个结构,外部进程通过key_t(一个不断增长的循环大整数int)来访问内核结构并相互交互。
两种生成IPC key的方式
-
直接在头文件中定义一个key_t值作为IPC key,但是这样定义下来的值可能是被使用过的
-
通过一个路径名和0-255的项目id,通过调用ftok函数生成一个IPC key,将其作为extern量,供各进程使用,作为key_t变量
#include<sys/ipc.h> key_t ftok(const char* path,int id);
-
直接指定为IPC_PRIVATE 往往用在有亲缘关系的进程当中
信号量
在多进程当中用来作为共享对象访问计数器的,一个要访问临界区资源的进程如果使用信号量,那么要走如下三步。本质就一计数器
- 测试控制资源的信号量
- 信号量为正,那么可以控制资源,信号量-1
- 信号量为0,进程阻塞,等到信号量大于0后返回(1)
当进程完成临界资源的访问后,信号量+1
信号量-1的操作是P操作(加锁),+1的操作是V操作(解锁) 信号量不是pthread_mutex_t
上述内容是信号量理论做法,XSI IPC中的信号量是由信号量集合表示的,具体三函数semget,semctl,semop如下
由于信号量是在内核态中的(semid_ds),因此其对所有的用户都有效,但是只有其拥有者可以删除这个信号量,接口如下:
#include<sys/sem.h>
//创建信号量集合函数,返回信号量ID,出错-1
//nsems表示创建的信号量个数
//flag:
//IPC_CREAT如果信号量不存在,那么创建,否则获取
//IPC_EXECL只有信号量不存在才创建,如果已经存在,那么报错
int smid = int semget(key_t key,int nsems,int flag);
//初始化信号量集合函数,成功0,失败-1
//semnum操作信号在信号集合中的编号,从0开始,如果要指定集合中某一个成员的值,就设置这个参数
//cmd有如下常用取值
/*
1. IPC_STAT 读取信号量集合数据结构
2. IPC_SET 设置semid_ds中的ipc_perm结构,取自于semun中的buf参数
3. IPC_RMID 删除该信号集合,前提是得有操作权才能删除,否则删除不掉的
4. SETALL 对信号集合中所有信号进行设置,使用semun.array来进行设置
5. SETVAL 对semnum指定的成员设置,使用semun.val进行设置
6. GETALL
7. GETVAL
*/
//最后一个可选参数是union semun
union semun{
int val;//用来配合SETVAL使用,用来设置或者获取信号量集合中单个值
struct semid_ds* buf;//用于IPC_SET|IPC_STAT
unsigned short* array;//这个数组实际上,就是用来设置信号量集合中值的数组
}
int semctl(int semid,int semnum,int cmd,...);
//对指定的信号集合进行操作的函数,成功返回0,失败返回-1
//nops表示操作数,一般信号集合中的一个信号量对应一个操作
struct sembuf{
unsigned short sem_num;//第几个信号量
short sem_op;//具体操作
short sem_flg;//IPC_NOWAIT表示对信号操作不满足时,不会阻塞,立刻返回
//SEM_UNDO,表示程序结束后,会重置semop的值
};
/*
具体操作细节如下
1. sem_op = 0 ,此时如果信号量不为0,那么进程会阻塞直到其变为0
2. sem_op > 0 ,相当于v操作,用于释放资源,会被加到现有信号量值当中
3. sem_op < 0 , 相当于p操作,用于获取资源,如果其绝对值大于信号现在的值,那么阻塞(不NOWAIT的情况下),否则从信号量中减去该值的绝对值
*/
int semop(int semid,struct sembuf semoparray[],size_t nops);
一个相同代码的进程通过信号量相互阻塞的例子。
三步走
- semget创建/获取sem结构 (事先得通过ftok或者IPC_PRIVATE获得key)
- semctl初始化/获取sem结构
- semop执行sem集合中每个信号量的操作
下面的例子有个逻辑上的问题,如果一个进程加锁后,另外一个进程刚刚启动又会重置sem的值,使得计数器被重置,从而加锁失效
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<stdint.h>
#define LOCK -1
#define UNLOCK 1
int main(void)
{
//create ipc key
char pwd[100];
//get current work dir
getcwd(pwd,100);
key_t key = ftok(pwd,0);
printf("IPC key is %d\n",key);
//create sem struct
//如果第三个flag设置为了IPC_CREAT,那么在信号量存在的情况下就可以直接获取了
int semid = semget(key,1,IPC_CREAT);
printf("Semid is %d\n",semid);
//init sem struct
//这个array就相当于信号量集合初始值数组
ushort array[1] = {1};
int val = 0;
if(semctl(semid,0,SETALL,array) < 0)
{
char* error = "sem init bug\n";
write(STDERR_FILENO,error,strlen(error));
//exit是在stdlib头文件里面的
exit(0);
}
printf("Trying lock:");
getchar();
//define sem op
struct sembuf args;
args.sem_flg = SEM_UNDO;
args.sem_num = 0;
args.sem_op = LOCK;
if(semop(semid,&args,1) < 0)
{
char* error = "sem lock bug\n";
write(STDERR_FILENO,error,strlen(error));
exit(0);
}
/*critical section*/
printf("%d Get lock--",getpid());
getchar();
args.sem_op = UNLOCK;
if(semop(semid,&args,1) < 0)
{
char* error = "sem unlock bug\n";
write(STDERR_FILENO,error,strlen(error));
exit(0);
}
printf("%d unlocked\n",getpid());
exit(0);
}
ps. 可以把信号量理解为互斥量的原语
XSL信号量的问题
- 创建和初始化不是原子的
- 接口恶心
共享内存
由于进程间通讯本身就是在内核态中进行的,因此进程间多次交换数据会引起大量的上下文切换,从而导致效率变低,因此可以引入shared memory从而提高执行效率。最快的IPC方法
由于进程之间内存共享,因此需要一些同步机制,往往使用信号量,接口如下
#include<sys/shm.h>
#include<sys/ipc.h>
//返回shmid flag取IPC_CREAT IPC_EXCEL
//size表示共享内存大小,必须是页面整数倍
int shmget(key_t key,size_t size,int flag);
//设置函数,成功0,错误-1
//cmd取如下值
/*
1. IPC_STAT 获取shmid_ds结构 这个是shm在内核中的描述结构
2. IPC_RMID 如果最后一个进程也detach这个shm,那么就从内核中删除它,否则实际上不会删除。删除的前提是,当前执行进程的用户是有权限的
3. SHM_LOCK | SHM_UNLOCK linux上提供的API可以对shmd进行加锁,省得取用sem4
*/
int shmctl(int shmid,int cmd,struct shmid_ds* buf);
//映射函数,将共享内存的区域映射到进程本地地址空间,成功返回shmd指针,否则-1
//addr如果为0,那么会返回由系统分配的链接到shmd的地址
//如果addr不为0,并且flag不为SHM_RND,那么将共享内存地址连接到addr位置
//flag可以为SHM_RDONLY表示只读shmd,否则可读可写
//推荐使用addr = 0的方法
void* shmat(int shmid,const void* addr,int flag);
//对于使用完毕了的shmd,需要调用shmdt将shmd与进程地址空间分离
//shmd当中也有一个计数器,用来记录有多少进程使用了这块shmd,调用shmdt后,会使得计时器-1
int shmdt(const void* addr);
shmd使用步骤
- key_t生成/获取,shmget获取/生成shmd结构体
- shmctl设置/加锁shmd
- shmat链接shmd地址位置
- 使用完毕后shmdt分离shm
一个简单的例子,write进程从共享内存中写入hello world,read进程从中拿出来
#include<sys/shm.h>
#include<sys/ipc.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<stdint.h>
#include<string.h>
#define SIZE 2048
int main(int argc,char* argv[])
{
//get key
char pwd[100];
getcwd(pwd,100);
key_t key = ftok(pwd,0);
//get shmid
int shmid = shmget(key,2048,IPC_CREAT);
//lock与unlock只有root用来才能执行
//shmctl(shmid,SHM_LOCK,NULL);
//map shmd
char* data = NULL;
//default flag is 0
//使用的是系统分配的进程地址空间地址与共享内存地址映射
data = shmat(shmid,(void*)0,0);
if(strcmp(argv[1],"write") == 0)
{
printf("write process %d\n",getpid());
memset(data,'\0',SIZE);
strcpy(data,"hello world");
}
else
{
printf("read process %d\n",getpid());
printf("data: %s\n",data);
}
shmdt(data);
exit(0);
}
拓展:dpdk中大量运用了shm 实际上,用户地址空间中堆栈当中的一块空间会被映射到共享内存中。
消息队列
XSI
存储在内核态中的消息链表,可以自定义存储在消息链表中的消息,mq是FIFO顺序的。该结构体的第一个成员必须是表明消息类型的long类型。以下是一个例子
struct mymsg{
long mytype;//可以根据这个值,从而非FIFO地从队列中取数据
char data[512];
};
具体接口如下
#include<sys/msg.h>
#include<sys/ipc.h>
//获取msgid flag取值同上
int msgget(key_t key,int flag);
//cmd取值如下
/*
IP_STAT 获取msgid_ds信息
IP_RMID 删除结构
IP_SET 通过buf结构来设置内核中mq描述符,可以将msg_qbytes也就是消息队列最大大小设置给内核中的队列
*/
struct msqid_ds
{
struct ipc_perm msg_perm; /* structure describing operation permission */
__MSQ_PAD_TIME (msg_stime, 1); /* time of last msgsnd command */
__MSQ_PAD_TIME (msg_rtime, 2); /* time of last msgrcv command */
__MSQ_PAD_TIME (msg_ctime, 3); /* time of last change */
__syscall_ulong_t __msg_cbytes; /* current number of bytes on queue */
msgqnum_t msg_qnum; /* number of messages currently on queue */
msglen_t msg_qbytes; /* max number of bytes allowed on queue */
__pid_t msg_lspid; /* pid of last msgsnd() */
__pid_t msg_lrpid; /* pid of last msgrcv() */
__syscall_ulong_t __glibc_reserved4;
__syscall_ulong_t __glibc_reserved5;
};
int msgctl(int msgid,int cmd,struct msgid_ds *buf);
//发送数据函数,成功返回0否则-1
//ptr必须指向类似上述mytype的结构体,nbytes表示消息长度
//flag默认为0,可以设置为IPC_NOWAIT表示非阻塞IO
int msgsnd(int msgid,const void* ptr,size_t nbytes,int flag);
//获取数据函数,成功返回消息数据长度,否则返回-1
//ptr同样是指向上述mymsg结构体的指针,nbytes表示获取消息数据的字节数量
/*
type = 0 表示直接拿队列首元素
type > 0 表示拿去指定type类型的首元素
type < 0 表示拿取比type绝对值类型小的中最小类型的首个消息
*/
//flag默认为0,可以设置为IPC_NOWAIT表示非阻塞IO
ssize_t msgrcv(int msgid,void* ptr,size_t nbytes,long type,int flag);
一个简答的例子
进程A从console写入数据到消息队列中,进程B从消息队列中拿出数据并写到console
POSIX
上述消息队列是通过Sys5 XSI接口实现的 POSIX也有提供了一套mq接口,其可以和siganl相结合来实现实时的mq
一个简单的发布订阅例子
sem4 shm mq都是异步IPC
管道
最古老的IPC方式,管道本身是存储在内核态当中的,用户进程通过两个fd来和管道交互。由于管道通信是单向的,因此需要决定那一边是用来写的,哪一边是用来读的。只能在有公共祖先进程的两个进程之间进行通信,其只能在两个进程间使用
#include<unistd.h>
//成功返回0 否则-1 两个文件描述符,其中fd[0]是读描述符 fd[1]是写描述符
//一般,先调用pipe,然后forl子进程,让子进程与父进程进行通信。
int pipe(int fd[2]);
基本流程
- 调用pipe,获取读写描述符 0是读描述符 1是写描述符
- 选择关闭不需要的描述符
- 往指定的描述符调用read write即可
如果想要使用双向pipe就需要创建两个pipe
一个例子
子父进程pipe通信例子,父进程读入数据后写给文件描述符,子进程再将其输出到console
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<wait.h>
#include<stdint.h>
int main(void)
{
int fd[2];
int n;
pid_t pid;
char line[20];
pipe(fd);
pid = fork();
if(pid > 0)
{
close(fd[0]);
write(fd[1],"hello world\n",12);
}
else
{
close(fd[1]);
n = read(fd[0],line,n);
write(STDOUT_FILENO,line,n);
}
exit(0);
}
//下面是一个通过命令行读写的例子
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<wait.h>
#include<stdint.h>
int main(void)
{
int fd[2];
char buf[20];
int len = 0;
if(pipe(fd) < 0)
{
printf("Pipe init Bug\n");
exit(0);
}
pid_t pid = fork();
//子进程会获得父进程fd的拷贝
if(pid > 0)
{
close(fd[0]);
printf("Parent process %d read: ",getpid());
len = read(STDIN_FILENO,buf,sizeof(buf));
write(fd[1],buf,len);
close(fd[1]);
}
else if(pid == 0)
{
close(fd[1]);
printf("Child process %d write: ",getpid());
len = read(fd[0],buf,sizeof(buf));
write(STDOUT_FILENO,buf,len);
close(fd[0]);
}
exit(0);
}
上述例子中,读取数据一定用fd[0],写入数据一定用fd[1]
可以通过管道来实现一组子父进程之间的同步机制,具体函数如下
- TELL_WAIT
- TELL_PARENT
- TELL_CHILD
- TELL_PARENT
- WAIT_CHILD
具体实现如下:
除此之外还可以用信号来实现上述同步机制
有名管道
如果要无关管道之间进行通信,则需要使用FIFO管道,可以在多个进程中使用。本质上是创建一个文件,然后无关进程通过共同读写这个文件来进行数据交换。一旦创建一个fifo后就可以使用open打开它,一般的文件读写函数都可以用于fifo
在操作fifo前也必须先open它
#include<sys/types.h>
#include<sys/stat.h>
//成功返回0,否则返回-1,mode_t参数和open参数一致
int mkfifo(const char* pathname,mode_t mode);
//每个被创建的fifo文件在使用时都需要通过open打开后才能用
一个无关进程使用fifo进行读写的例子,FIFO本身也是以先进先出的顺序读写数据的
//client代码
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<string.h>
#include<stdio.h>
#define FIFO_PATH "../fifo"
int main(void)
{
if(access(FIFO_PATH,F_OK) < 0)
mkfifo(FIFO_PATH,O_CREAT);
int len = 0;
int fd = open(FIFO_PATH,O_RDONLY);
char buf[64];
while(1){
while((len = read(fd,buf,64)) > 0)
write(STDOUT_FILENO,buf,len);
sleep(1);
}
close(fd);
exit(0);
}
//server代码
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<string.h>
#include<stdio.h>
#define FIFO_PATH "../fifo"
int main(void)
{
if(access(FIFO_PATH,F_OK) < 0)
mkfifo(FIFO_PATH,O_CREAT);
int len = 0;
int fd = open(FIFO_PATH,O_WRONLY);
char buf[64];
while(1){
printf("Enter msg: ");
len = read(STDIN_FILENO,buf,64);
if(len != 0)
write(fd,buf,len);
sleep(1);
}
close(fd);
exit(0);
}
上述代码有个问题,FIFO文件是临界区资源,没加锁,导致可能会出现输入数据与读取数据不一致的情况出现,可以用sem4或者pthread_t那一堆东西加锁
双向通信管道
和pipe相比,通信可以是双向的。可以将其理解成一个支持双向通信的pipe
#include<sys/socket.h>
//domain往往取PF_LOCAL
//type同样可以取SOCK_STREAM|SOCK_DGRAM sv是一个大小为2的int sockfd数组
//成功返回0 否则-1
int socketpair(int domain,int type,int protocol,int *sv);
网络编程基础
基础
围绕着套接字编程建立的体系 socket,该函数会返回一个以文件描述符表示的套接字(网络链接端点),类似FIFO也会创建一个socket类型的文件。
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h> //internet socket
#include<sys/un.h> //unix socket
//domain取值如下
/*
1. PF_LOCAL local domain protocols used on the same system
2. PF_INET ipv4
3. PF_INET6 ipv6
*/
//types取值如下
/*
SOCK_STREAM 可靠的,有序的,流式的传输 TCP
SOCK_DGRAM 无序的,数据包 UDP
SOCK_RAW 用来传输底层协议 比如ICMP数据报
*/
//PROTOCOL如果设置为0,那么会根据domain和type选择默认协议,常见取值如下
/*
1. IPPROTO_TCP
2. IPPROTO_UDP
3. IPPROTO_IP
4. IPPROTO_IPV6
5. IPPROTO_ICMP
*/
int socket(int domain,int type,int protocol);
//获取sock信息,往往在系统为server分配端口后调用该函数获取所分配的端口号,这里的len需要是已经赋值的值 eg socklen_t len = sizeof(addr);
int getsockname(int fd,struct sockaddr* addr,socklen_t* len);
//当创建完一个sock后,通过bind函数将其与一个name相绑定
//sockaddr往往并不直接使用,根据domain,选择不同的sockaddr,然后再嵌入到socket中
/*
UNIX Domain
struct sockaddr_un;
*/
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_); //sun_family 表示地址集合 在PF_LOCAL下就用PF_LOCAL
char sun_path[108]; /* Path name. 表示所创建的套接字文件地址 */
};
/*
IP Domain
struct sockaddr_in;
*/
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_); //sin_family 指定PF_INET即可
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
int bind(int sock,const struct sockaddr* name,socklen_t namelen);
//下面两个函数主要用于tcp协议,在flags = 0的情况下是等同于write和read的
//flags取值如下
/*
1. MSG_DONROUTE 数据包不过路由器,在局域网上传递
2. MSG_OOB 表示接收和发送带外数据
3. MSG_PEEK 接收数据时不从缓冲区移走数据,其他进程还可以读取数据
4. MSG_WAITALL 在数据量不够的情况下,读操作会等待
*/
//下述四个函数的返回值,如果成功都是返回读写个数,否则-1
int send(int sockfd,void* buf,int len,int flags);
int recv(int sockfd,void* buf,int len,int flags);
//下面这两个接口主要用在udp协议的传输当中
int sendto(int s,void* buf,int len,unsigned int flags,const struct sockaddr* to,int tolen);
int recvfrom(int s,void* buf,int len,unsigned int flags,const struct sockaddr* from,int fromlen)
基本步骤
Server
- 建立socket,通过fcntl来设置一些属性比如O_NONBLOCK
- 建立socketaddr_un|in对象,作为socket名称,设置其中属性
- 调用setsockopt函数,来设置socket属性,诸如reusedaddr
- 调用bind函数,将socket和sockaddr绑定
- 对sockfd进行读写操作,或者执行listen,connect,accept操作
- 执行完毕后,需要close fd 并且unlink生成的socket文件
Client
- 建立socket,通过fcntl来设置一些属性比如O_NONBLOCK
- 建立socketaddr_un|in对象,作为socket名称,设置其中属性
- 调用setsockopt函数,来设置socket属性,诸如reusedaddr
- 直接通过sockaddr和sockfd进行IO操作,或者进行链接操作即可
- 完成后需要关闭sockfd
socketpair和socket的区别在于,前者的数据是不会经过内核网络栈的处理的
一个本地通信的例子
使用PF_LOCAL Domain,哪怕是这个简单demo,都会有以下问题
- socket默认是阻塞IO的,而如果在执行IO函数时,陷入了阻塞状态,则接收不到信号 设置socket非阻塞(轮询)的方法和设置一般文件是一样的 fcntl(fd,F_SETFL,O_NONBLOCK)
- socketaddr在没有进行复用设置的情况下时没有办法关闭服务器后再开启服务器的,会报错 errno = 98
- 在定义socket生成sockfd错误的话,会报错 errno = 9
这个简单demo中实现了client从命令行接收数据后,通过PF_LOCAL的方式经过SOCK_DGRAM将数据发送给server,server再把数据打印到console上面
//server代码
#include<sys/socket.h>
#include<sys/un.h>
#include<errno.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<signal.h>
#define PATH "../key"
#define SIZE 64
static int flag = 1;
void handler(int status)
{
flag = 0;
}
int main(void)
{
if(signal(SIGINT,handler) == SIG_ERR)
{
printf("signal bug %d\n",errno);
exit(0);
}
int len = 0;
int on = 1;
char buf[SIZE];
struct sockaddr_un name;
name.sun_family = AF_LOCAL;
strcpy(name.sun_path,PATH);
/*
一定注意,socket默认是阻塞IO的,
如果在运行过程中陷入了阻塞态,那么此时是接受不了信号的
从而导致没法关闭socket描述符和删除key文件,导致后续程序都关闭,重启不了
*/
int fd = socket(AF_LOCAL,SOCK_DGRAM,0);
//设置非阻塞的方法和一般文件是一样的
fcntl(fd,F_SETFL,O_NONBLOCK);
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
if(bind(fd,(struct sockaddr*)&name,sizeof(name)) < 0)
{
//errno = 9 表示文件描述符错误
//errno = 98 表示关闭的socket链接在短时间内又打开
printf("bind error %d\n ",errno);
exit(0);
}
while(flag != 0)
{
sleep(1);
while((len = read(fd,buf,SIZE)) > 0)
{
printf("Get data : %s",buf);
//如果不冲刷缓冲器,会又遗留数据的情况
memset(buf,0,SIZE);
}
}
close(fd);
//删除使用的套接字文件
unlink(PATH);
exit(0);
}
//client代码
#include<sys/socket.h>
#include<sys/un.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<signal.h>
#include<errno.h>
#define PATH "../key"
#define SIZE 64
static int flag = 1;
void handler(int status)
{
flag = 0;
}
int main(void)
{
if(signal(SIGINT,handler) == SIG_ERR)
{
printf("signal bug %d\n",errno);
exit(0);
}
int len = 0;
char buf[SIZE];
struct sockaddr_un name;
name.sun_family = AF_LOCAL;
strcpy(name.sun_path,PATH);
int fd = socket(AF_LOCAL,SOCK_DGRAM,0);
while(flag)
{
len = read(STDIN_FILENO,buf,SIZE);
if(len > 0)
sendto(fd,buf,len,0,(struct sockaddr*)&name,sizeof(name));
}
close(fd);
exit(0);
}
Dgram
跨网络通信通信使用的是大端字节序,因此需要使用hton/ntoh进行转换。DGRAM使用数据包的方式来传递数据,比如UDP协议。在这种模式下发送数据不需要监听,直接发即可。但必须用sendto
当使用PF_INET domain的通信时,需要使用如下结构
#include<sys/socket.h>
#include<netinet/in.h>
socklen_t len;//用来表示sockaddr长度
struct sockaddr_in;//详细见基础
//如果将sockaddr_in.sin_addr.s_addr设置为INADDR_ANY或者0,表示会监听本机所有网卡发送上来的树
//如果将sockaddr_in.sin_port设置为0,那么表示会由操作系统分配一个端口用于接收数据
//上述两者都为0往往用在服务器程序中,接收任意网卡发送上来的指定端口号的数据
#include<netdb.h>
struct hostent//用来描述主机名信息的结构体
{
char *h_name; /* Official name of host. */
char **h_aliases; /* Alias list. */
int h_addrtype; /* Host address type. */
int h_length; /* Length of address. */
char **h_addr_list; /* List of addresses from name server. h_addr_list[0]*/
#ifdef __USE_MISC
# define h_addr h_addr_list[0] /* Address, for backward compatibility.*/
#endif
};
//这个函数只能返回IPV4肚子鼓
struct hostent* gethostbyname(char*);
//这个函数通过指定flags = PF_INET | PI_INET6 从而可以获取ipv4或者ipv6地址
struct hostent* gethostbyname2(char*,int flags);
Internet Domain是不需要在操作完成后删除(unlink) socket文件的
DGRAM通信的基本流程
Servert
- 调用socket开辟描述符
- 建立sockaddr_in结构,指定IP,PORT等信息
- 调socksetopt等来设置sock信息
- 调bind将name和sockfd绑定
- dgram不需要listen因此,直接调read或者recv等函数接收数据即可
- INET Domain的套接字编程无需unlink socket文件,完成后直接close即可
Client
- 调socket
- 建立sockaddr_in结构
- 通过addr,hostname等等信息指定hostent,如果发送给本机那么将sockaddr_in.sin_addr.s_addr = 0即可
- 调socksetopt等设置sock信息
- udp等dgram不需要链接,因此可以直接sendto,send函数将数据发送给指定的sockaddr
- 完成后close sockfd
细节:在设置sockaddr_in时,所有的数据就必须是大端字节序的了,需要调hton系列函数
不管是Dgram还是Stream所有套接字编程都必须通过ip+port+ipproto的方式来确定链接,因此需要比较C-S两者的配置是否一致
一个使用UDP协议,进行IPV4通信的例子如下所示,向本机的进程通过inet的方式发送udp数据包:这里用的阻塞IO方式
//server代码
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<stdint.h>
#define SIZE 64
//下面这套获取IP的接口是有点问题的
#define SETIP(a,b,c,d) (uint32_t)(((a << 24)) | (b << 16) | (c << 8) | d)
static char* getIP(int o_ip)
{
char *addr;
addr = (char*)malloc(20);
int ip = ntohl(o_ip);
sprintf(addr,"%d.%d.%d.%d",(uint32_t)((ip & 0x11000000) >> 24),
(uint32_t)((ip & 0x00110000) >> 16),
(uint32_t)((ip & 0x00001100) >> 8),
(uint32_t)(ip & 0x00000011));
return addr;
}
int main(void)
{
int fd = socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP);
//这一步可以说是分配操作
//其中设置的数据都必须是网络字节序(大端序的)
struct sockaddr_in name;
socklen_t len;
name.sin_family = PF_INET;
name.sin_addr.s_addr = htonl(INADDR_ANY);//表示会从本机上的所有网卡监听数据
name.sin_port = htons(0); //表示会由操作系统去分配一个可以使用的端口号
//DGRAM传递的是不可靠协议的数据报,因此不需要listen
bind(fd,(struct sockaddr*)&name,sizeof(name));
//要得到分配后的port信息要使用getsockname
//最终取得的数据都必须先转换成主机序(小端序)再进行输出
len = sizeof(name);
getsockname(fd,(struct sockaddr*)&name,&len);
printf("ip:%s port:%d\n",getIP(name.sin_addr.s_addr),htons(name.sin_port));
char buf[SIZE];
int datalen = 0;
printf("Prepare to get data....\n");
datalen = read(fd,buf,SIZE);
if(datalen > 0)
printf("%s\n",buf);
//socket domain PF_INET是不需要unlink socket的
close(fd);
}
//client代码
#include<sys/socket.h>
#include<netdb.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<stdint.h>
#define SIZE 64
#define SETIP(a,b,c,d) (uint32_t)(((a << 24)) | (b << 16) | (c << 8) | d)
#define DEST_HOST_NAME "iZwz92bfh2uw40ntqgmu4pZ"
static char* getIP(int o_ip)
{
char *addr;
addr = (char*)malloc(20);
int ip = ntohl(o_ip);
sprintf(addr,"%d.%d.%d.%d",(uint32_t)((ip & 0x11000000) >> 24),
(uint32_t)((ip & 0x00110000) >> 16),
(uint32_t)((ip & 0x00001100) >> 8),
(uint32_t)(ip & 0x00000011));
return addr;
}
int main(int argc,char* argv[])
{
int target_port = atoi(argv[1]);
int fd = socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP);
//在发送时,可以通过地址,主机名称等信息得到目标主机的hostent结构体
//下面的代码跑不起来
struct sockaddr_in name;
/*
struct hostent *host_name;
host_name = gethostbyname(DEST_HOST_NAME);
printf("target addr %d\n",(uint32_t)host_name->h_addr_list[0]);
//h_addr_list[0]来获取目标ip地址 h_length获得长度
memcpy(&name.sin_addr.s_addr,host_name->h_addr_list[0],host_name->h_length);
*/
//ip地址0.0.0.0表示本机地址,其是不能作为目标地址的,而127.0.0.1是本机上的回环地址
name.sin_addr.s_addr = 0;
name.sin_family = PF_INET;
name.sin_port = htons(target_port);
char buf[SIZE];
int len = 0;
printf("Enter data : ");
len = read(STDIN_FILENO,buf,SIZE);
sendto(fd,buf,len,0,(struct sockaddr*)&name,sizeof(name));
close(fd);
}
Stream
往往通过tcp协议去使用stream方式来传输数据。与udp不同,tcp在传输前,server需要先调listen来监听端口来处理链接,再通过accept从链接队列中拿出一个链接(每accept一个connect request会创建一个新的sockfd),对这个sockfd进行读写。对于client来说,通过connect来建立链接。其涉及的接口如下:
#include<sys/socket.h>
//与目标服务器建立链接,如果成功返回0,否则返回-1
//阻塞与非阻塞下connect的表现会有所不同
//这里的sockaddr和sendto一样都表示目标地址
int connect(int sockfd,struct sockaddr* serv_addr,int addr_len);
//如果成功就返回0,否则返回-1
//这个函数表示服务器开始监听数据,所有新声明的请求全都会被加入请求队列中,这个backlogs表示请求队列中最大元素个数一般设为0-5
int listen(int fd,int backlogs);
/*
tcp链接三次握手,都在底层完成,完成后链接会被放到一个大小为backlog的内核队列中,当前个数+1,每调用一次accept,队列中元素的当前个数-1
*/
//取出监听队列中的第一个链接请求,并且生成一个新的套接字
//这里的sockaddr和socklen_t表示的是发送请求过来的client的地址信息
//注意每个由accept创建的套接字,在其执行完毕后,都需要关闭描述符 也就是说tcp服务器即需要关闭链接的套接字描述符也需要关闭socket本身的描述符
//accept也是需要事先获取len的 struct socklen_t len = sizeof(addr);
int accept(int sockfd,struct sockaddr* addr,socklen_t* len);
基本流程如下:
Server:
- Server端调用socket指定SOCK_STREAM与IPPROTO_TCP
- 建立sockaddr_in6|in,设置family,addr,port
- 根据需要调用setsockopt与getsockname
- 调用listen开始监听fd指向的内核连接队列
- 死循环去调用accept从连接队列中去取出连接并且进行读写IO (这里往往是多线程|进程的)
- 最后每个完成读写的线程|进程需要关闭accept的描述符
- 服务器进程结束后,listen的描述符也需要关闭
Client:
- Client调用socket
- 建立目标sockaddr
- 调用setsockopt
- 调用connect与目标服务器进行连接
- 进行读写操作
- 完成后关闭描述符 别忘记了hton ntoh
一个多线程tcp客户端服务器通信的例子:
服务端每次从连接队列accept一个描述符后,就开辟一个线程用来进行读写,并且对于每个fd连接来说,都是非阻塞的IO方式,从而让服务器可以一次处理多个client的请求。并且这个例子是使用ipv6协议的
//Server代码
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>
#include<pthread.h>
#define SIZE 64
#define BACKLOG 3
static int MAX_CLIENTS = 2;
static int CUR_CLIENTS = 0;
void* Server_Handler(void* args)
{
int fd = *(int*)args;
fcntl(fd,F_SETFL,O_NONBLOCK);
char buf[SIZE];
int data_len = 0;
while(1)
{
if((data_len = read(fd,buf,SIZE)) > 0)
{
printf("%d Client data : %s\n",fd,buf);
memset(buf,0,data_len);
}
sleep(1);
}
close(fd);
exit(0);
}
int main(int argc,char* argv[])
{
char buf[SIZE];
int data_len = 0;
int fd = socket(PF_INET6,SOCK_STREAM,0);
struct sockaddr_in6 name;
//ipv6地址一共128位也就是16字节,需要通过16字节的字节数组去表示
uint8_t src_addr[16];
memset(src_addr,0,16);
socklen_t len;
name.sin6_family = PF_INET6;
name.sin6_port = htons(0);
//进行地址赋值的时候也是直接用memcpy把数据拷贝过去,需要注意ipv6的地址是不需要设置字节序的
//memcpy(name.sin6_addr.s6_addr,src_addr,16);
//in6addr_any == INADDR_ANY
//sin6_addr.s6_addr是一个16字节的数组
name.sin6_addr = in6addr_any;
if(bind(fd,(struct sockaddr*)&name,sizeof(name)) < 0)
{
printf("bind error : %d\n",errno);
exit(0);
}
len = sizeof(name);
getsockname(fd,(struct sockaddr*)&name,&len);
//这里获取端口号必须要经过ntohs转换不然会有问题
printf("Sys port is %d\n",ntohs(name.sin6_port));
if(listen(fd,BACKLOG) < 0)
{
printf("listen failed : %d\n",errno);
exit(0);
}
//这个循环主要是从连接队列中取连接,并且新建套接字
while(1)
{
struct sockaddr_in6 client;
socklen_t cl_len;
cl_len = sizeof(client);
//accept也是事先需要获取len的
int cl_fd = accept(fd,(struct sockaddr*)&client,&cl_len);
if(cl_fd < 0)
{
printf("accept failed : %d\n",errno);
exit(0);
}
if(CUR_CLIENTS >= MAX_CLIENTS)
{
printf("Connect queue is full\n");
continue;
}
CUR_CLIENTS++;
pthread_t new_thread;
pthread_create(&new_thread,NULL,Server_Handler,&cl_fd);
}
close(fd);
return 0;
}
//client代码
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>
#include<pthread.h>
#define SIZE 64
#define BACKLOG 3
static int MAX_CLIENTS = 2;
static int CUR_CLIENTS = 0;
int main(int argc,char* argv[])
{
int port = atoi(argv[1]);
printf("target port:%d\n",port);
uint8_t address[16];
memset(address,0,16);
int fd = socket(PF_INET6,SOCK_STREAM,0);
struct sockaddr_in6 name;
memcpy(name.sin6_addr.s6_addr,address,16);
name.sin6_family = PF_INET6;
name.sin6_port = htons(port);
//connect中的sockaddr表示的是服务端的信息
if(connect(fd,(struct sockaddr*)&name,sizeof(name)) < 0)
{
printf("Connect failed:%d\n",errno);
exit(0);
}
char buf[SIZE];
int data_len = 0;
while(1)
{
if((data_len = read(STDIN_FILENO,buf,SIZE)) > 0)
write(fd,buf,data_len);
memset(buf,0,SIZE);
}
close(fd);
exit(0);
}
IPV6概述
通过netstat -na可以查看系统中当前端口号的使用信息,再通过grep的方式去把我需要的东西给截取出来。
ipv6的授权接口与ipv4会有一些差异。
//首先socket指定的域位PF_INET6
socket(PF_INET6,SOCK_STREAM|SOCK_DGRAM,0);
//其次IPV6的地址结构完全不同
struct sockaddr_in6;
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_); //sin6_family PI_INET6
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address sin6_addr.s6_addr 16B数组 */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
//ipv6的地址在设置的时候往往使用一个16字节的数组通过memcpy进行拷贝的赋值
网络工具
tcpdump
在linux系统下,可以直接使用tcpdump来对网络数据包进行抓包。其常用的指令和内容如下
telnet
高级IO
非阻塞IO
阻塞IO是指,当运行到IO时,进程会从就绪态进入阻塞态,CPU会给其他进程使用,此时接收不到信号。非阻塞IO则是指,当运行到IO时,进程继续运行,如果当前进程再次调用IO函数,在没有执行完的情况下,则会返回错误信息-1。进程会始终被IO进程占用,采用轮询方法,反复上下文切换来执行IO,效率也是比较低的
O_NONBLOCK
fcntl(int fd,F_SETFL,O_NONBLOCK);
例子代码如下
#include<errno.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<sys/times.h>
#include<time.h>
#define SIZE 500000
char buf[SIZE];
int main(void)
{
/*
char* tp_buf = "1";
int fd = open("./txt",O_RDWR|O_CREAT|O_TRUNC,S_IRWXG);
for(int i = 0 ; i < SIZE ; i++)
write(fd,tp_buf,1);
close(fd);
*/
struct timespec start;
struct timespec end;
int fd = open("./txt",O_RDONLY);
fcntl(fd,F_SETFL,O_NONBLOCK);
int rest = SIZE , cur = 0;
char* ptr = buf;
while(rest > 0)
{
errno = 0;
cur = read(fd,ptr,rest);
if(cur > 0)
{
rest -= cur;
ptr += cur;
}
else
printf("Read:%d ERRON:%d\n",cur,errno);
}
printf("\nRead Successfully\n");
fcntl(STDOUT_FILENO,F_SETFD,O_NONBLOCK);
rest = SIZE , cur = 0;
ptr = buf;
clock_gettime(CLOCK_REALTIME,&start);
while(rest > 0)
{
errno = 0;
cur = write(STDOUT_FILENO,ptr,rest);
if(cur > 0)
{
rest -= cur;
ptr += cur;
}
else
printf("Write:%d ERRON:%d\n",cur,errno);
}
clock_gettime(CLOCK_REALTIME,&end);
printf("\nWrite Successfully %f ms\n",(end.tv_nsec-start.tv_nsec)/(double)1000000);
close(fd);
exit(0);
}
记录锁
记录锁可以确保某一个进程在读写文件某一个区域时是原子的。只锁定了文件中的一个区域,用来进行进程间文件读写的同步。相当于读写锁的扩展,接口如下
#include<fcntl.h>
int fcntl(int fd,int cmd,struct flock* flockptr);
struct flock{
short l_type;//F_RDLOCK,F_WRLOCK,F_UNLOCK
short l_whence;//SEEK_SET,SEEK_CUR,SEEK_END,
off_t l_start;//起点偏移位置
off_t l_len;//如果为0,那么锁定范围就为最大偏移量
pid_t l_pid;
};
//可以通过l_whence = SEEK_SET l_start = 0 l_len = 0 那就意味着对整个文件加锁
规则如下(cmd)
- F_GETLK 测试是否可以设置FLK
- F_SETLK 设置FLK,如果无法设置,则返回负数
- F_SETLKW 设置FLK,如果无法设置,就阻塞
先由F_GETLK测试能否加锁,再由F_SETLK|F_SETLKW来加锁
如果一个进程对文件的区域已经有了一把锁,那么该进程后续加上的锁会代替掉之前加上的锁
apue上的加锁解锁例子
加锁时,对应锁必须要有对应权限,读锁有读权限,写锁有写权限
一个例子
记录锁继承与释放的规则
- 子进程是不继承父进程文件锁的 否则会出现子父进程同时对一个文件有写锁的情况
- exec系列函数会继承原来的文件锁,在没有设置close-on-exec的情况下
- 所有文件锁在进程结束时会结束
如果一个文件描述符被关闭了,那么其对应的文件锁也会被释放掉!
其他文件锁syscall
-
lockf
只支持互斥锁,不支持共享锁,并且和fcntl相比,只能从文件首部开始指定范围
#include<unistd.h> int lockf(int fd,int value,off_t size); //value F_UNLOCK F_LOCK F_TLOCK(trylock) F_TEST //size表示从起始位置的加锁范围
IO多路复用
对于一般服务器进程来说,需要处理多个链接,阻塞IO会造成一部分client数据接收不到,非阻塞IO在没有数据的情况下会忙等,从而浪费cpu资源。**IO多路复用说人话是当任一文件描述符已经准备IO后,会调用select函数来进行告知哪一个fd可以去执行操作。**其也会造成阻塞,只不过不阻塞在sockfd的IO阶段,而是阻塞直到某一个sockfd准备好了IO。
常用集合如下:
#include<sys/select.h>
/* fd_set for select and pselect. fd集合,用于select或者pselect控制*/
//所谓的fd_set表示描述符集合,实际上是一个长整型数组,其中的每个元素的每一位对应一个fd
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
//其数组大小为1024/(8 * 8),也就是最大16个数字
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
//清空描述符集合
FD_ZERO(fd_set* set);
//添加描述符集合
FD_SET(int fd,fd_set* set);
//删除描述符集合
FD_CLR(int fd,fd_set* set);
//判断指定fd是否在描述符集合中
FD_ISSET(int fd,fd_set* set);
//告知内核在指定描述符中有一个|多个时间发送时,或者在经过val的时间后才返回,否则阻塞。
/*
val的取值 这个参数可以被理解为每多少秒轮询一次
1. val = NULL,永久阻塞直到某一个描述符返回
2. 等待val指定的时间,如果没有描述符准备就绪那么依旧返回
3. val = 0 此时和非阻塞IO的轮询方法一致
*/
/* A time value that is accurate to the nearest
microsecond but also has a range of years. */
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
//nfds表示最大文件描述符 + 1 = 文件描述符个数
//readfds writefds exceptfds表示检测文件描述符的参数
//返回就绪的描述符个数,如果返回0则表示计时已到,返回-1表示错误
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,timeval* val);
int pselect()
poll();
epoll();
IO多路复用的基本流程
Client端代码流程不变,Server端流程如下 以tcp协议为例
- 首先需要绑定SIGCHLD对应的reap函数用来回收僵尸进程
- 调用socket建fd
- 建立sockaddr_in绑定端口号等信息
- 调用fcntl与setsockopt
- 调bind绑定 完成后根据需要调用getsockname
- 调listen监听内核连接队列
- 主循环中清空fd_set,将sockfd进行FD_SET,每次循环前重置timeval
- 调用select, 返回值大于0的情况下,根据FD_ISSET调用accpet
- 开辟新进程/线程来对accept返回的描述符进行IO,完成后需要close
- 整个系统退出后也需要close
通过IO多路复用解决了一个进程上开辟了多个port用来监听数据,避免忙等和阻塞的情况。但是往往服务器进程会在一个port上去监听多个client链接。
一个多进程IO多路复用的例子,这个例子中client端的代码是一样的有区别的只是server端代码,通过select选取出可用的内核连接队列描述符,再从这个描述符生成网络IO描述符,开辟进程后再运行。
//server端代码
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/select.h>
#include<sys/signal.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/time.h>
#include<netinet/in.h>
#define SIZE 128
void handleIO(int fd)
{
char data[SIZE];
memset(data,0,SIZE);
int len;
while(1)
{
if((len = read(fd,data,SIZE)) > 0)
{
printf("%s\n",data);
memset(data,0,SIZE);
}
}
printf("connect close\n");
close(fd);
exit(0);
}
void handleSocket(int sockfd)
{
int new_fd = 0;
struct sockaddr_in addr;
socklen_t addr_len = sizeof(addr);
if((new_fd = accept(sockfd,(struct sockaddr*)&addr,&addr_len)) < 0)
{
printf("accept error : %d\n",errno);
exit(0);
}
printf("accept connection %d\n",new_fd);
//IO多路复用中会额外开辟一个进程或者线程去处理IO
if(fork() == 0)
handleIO(new_fd);
}
void reap(int status)
{
//#include<sys/wait.h>
wait(NULL);
}
int main(int argc,char* argv[])
{
//这一步操作主要是用来reap僵尸进程的
//SIGCHLD信号是当子进程结束时,会发送信号给父进程
signal(SIGCHLD,reap);
int fd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in addr;
socklen_t addr_len;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = PF_INET;
addr.sin_port = htons(0);
//fcntl与setsockopt
bind(fd,(struct sockaddr*)&addr,sizeof(addr));
addr_len = sizeof(addr);
getsockname(fd,(struct sockaddr*)&addr,&addr_len);
printf("target port : %d\n",ntohs(addr.sin_port));
listen(fd,5);
int flag = 0;
struct timeval time;
fd_set set;
while(1)
{
FD_ZERO(&set);
FD_SET(fd,&set);
//每次select完毕后time会被清零,因此每次循环前要重新赋值
time.tv_sec = 3;
time.tv_usec = 0;
//这个select fd相当于判断内核队列中有无需要读写?
if((flag = select(fd + 1,&set,NULL,NULL,&time)) < 0)
{
printf("Select error %d\n",errno);
exit(0);
}
else if(flag == 0)
{
printf("Waiting Connection\n");
continue;
}
else
{
if(FD_ISSET(fd,&set) > 0)
handleSocket(fd);
else
continue;
}
}
close(fd);
exit(0);
}
异步IO
基础
与前述几种IO模型的比较
-
阻塞IO
进程在执行IO过程中会上下文切换到内核态,此时进程无法执行其他内容 同步阻塞
-
非阻塞IO
进程在执行IO过程中,会反复进行上下文切换来寻求读写数据 同步非阻塞
-
IO多路复用
进程会检测描述符集合并且阻塞指定的时间,在描述符集合满足条件后,返回指定描述符个数,并且可以通过另一个进程/线程来处理描述符,从而做到描述符检测和描述符处理的并行。但是IO多路复用中依旧存在阻塞的情况,因为select本身是会产生阻塞,进行上下文切换的 异步阻塞
异步IO 则是完全异步,调IO接口后,进程还可以异步运行其他内容,不产生阻塞。异步非阻塞
三种异步IO的方式
- Semi-Asyc by select/poll with SIGPOLL
- Sys5 by ioctl with SIGPOLL 只能用于stream
- BSD derived by open & fcntl with SIGIO & SIGURG 只能用于终端与网络
POSIX AIO
主要由POSIX提供的一系列IO接口
内存映射IO(MMIO)
为进程的虚拟内存地址空间和物理地址空间创建映射关系。具体映射分为
- 文件映射,磁盘上的文件映射进程的VM
- 匿名映射,初始化全为0的内存空间
映射关系是否共享又分为
- 私有映射
- 共享映射
mmap函数本质上是将一个磁盘文件映射到进程的虚拟内存空间中,这样一来,读写虚拟内存空间就相当于直接读写磁盘文件。
#include<sys/mman.h>
//将文件与进程内存相映射
//addr表示指定映射在VM中的起始位置,一般为0表示由系统分配
//len表示映射长度
/*
prot表示对映射区域保护要求 这一部分参数是可以|运算级联的
1. PROT_READ
2. PROT_WRITE
3. PROT_EXEC
4. PROT_NONE
*/
/*
flag表示私有,共享映射参数
1. MAP_SHARED 表示共享映射,写mmap后的地址相当于修改原文件
2. MAP_PRIVATE 表示私有映射,mmap后映射的是磁盘后文件的副本,也就是说write不会修改其值
*/
void* mmap(void* addr,size_t len,int prot,int flag,int fd,off_t off);
//在进程结束时,会自动解除映射,调用munmap会强制提前结束映射
int munmap(void* addr,size_t len);
MMIO的使用方法
- MMIO的读往往都比read高效很多
- MMIO的写在小数据包(<1024B)的情况下会比write高效很多
MMIO在实际开发中的一些应用
- 与/dev/zero进行映射
- 由于linux下,所有设备都可以通过抽象的文件描述符去访问,因此可以将设备直接映射到进程的虚拟地址空间来进行IO
一个简单的利用MMIO复制文件的例子
#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
int main(int argc,char* argv[])
{
int fdin,fdout;
fdin = open(argv[1],O_RDONLY);
fdout = open(argv[2],O_RDWR | O_CREAT | O_TRUNC,S_IRWXG);
//stat表示文件属性
struct stat size;
off_t len = 0;
//stat(char* path,struct stat* file_stat) 获取文件属性
stat(argv[1],&size);
len = size.st_size;
//ftruncate(int fd,off_t len) 生成指定空文件
ftruncate(fdout,len);
//addr指定为0则表示系统给出的虚拟地址空间 offset = 0表示从文件首部开始
void* src = mmap(0,len,PROT_READ,MAP_PRIVATE,fdin,0);
void* dst = mmap(0,len,PROT_READ|PROT_WRITE,MAP_SHARED,fdout,0);
memcpy(dst,src,len);
munmap(src,len);
munmap(dst,len);
exit(0);
}
信号驱动的IO
通用函数
- #include<unistd.h>
- #include<getopt.h>
命令行参数解析
//用来解析由DPDK命令行所得到的参数信息
//optstring 短参数a:b::c 分别表示的意思 -a 100 -b200 -c
/*struct option{
char* name;名称
int hashflag;no_argument,required_argument
int *flag;如果为NULL返回val,否则val返回0,flag指向对应的返回值
int val;返回值
}*/
//longindex 返回longindex在option数组中的下标
//找到短选项就返回其值,找到长选项就返回看flag是否为NULL来判断返回什么
//如果最后所有均找完就返回EOF
int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex)
如果要同时解析长短选项,需要使用switch嵌套的方式
#include<unistd.h>
#include<getopt.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//linux长短选项解析
//长选项取值的全局参数
int lopt;
//长选项数组
static struct option opts[]={
{"Version",no_argument,NULL,'v'},
{"case",required_argument,&lopt,1},
{0,0,0,0},
};
//短选项字符串,短选项只能用一个字符作为选项起始,如果ab,那么表示-a -b两个选项
char* const short_opts="c:q:";
int main(int argc,char* argv[])
{
//得到每一项选项的遍历变量
int opt;
//记录长选项在长选项数组中ID的变量
int loptid;
//一般使用嵌套switch的方法来解析长短选项
while((opt = getopt_long(argc,argv,short_opts,opts,&loptid)) != EOF){
if(opt == -1)
break;
switch (opt)
{
//optarg可以得到短选项的值,也可以得到长选项的值
case 'c':
printf("cores:%d\n",atoi(optarg));
break;
case 'q':
printf("queues:%d\n",atoi(optarg));
break;
default:
switch (opts[loptid].val)
{
case 'v':
printf("Version:2.3.0\n");
break;
case 1:
printf("Case:%d\n",atoi(optarg));
break;
};
}
}
return 0;
}
参数格式相关
#include<in.h>
htons();