Linux下高级C编程
第一章 unix/linux系统的基本概念
第二章 unix/linux系统下的编程基础和开发方式
第三章 unix/linux系统下的内存管理
第四章 unix/linux系统下的文件管理和目录操作
第五章 unix/linux系统下的进程管理
第六章 unix/linux系统下的信号处理
第七章 unix/linux系统下的进程间通信
第八章 unix/linux系统下的网络编程
第九章 unix/linux系统下的多线程编程
文章目录
前言
主要学习unix/linux系统下的API编程一、linux系统的基本概念
1.linux系统简介
2.gcc/cc基本使用
gcc/cc xxx.c 可以编译链接C源程序生成一个可执行文件 a.out
整个流程分4步:
(1) 预处理/预编译 (包含头文件的扩展,以及执行宏替换等,生成 .i 文件)
(2) 编译 (将高级语言程序翻译成汇编语言,得到汇编文件,生成 .s 文件)
(3) 汇编 (将汇编语言翻译成机器指令,得到目标文件,生成 .o 文件)
(4) 链接 (将目标文件和标准库链接,得到可执行文件)
3.常见的编译选项
-E 实现预处理的执行,默认将处理输出到控制台,可以通过 -o 选项指定输出到 xxx.i 文件中,预处理文件中包含很多头文件,类型别名,以及各种函数声明等
-S 实现编译处理,得到一个 .s 为后缀的汇编文件
-c 实现编译处理,得到一个 .o 为后缀的目标文件
-v 查看编译器的版本信息
-std 指定执行的C标准
-wall 尽可能的产生警告信息
-werror 将警告当做错误进行处理
-g 产生调试信息,采用GDB进行单步调试
-O 进行具体的优化
-x 指定源代码的具体编程语言
4.常用的C程序文件后缀
.h 头文件 包含结构体的定义,变量和函数的声明
.c 源文件 包含变量和函数的定义
.i 预处理文件
.s 汇编文件
.o 目标文件
.a 静态库文件 对功能函数打包
.so 共享库文件 对功能函数打包
5.多文件编程
预处理:
#include 表示将指定文件的内容包含进来
#define 表示一个宏
#undef 表示取消一个宏
#if 表示如果
#ifdef 表示如果定义
#ifndef 表示如果没有定义
#else 表示否则
#elif 表示否则如果
#endif 表示结束判断
常用指令:
#line 整数n 表示修改代码的行数/指定行号,修改下一行行号为n
#error 字符串 表示产生一个错误信息
#warning 字符串 表示产生一个警告
#pragma GCC dependency 文件名 表示当前文件依赖于指定的文件,如果当前文件的最后一次修改时间早于依赖的文件,则产生警告信息
#pargma GCC poison 标识符 表示将后面的标识设置成毒药的意思,一旦使用标识符,则产生错误或者警告信息
#pargma pack (整数n) 表示按照整数n的倍数进行对齐补齐,必须按照2的最小次方对齐补齐
6.预定义宏
__FILE__ 表示获取所在的文件名(使用 %s 打印输出)
__BASE_FILE__ 表示获取正在编译的原文件名(使用 %s 打印输出)
__LINE__ 表示获取所在行号(使用 %d 打印输出)
__FUNCTION__/__func__ 表示获取所在函数名称(使用 %s 打印输出)
__DATE__ 获取日期信息(使用 %s 打印输出)
__TIME__ 获取时间信息(使用 %s 打印输出)
7.环境变量的概念和使用
环境变量: 存放和编程环境/系统环境相关信息的变量
PATH/path 就是一个环境变量,存放各种路径,在 PATH/path 中存放路径的程序不需要在增加路径就可以直接运行
编程相关的环境变量:
CPATH/C_INCLUDE_PATH 主要表示C语言中头文件的所在路径
CPLUS_INCLUDE_PAHT 主要表示C++头文件所在路径
LIBRARY_PATH 编译链接时查找静态库和共享库的路径
LD_LIBRARY_PATH 运行时查找共享库的路径
查找头文件的方式:
(1) #include <>
表示去系统默认的路径中查找指定的头文件
(2) #include ""
表示去当前工作目录中进行查找
(3) 配置环境变量 CPATH 进行查找
export CPATH=$CPATH:头文件所在路径
(4) 使用编译选项进行查找(重点)
gcc/cc xxx.c -l 头文件所在的路径
二、linux系统下的编程基础和开发方式
1.库文件的概念和使用
(1)静态库
静态库文件在使用时,直接将调用的功能代码复制到目标文件
缺点:造成目标文件变大,不利于修改和维护
优点:不需要跳转,所以效率比较高,不依赖于静态库文件
(2)共享库
共享库文件在使用时,将所调用功能代码在共享库文件中的地址拷贝到目标文件中
缺点:需要跳转,所以效率比较低,依赖于共享库文件的存在
优点:目标文件比较小,修改和维护比较方便
2.静态库的生成和调用
静态库文件的生成步骤:
(1) 编写源码程序 xxx.c(例如 add.c)
(2) 只编译不连接,生成目标文件 xxx.o(例如 add.o)
(3) 使用 ar -r 命令生成静态库文件
ar -r lib库名.a 目标文件1 目标文件2 ...
lib库名.a 叫做静态库文件名
例如: ar -r libadd.a add.o
静态库文件的调用步骤:
(1) 编写调用库文件的源码 xxx.c(例如 main.c)
(2) 只编译不链接生成目标文件 xxx.o(例如 mian.o)
(3) 链接测试程序和库文件,方法有三种:
1) 直接链接
gcc/cc main.o libadd.o
2) 通过编译器选项进行间接链接
gcc/cc main.o -l 库名 -L 库文件所在的路径
3) 配置环境变量 LIBRARY_PAHT 进行链接
export LIBRARY_PATH=$LIBRARY_PAHT:.
gcc/cc main.o -l 库名
3.共享库的生成和调用
共享库文件的生成步骤
(1) 编写源程序 xxx.c(例如 add.c)
(2) 只编译不链接,生成目标文件 xxx.o(例如 add.o)
gcc/cc [-fpic]/*小模式选项,希望目标文件越小越好*/ add.c
(3) 生成共享库文件
gcc/cc -shared xxx.o -o lib库名.so
例如: gcc/cc -shared xxx.o -o libadd.so
共享库文件的调用步骤
(1) 编写调用库文件的源程序 xxx.c(例如: main.c)
(2) 只编译不链接,生成目标文件 xxx.o(例如: main.o)
gcc/cc -c main.c
(3) 链接测试程序和库文件,方法有三种:
1) 直接链接
gcc/cc main.o libadd.so
2) 通过编译器选项进行间接链接
gcc/cc main.o -l 库名 -L 库文件所在的路径
3) 配置环境变量 LIBRARY_PATH 进行链接
export LIBRARY_PATH=$LIBRARY_PATH:.
gcc/cc main.o -l 库名
4.共享库动态加载
(1)dlopen 函数
打开共享库文件
函数: void * dlopen( const char * pathname, int mode);
参数一: 字符串形式的共享库文件名
参数二: 加载标志
RTLD_LAZY 延迟加载
RTLD_NOW 立即加载
返回值:
失败返回: NULL
成功返回: 该共享库对应的句柄/地址
(2)dlerror 函数
排查显示出错的信息
函数: char * dlerror(void);
返回值:
当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功
(3)dlsym 函数
根据参数指定的句柄和函数名,返回函数名所对应的内存地址
函数: void * dlsym(void*handle,constchar*symbol);
参数一: 共享库句柄,dlopen的返回值
参数二: 字符串类型的符号,也就是函数名
返回值:
函数的地址
(4)dlclose 函数
关闭参数指定的共享库句柄,也就是关闭共享库
函数: int dlclose (void *handle);
5.C语言的错误处理
return 0; 表示程序正常结束
return -1; 表示程序不正常结束
1.错误表示的一般规则(是否出错)
C语言中通过使用返回值来表示是否出错,根据返回值来进行具体的错误处理
(1) 如果返回值类型是int型,并且返回的值不可能是负数时则使用返回值-1代表出错,其他数据类型表示正常返回
(2) 如果返回值类型是int型,并且返回值可能是负数时,则需要使用指针取出返回的数据,返回值仅仅表示是否出错,-1 表示出错,0 表示正常返回
(3) 如果返回值类型是指针类型,则返回NULL代表出错
(4) 如果不考虑是否出错,返回值类型使用void即可
2.错误编号和错误信息(为什么错了)
#include<errno.h> 头文件
errno 是一个全局变量,当函数调用失败时,会将具体的错误编号设置到errno中可以在通过error来获取错的原因
3.错误信息
(1) strerror函数 表示字符串错误,这个函数主要用于将参数指定的错误编号翻译成对应的错误信息返回
(2) perror函数 表示最后一个错误信息大印出来,参数s不为空时原样输出,后面追加一个冒号和空格,再跟着错误信息以及换行
(3) printf函数 printf("%m"); 打印错误信息
注意:
判断函数的调用是否成功,还是需要根据返回值进行判断,而在确定已经调用失败的情况下,这时可以通过errno来获取错误的原因,也就是不能直接使用error来判断函数的调用是否成功
6.环境表的概念和使用
1.环境表的概念
环境表 是指环境变量的集合,而每个进程都拥有一张独立的环境表,来保存和当前进程相关的环境变量信息
环境表采用字符指针数组来存储所有的环境变量,使用全局变量 char** environ 来记录环境表的首地址,使用NULL来代表环境表的结束,所以访问环境表则需要借助 environ 变量
2.环境表相关的处理
(1) getenv 函数
函数: char *getenv(const char *name);
表示根据参数指定的环境变量名去环境表中进行查找,返回该环境变量所对应的环境值,查找失败则返回NULL
(2) setenv 函数
函数: int setenv(const char *name, const char *value, int overwrite);
第一参数 环境变量名
第二参数 环境变量值
第三参数 是否修改,如果参数指定的环境变量不存在则增加,如果存在并且overwrite 是非0,则修改环境变量值,是否环境变量不变
(3) putenv 函数
函数: int putenv(char *string);
表示按照参数的内容增加/修改环境变量,其中string的格式为: name=value, 如果不存在则添加,存在则修改
(4) unsetenv 函数
函数: int unsetenv(const char *name);
表示根据参数指定的环境变量去环境表中进行删除
(5) clearenv 函数
函数: int clearenv(void);
表示清空整个环境表
3.主函数的原型
int main(int argc,char* argv[],char* envp[]){}
第一个参数:命令行参数的个数
第二个参数:命令行参数的地址信息
第三个参数:环境表的首地址
三、linux系统下的内存管理
1.程序和进程的概念
程序:表示存放在硬盘上的可执行文件
进程:表示在内存中正在运行的程序
2.进程中的内存区域划分 (内存地址从小到大,其中堆和栈没有明确的分割线,可以适当的调整)
(1) 代码区 存放程序的功能代码的区域 例如:函数名
(2) 只读常量区 主要存放字符串常量和const修饰的全局变量
(3) 全局区 主要存放已经初始化的全局变量和static修饰的局部变量
(4) BSS段 主要存放没有初始化的全局变量和static修饰的局部变量
(5) 堆区 主要表示使用 malloc/calloc/realloc 等手动申请的动态内存空间,内存由程序员手动申请手动释放
(6) 栈区 主要存放局部变量(包括函数的形参),const修饰的局部变量,以及块变量,该内存区域由操作系统自动管理
3.字符串存储形式之间的比较
对于一个存储常量字符串的字符指针和字符数组来说,字符指针可以改变指向,不可以改变指向的内容,而字符数组可以改变指向的内容,不可以改变指向
对于一个存储常量字符串的动态内存空间来说,其中指向该内存空间的指针,既可以改变指向,又可以改变内容
4.虚拟内存管理技术
unix/Linux系统的内存都是采用虚拟内存管理技术来进行管理的,
即:每个进程都有0~4G的内存地址 (虚拟的,并不是真实存在的),由操作系统负责把内存地址和真实的物理内存映射起来.
因此,不同进程的内存地址看起来是一样的,但是所对应的物理内存是不一样的
每个进程中0~4G的虚拟地址空间分为: 用户空间和内核空间.
用户空间指0~3G虚拟地址空间,而内核空间指3G~4G的虚拟地址空间.
用户程序运行在用户空间,内核空间只有系统内核才能访问,用户程序不能直接访问内核空间,
不过系统内核提供了一些系统函数负责从用户空间切换到内存空间
内存地址的基本单位是字节,内存映射的基本单位是内存页,目前主流的操作系统中一个内存页是4kb (4096字节)
1Tb=1024Gb
1Gb=1024Mb
1Mb=1024Kb
1Kb=1024byte (字节) 1字节= 8位
1byte=8个bit (二进制位)
5.段错误的由来
(1) 使用scanf函数时缺少 &
(2) 空指针/野指针的使用
(3) 试图使用一个没有经过映射的虚拟地址可能引发段错误
STL(标准模版库) - 申请/释放动态内存
new/delete - 申请/释放动态内存,c++语言中的运算符
malloc()/free() - 申请/释放动态内存,标c函数
sbrk()/brk() - 申请/释放动态内存,UC函数
mmap()/munmap() - 建立/解除 到内存的映射
6.使用malloc 申请动态内存的特性
(1) 使用malloc 申请内存的注意事项
使用malloc申请动态内存时,可能还需要额外的12个字节来存储一些用于管理动态内存的信息,比如内存的大小等
malloc底层采用链表的形式去处理多个内存块,也就是需要保存有关 下一个内存块/上一个内存块 的信息
使用malloc申请的动态内存,千万不要越界访问,因为极有可能破坏管理信息,从而引发段错误
(2) 使用malloc 申请内存的一般映射规则
一般来说,使用malloc申请比较小的动态内存时,操作系统会一次性分配33个内存页,从而提高效率
7.使用free 释放动态内存的特性
一般来说,使用malloc申请比较大的内存时,系统会默认分配34个内存页,
当申请的内存超过34个内存页时,则系统会再次分配33个内存页(也就是按照33个内存页的整数倍进行配)
使用free释放动态内存时,释放多少则在动态内存总数中减去多少,
当所有的内存释放完毕时,进程还保留33个内存页备用,知道进程结束,主要是为了提高效率
8.内存处理的相关函数
(1) getpagesize函数
函数: int getpagesize(void);
主要用于获取系统中一个内存页的大小,一般为4kb
(2) sbrk 函数
函数: void *sbrk(intptr_t increment);
主要用于按照参数指定的大小来调整内存块的大小,
如果参数大于0表示申请内存,如果参数等于0表示获取内存块的当前位置,
如果参数小于0表示释放内存,成功返回之前内存块的地址,失败返回-1
sbrk操作内存的一般规则:
申请比较小的内存时,一般会默认分配1个内存页,
申请的内存超过1个内存页时,会再次分配1个内存页,
释放内存时,释放完毕后剩余的内存如果在一个内存页内,则一次性释放1个内存页
(3) brk 函数
函数: int brk(void *addr);
表示操作内存的末尾地址到参数指定的位置,
如果参数指定的位置大于当前的末尾位置,则申请内存,如果参数指定的位置小于当前的末尾位置,则释放内存
(4) mmap 函数
函数: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
第一个参数:建立映射的起始地址,给NULL 由系统内核选择
第二个参数:建立映射的大小
第三个参数:映射的权限
PROT_EXEC - 可执行
PROT_READ - 可读
PROT_WRITE - 可写
PROT_NONE - 不可访问
第四个参数: 映射的模式
MAP_SHARED - 共享的
MAP_PRIVATE - 私有的
MAP_ANONYMOUS - 映射到物理内存
第五个参数:文件描述符,映射物理内存给0即可
第六个参数:文件偏移量,映射物理内存给0即可
返回值 :成功返回映射的地址,失败返回MAP_FAILED (-1)
函数功能:建立文件/设备 到内存映射
(5) munmap函数
函数: int munmap(void *addr, size_t length);
第一个参数:映射的地址
第二个参数:映射的大小
函数功能:删除指定的映射
四、linux系统下的文件管理和目录操作
1.基本概念
linux/unix系统中,几乎所有的一切都可以统称文件,因此,对于文件的操作几乎适合所有的设备等等,目录也可以看作文件处理
/dev/null 空设备
echo 字符串 表示原样输出字符串内容
echo 字符串>文件名 表示写入字符串到文件
echo 字符串>/dev/null 表示丢弃结果信息
cat /dev/null>文件名 表示清空文件
/dev/tty 输入输出设备,一般默认为终端(了解)
echo 字符串>/dev/tty 表示输出到输入输出设备
cat /dev/tty 表示读取/打印输入输出设备内容
2.文件的相关处理函数
(1) open 函数
函数: int open(const char *pathname, int flags, mode_t mode);
第一个参数:字符串形式的路径和文件名
第二个参数:操作标志 必须包含以下访问权限中的一个:
O_RDONLY - 只读
O_WRONLY - 只写
O_RDWR - 可读可写
还可以按位或上以下标志中的一个:
O_CREAT - 文件存在则打开,不存在则创建
O_EXCL - 与O_CREAT搭配使用,如果存在则创建失败
O_TRUNC - 文件存在,是个规则文件,打开方式有写权限,则清空文件
O_APPEND - 追加
第三个参数:操作模式用于指定创建的新文件权限,如:0644 查找权限 ls -l 文件名
返回值: 成功返回一个新的文件描述符,失败返回-1,文件描述符就是一个非负整数,用于代表一个打开的文件
(2) close 函数
函数: int close(int fd);
参数: 文件描述符
(3) read 函数
函数: ssize_t read(int fd, void *buf, size_t count);
第一个参数:文件描述符 (数据从哪里读取)
第二个参数:缓冲区的首地址 (数据存到哪里去)
第三个参数:读取的数据大小
返回值:成功返回读取到的数据大小,失败返回-1
(4) write 函数
函数: ssize_t write(int fd, const void *buf, size_t nbyte);
第一个参数:文件描述
第二个参数:偏移量
第三个参数:从什么地方开始偏移
SEEK_SET - 文件的起始位置
SEEK_CUR - 文件的当前位置
SEEK_END - 文件的尾部位置
返回值: 成功返回当前位置距离文件头的偏移量,失败返回-1
3.文件描述符
文件描述符本质就是一个整数,可以代表一个打开文件.
但是文件的信息并不是保存在文件描述符中,而是存在文件表等结构中,使用open函数打开一个文件时,会把文件的信息放入文件表等结构中,
但是处于安全和效率等因素的考虑,文件表等结构不适合直接操作,而是给文件表对应一个编号,使用编号进行操作,这个编号就是文件描述符
系统还会管理文件描述符,在每个进程中都会有一张描述符总表,当有新的文件描述符需要时,会去总表中查找未使用的最小值并且返回.
文件描述符本质就是非负整数,也就是从0开始,一直OPEN_MAX(在linux系统中一般是255),其中0 1 2被系统占用,分别代表标准输入,标准输出以及标准错误
注意:
打开不同的文件时,对应的问价表和v节点表信息都不同,而多次打开相同文件时,v节点表信息相同,文件表信息不同close()函数的工作方式:
先把文件描述符与文件表的对应关系解除,不一定会删除文件表,只有当文件表没有与其他描述符对应时才会删除文件表(一个文件表可以对应多个描述符),close()函数也不会修改描述符的整数值,但是会让一个描述符无法代表一个文件
4.文件的非读写函数
(1) dup 函数
函数: int dup(int oldfd);
表示根据参数指定的文件描述符进行拷贝,成功返回新的文件描述符,失败返回-1
(2) dup2 函数
函数:int dup2(int oldfd, int newfd);
表示将参数newfd作为参数oldfd的拷贝,如果有必要,则先关闭newfd,成功返回新描述符,也就是newfd,失败返回-1
(3) fcntl 函数
根据文件描述符对文件执行的操作:
a. 复制文件描述符
b. 设置/获取文件描述符的标志(了解)
c. 设置/获取文件状态的标志(了解)
d. 实现文件锁的功能(掌握)
函数: int fcntl(int fd, int cmd, ... /* arg */ );
第一个参数:文件描述符
第二个参数: 操作的命令
F_DUPFD - 复制文件描述符的功能,寻找最小的有效的大于等于第三个参数arg的描述符作为新的描述符,与dup2()函数所不同的是,不会强制关闭已经被占用的描述符 fcntl(fd,F_DUPFD,5);
F_GETFD/F_SETFD - 获取/设置文件描述符的标志
F_GETFL/F_SETFL - 获取/设置文件状态的标志
F_SETLK/F_SETLKW/F_GETLK - 加锁/解锁/测试锁是否存在
第三个参数:可变长参数,参数是否需要,主要取决于参数cmd
返回值:
F_DUPFD - 成功返回新的文件描述
F_GETFD - 成功返回文件描述符的标志值
F_GETFL - 成功返回文件状态的标志值
其他操作成功返回0,所有的操作失败返回-1
(4)使用fcntl 函数实现文件锁功能
当使用多个进程同时读写文件时,可能会引发文件的混乱,如果左右的进程都是读文件,则可以同时进行,但是只要有一个进进程执行写操作,则多个进程应该串行操作而不是并行,使用文件锁实现上面的想法,文件锁就是读写锁,一把读锁和一把写锁,其中读锁是一把共享锁,允许其他进程加读锁,不允许加写锁,写锁是一把互斥锁,不允许其他进程加读锁和写锁
使用文件锁的功能时:
第二个参数的取值
F_SETLK/F_SETLKW/F_GETLK - 加锁/解锁/测试锁是否存在
第三个参数取值:指定以下结构体类型的指针(一般使用的时候用 &取结构体的地址)
struct flock
{ ...
short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */ /*锁的类型*/
short l_whence; /* How to interpret l_start:SEEK_SET, SEEK_CUR, SEEK_END */ /*表示从什么地发开始*/
off_t l_start; /* Starting offset for lock *//*偏移量*/
off_t l_len; /* Number of bytes to lock *//*锁定的字节数*/
pid_t l_pid; /* PID of process blocking our lock(F_GETLK only) *//*加锁的进程号 , 给 -1 */
...
};
1)F_SETLK 的使用 主要用于加锁/解锁 的功能
测试结果:加完读锁之后,还是可以向文件写入数据的,结果说明锁是独立于文件的,并没有真正锁定对文件的读写操作,也就是说锁只能锁定其他的锁换句话说,如果加了一把锁,可以导第二个加锁失败(两个读锁除外)
如何使用文件锁来控制是否可以对文件进行读写操作。
实现方案:在执行读操作之前尝试加读锁,在执行写操作之前尝试加写锁,根据能不能枷锁成功来决定是否进行读写操作即可。
释放文件锁:
a.进程结束,自动释放所有文件锁
b.将文件锁的类型设置为F_UNLCK,使用fcntl 重新设置来实现解锁的效果
2)F_SETLKW 的使用 功能与F_SETLK类似,所不同的是,加不上锁会等待,直到锁能加上为止
3)F_GETLK 的使用 测试一下参数锁能否加上,如果能加上,则不会去加锁,而是将锁的类型改成F_UNLCK;如果不能加上,则见将文件中已经存在的锁的信息通过参数锁带出来,并且将l_pid设置为真正给文件枷锁的进程号,所以可以使用l_pid判断锁是否能加上
(5)stat/fstat函数
函数: int stat(const char *pathname, struct stat *statbuf);
函数: int fstat(int fd, struct stat *statbuf);
函数功能:获取指定文件的状态信息
第一个参数:文件的路径/文件描述符
第二个参数:结构体指针,结构体变量的地址
struct stat
{
...
mode_t st_mode; /*文件的类型和权限*/
off_t st_size; /*文件的大小*/
time_t st_mtine; /*文件的最后一次修改时间*/
...
};
扩展:
#include<time.h>
char *ctime(const time_t *timep); =>主要用于将整数类型的时间转换为字符串形式的时间
struct tm *localtime(const time_t *time); =>主要用于将整数形式的时间转换为结构体的指针类型
结构体的类型:
struct tm {
int tm_sec; /* seconds 秒*/
int tm_min; /* minutes 分钟*/
int tm_hour; /* hours 小时*/
int tm_mday; /* day of the month 日*/
int tm_mon; /* month 月 +1 */
int tm_year; /* year 年 +1900 */
int tm_wday; /* day of the week 星期几*/
int tm_yday; /* day in the year 年中的第几天 +1*/
int tm_isdst; /* daylight saving time 夏令时 */
};
(6)access函数
函数:int access(const char *pathname, int mode);
函数功能:主要用于文件是否存在以及对应的权限
第一个参数:文件的路径和文件名
第二个参数:操作模式
F_OK 判断文件是否存在
R_OK 判断文件是否可读
W_OK 判断文件是否可写
X_OK 判断文件是否可执行
(7)chmod 函数
函数功能:主要用于将参数指定的文件权限修改为参数二指定的值
(8)truncate 函数
函数功能: 主要用于将参数一指定的文件大小截取到参数二指定的大小,如果文件变小则多余数据丢失,如果文件变大则扩展出来的区域用"\0"填充
(9)umask 函数
函数功能: 主要用于设置创建文件时需要屏蔽的权限,返回之前屏蔽的权限
(10)其他处理函数:
link() 主要用于创建硬链接
unlink() 主要用于删除硬链接
rename() 主要用于重命名
remove() 主要用于删除指定的文件
5.目录管理
常用的函数
(1) opendir 函数
函数: DIR *opendir(const char *name);
函数功能: 主要用于打开参数指定目录,并且返回该目录所在地址。
(2) readdir 函数
函数: struct dirent *readdir(DIR *dirp);
函数功能: 主要用与读取参数指定的目录中内容,返回结构体指针。
struct dirent {
ino_t d_ino; /* inode number i节点的编号*/
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 supportedby all file system types 文件的一个类型*/
char d_name[256]; /* filename 文件名*/
};
(3) closedir 函数
函数功能: 主要用于关闭参数指定的目录。
(4) 其他函数:
mkdir() 创建一个目录
rmdir() 删除一个目录
chdir() 切换所在的目录
getcwd () 获取当前程序所在的工作目录
五、linux系统下的进程管理
1. 基本概念
进程 表示在内存中运行的程序
程序 表示在磁盘上运行的可执行文件
2. 基本命令
ps 表示察看当前终端上启动的进程(进程的快照)
ps 命令的执行结果如下:
PID - 进程号(重点)
TTY - 终端的编号
TIME - 消耗cpu的时间
CMD - 命令的名称和参数
ps - aux 表示显示所有包含其他使用者的进程
ps - aux | more 表示将ps - aux 的结果进行分屏显示
ps - aus |more 执行的结果如下:
USER 进程的属主(熟悉)
PID 进程编号(掌握)
%CPU 占用CPU的百分比
%MEM 占用内存的百分比
VSZ 虚拟内存的大小
RSS 物理内存的大小
TTY 终端编号
STAT 进程的状态信息(熟悉)
START 进程的启动时间
TIME 消耗CPU的时间
CMD 命令的路径和名称以及参数(掌握)
ps - ef 表示以全格式的方式显示当前所有的进程
ps - ef | more 分屏显示命令的执行结果
ps - ef | more的执行结果:
PPID - 父进程的进程号
常见的进程状态:
S 休眠状态
s 进程的领导者(拥有子进程)
Z 僵尸进程
R 正在运行的进程
O 可运行的进程
T 挂起状态
< 优先级高
N 优先级低
...
目前主流的操作系统都支持多进程,如果进程A启动了进程B,那么进程A叫做进程B的父进程,而进程B叫做A的子进程
进程0是系统内部的进程,负责启动进程1(init),也会启动进程2,而且其他所有的进程都是 进程1/进程2,直接/间接 地启动起来
3.各种ID的获取
PID - 进程号,是进程的唯一标识,操作系统采用延迟重用的策略来管理进程号,保证在每一个时刻PID都唯一
PPID - 父进程号,与进程号的使用策略相同
关于ID 的函数:
getpid() - 获取进程号
getppid() - 获取副进程号
getuid() - 获取用户ID
getgid() - 获取用户组ID
4.进程相关函数
创建:
(1) fork函数
函数功能:主要用于以复制正在调用进程的方式去创建一个新的进程,新进程叫做子进程原来的进程叫做父进程,成功时父进程返回子进程的ID,子进程返回0,失败时返回-1
fork 创建子进程的代码执行方式是:
a. fork 之前的代码,父进程执行一次
b. fork 之后的代码,父子进程各自执行一次
c. fork 函数的返回值,父子进程各自返回一次
注意事项:fork 创建成功子进程之后,父子进程各自独立,并没有明确的先后执行顺序
父子进程的关系:
a. 父进程启动了子进程,父进程同时启动,如果子进程先结束,则父进程负责帮助回收子进程的资源
b.如果父进程先结束,子进程会变成孤儿进程,子进程变更父进程(重新认定父进程,一般选择 init(1) 进程作为父进程), init进程也叫做孤儿院
c.如果子进程先结束,但是父进程由于各种原因没有回收子进程的资源,子进程变成僵尸进程
父子进程中内存资源的关系:
使用fork创建的子进程,会复制父进程中除了代码区之外的其他内存区域,代码区和父进程共享
扩展:
a.如何创建出4个进程?
fork();
fork();
4个进程: 1个父进程,2个子进程,1个孙子进程
b.如何创建出3个进程?
pid_t pid = fork();
if(0 != pid) //父进程
{
fork();
}
3个进程:1个父进程 + 2 个子进程
c. 俗称 fork 炸弹
while(1)
{
fork();
}
进程的一个终止:
正常终止进程的方式:
a. 在main函数中执行了return0;
b. 调用exit()函数终止进程
c. 调用_exit()/_Exit()函数
d. 最后一个线程返回
e. 最后一个线程调用了pthrread_exit()函数
非正常终止的方式:
a. 采用信号终止进程
b. 最后一个线程被其他线程取消
退出:
(2) _exit/_Exit 函数(uc的函数/标c的函数)
函数功能:这两个函数都用于立即终止正在调用的进程,参数作为返回值返回给父进程,来代表进程的退出状态,可以使用wait 系列函数获取推出状态
_exit 函数
函数功能:主要用于引起正常进程的终止,参数status中的低8位作为退出状态信息返回给父进程,该函数在终止进程期间会调用atexit()/on_exit()函数注册过的函数
atexit 函数
函数功能:主要用于按照参数指定的函数进行注册,注册过的函数会在正常进程终止时被调用
等待:
(3) wait 函数
函数功能:主要用于挂起正在运行的进程进入组塞状态,直到有一个子进程终止,参数用于获取终止进程的退出状态,成功返回终止进程的进程号,失败返回-1
WIFEXITED(*status) 判断是否正常终止
WEXITSTATUS(*status) 获取进程退出状态的信息
(4) waitpid 函数
函数功能:挂起当前正在运行的进程,直到指定进程的状态发生改变为止
第一个参数:进程号
< -1 表示等待进程的组ID为pid绝对值的任意一个子进程(了解)
-1 表示等待任意一个子进程
0 表示等待和调用进程在同一个进程组的任意一个子进程(了解)
> 0 表示等待进程号为pid的进程
第二个参数: 指针参数,获取进程的推出状态信息
第三个参数: 选项 默认给0即可
返回值: 成功返回状态发生改变的进程号,失败返回-1
(5) 其他处理函数
vfork 函数
函数功能:功能与fork函数基本一样,所不同的是不会拷贝父进程的内存区域,而是直接占用,导致父进程进入阻塞状态,直到子进程终止,或者调用exec系列函数跳出为止,对于子进程的终止建议使用_exit()函数
exec系列函数:
execl 函数 函数功能:主要用于实现跳转的功能
第一个参数:执行的文件路径及文件名
第二个参数:执行的选项,一般给可执行文件的执行方式
第三个参数:可变长参数
注意:
vfork 函数本身没有太大的实际意义,一般与exec系列函数搭配使用。
fork 函数也可以和exec系列函数搭配使用,但是基本不会这样用。
system 函数
函数功能: 主要用于执行指定的shell命令,以及shell脚本
shell 脚本编写流程:
a. vi xxx.sh 文件
b. 编写shell指令到文件中
c. 给xxx.sh 文件增加执行权限 chmod a+x shell.sh
d. 执行xxx.sh文件
中断的概念:
中断指暂时停止当前程序的执行,转而去执行新的程序,或者处理出现的意外情况
中断分为两种:软件中断 和 硬件中断
六、linux系统下的信号处理
1.信号的基本概念和分类
(1)概念:
信号本质上就是软件中断,信号既可以作为进程间通信的一种机制,更重要的是,信号总是中断一个进程的正常运行,而信号更多的用于处理一些非正常情况
(2)特点:
a. 信号是异步的,进程并不知道信号什么时候会到来
b. 进程即可以发送信号,也可以处理信号
c. 每个信号都有一个名字,这些名字以SIG开头
(3)命令和分类:
使用命令: kill -l 用于察看当前操作系统所支持的信号
掌握信号:
信号2 SIGINT 使用ctrl + c 产生此信号
信号3 SIGQUIT 使用ctrl + \ 产生此信号
信号9 SIGKILL 使用kill -9 进程号发送此信号(该信号不能被用户捕捉)
一般来说, 在linux系统中信号是从1~ 64, 不保证连续, 其中1~31之间的信号, 叫做不可靠信号, 也就是信号不支持排队, 可能会丢失. 其中34~64 之间的信号叫做可靠信号, 也就是信号支持排队, 不会丢失.
不可靠信号有叫做非实时信号,可靠信号又叫做事实信号
2.信号的处理方式
(1)默认处理,绝大多数信号的处理方式都是终止进程
(2)忽略处理
(3)自定义处理
3.信号的处理函数
(1)signal 函数
函数的功能: 设置指定信号的指定处理方式
#include <signal.h>
typedef void (*sighandler_t)(int);
=> 给函数指针其个别名 叫做 sighandler_t
=> typedef void (*p) (int) sighandler_t;
sighandler_t signal(int signum, sighandler_t handler);
展开后是这样 =>
void (*)(int) signal(int signum,void(*)(int)handler);
继续展开后是这样 =>
void (*)(int) signal(int signum,void(*hander)(int));
最后展开后是这样 =>
void (*signal(int signum,void(*hander)(int)))(int);
首先 signal 是一个函数,具有两个参数的函数,第一个参数是int类型,第二个参数是函数指针类型
函数的返回值类型也是函数指针类型
函数指针类型是一个
指向具有int类型参数和void作为返回值类型的函数的指针
函数功能解析:
第一个参数:信号值/信号名称(处理那个信号)
第二个参数:信号的处理方式(如何进行处理)
SIG_IGN 忽略处理
SIG_DFL 默认处理
自定义函数地址 自定义处理
返回值: 成功返回之前的处理方式,失败返回SIG_ERR
父子进程对信号的处理方式:
对于fork函数创建的父子进程来说,子进程完全按照父进程的对信号的处理方式,也就是父进程忽略则子进程忽略,父进程自定义处理,则子进程自定义处理,父进程默认处理,则子进程默认处理
采用系统函数发送信号
(2) alarm 函数
函数功能:主要用于设置参数指定的秒数之后发送SIGALRM,如果参数为0表示没有设置新的闹钟.成功返回上一个闹钟没有来得及走的秒数,如果之前没有闹钟,则返回0
信号集
信号集:信号的集合用于存储多个信号
在linux系统中,信号的数值范围:1~64,采用最省内存的方式来表示所有的信号,采用什么样的数据数据类型呢?
信号集的数据类型是:
sigset_t 类型 底层还是采用每一个二进制代表一个信号的方式存储多个信号
查找sigset_t 的步骤 cc -E 02set.c -o 02set.i
typedef struct
{
unsigned long int __val[(1024 / (8 * sizeof (unsigned long int)))];
} __sigset_t;
信号集的基本操作:
sigemptyset() 清空信号集
sigfillset() 填满信号集
sigaddset() 增加信号到信号集
sigdelset() 删除信号几中的指定信号
sigismember() 判断信号是否存在
信号的屏蔽
sigprocmask 函数
函数功能: 主要用于检查和改变要屏蔽的信号集
第一个参数: 用什么养的方式屏蔽
SIG_BLOCK: ABC+CDE =>ABCDE
SIG_UNBLOCK: ABC-CDE =>AB
SIG_SETMASK: ABC CDE =>CDE
第二个参数: 新的信号集
第三个参数: 旧的信号集 用于带出之前屏蔽的信号集
sigpending 函数
函数的功能: 主要用于获取在信号屏蔽的期间来过的信号,通过参数将来过的信号带进去
sigaction 函数 => signal函数的增强版
函数功能: 主要用于设置/改变信号的处理方式
第一个参数: 信号值/信号的名称 (设置哪个信号)
(信号SIGKILL和SIGSTOP不能使用)
第二个参数: 针对信号的新处理方式
struct sigaction {
void (*sa_handler)(int);
=> 函数指针用于设置信号的处理方式,
=> 与signal函数中第二个参数相同, SIG_IGN SIG_DFL 函数名
void (*sa_sigaction)(int, siginfo_t *, void *);
=> 函数指针,作为第二种处理信号的方式
=> 是否使用该处理方式,依赖于sa_flags的值
sigset_t sa_mask;
=> 用于设置在信号处理函数执行期间需要屏蔽的信号
=> 自动屏蔽与正在处理的信号相同的信号
int sa_flags;
=> 处理标志
=> 写入 SA_SIGINFO 表示采用第二个函数指针处理信号
=> 写入 SA_NODEFER 表示解除对相同信号的屏蔽
=> 写入 SA_RESETHAND 表示自定义处理信号之后恢复默认处理方式
void (*sa_restorer)(void);
=> 保留成员,暂时不使用
};
第三个参数: 旧的处理方式(用于带出针对信号之前的处理方式)
SIGKILL和SIGSTOP两个信号是不能自定义处理的,只能默认处理
其中第二个函数指针的第二个参数类型如下:
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused hardware-generated signal(unused on most architectures) */
pid_t si_pid; /* Sending process ID *//发送信号的进程ID
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value *//发送信号时的附加数据
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address (since kernel 2.6.32) */
}
sigqueue 函数
函数功能: 表示指向的进程发送指定的信号和附加数据
第一个参数: 进程号(给谁发信号)
第二个参数: 信号值(发送什么样的信号)
第三个参数: 联合类型,附加数据
union sigval {
int sival_int; //整数
void *sival_ptr; //地址
};
4.计时器
在UNIX/linux系统中,操作系统会为每一个进程维护三种计时器:
真实计时器,虚拟计时器,实用计时器;
一般采用真实计时器进行工作,真实计时器采用发送SIGALRM信号进行工作的
#include <sys/time.h>
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
第一个参数: 计时器的类型
ITIMER_REAL 真实计时器,主要描述进程运行的真实时间,通过产生SIGALRM信号工作
ITIMER_VIRTUAL 虚拟计时器,主要描述进程在用户空间消耗的时间,通过产生SIGVTALRM信号工作
ITIMER_PROF 实用计时器,主要描述进程在用户空间和内核空间共同消耗的时间,通过产生SIGPROF信号工作
第二个参数: 计时器的新值
struct itimerval
{
struct timeval it_interval; /* next value */ 间隔时间
struct timeval it_value; /* current value */ 启动时间
};
struct timeval
{
long tv_sec; /* seconds */ 秒数
long tv_usec; /* microseconds */ 微秒(到秒为 10^6)
};
第三个参数: 用于获取计时器的旧值
函数功能: 用于获取/设置计时器的数值
七、linux系统下的进程间通信
1.概念:
两个/多个进程间的数据的交换,叫做进程间的通信
2.进程间通信的方式
(1)文件
(2)信号
(3)管道
(4)共享内存
(5)消息队列(重点)
(6)信号量集
(7)网络 (重点)
...
其中 (4) (5) (6) 统称为XSI IPC通信方式
(X/open system interface Inter-process commucation)
3.使用消息队列实现进程间通信
(1)概念:
使用不同的进程将发送的数据打包成具体个格式的消息,然后将消息放入到消息中,使用其他进程从消息中取出消息,从而实现进程间的通信
(2) 使用消息队列通信的流程
a.获取key值 使用ftok函数
b.创建/获取消息队列,使用msgget函数
c.发送消息/接受消息,使用msgsnd和msgrcv函数
d.如果不再使用消息队列,使用msgctl函数删除消息队列
(3) 相关函数解析
ftok 函数
函数功能: 根据路径和项目ID 来生成一个key_t类型的key值,生成的key提供给后续函数使用
第一个参数: 字符串形式的路径要求存在并且可以访问
第二个参数: 项目ID 要求非0
注意: 使用相同的路径和相同的项目ID 会生成相同的key值
msgget 函数
函数功能: 主要用于创建/获取消息队列,返回队列的ID
第一个参数:key值,ftok的返回值
第二个参数:标志
IPC_CREAT - 创建
IPC_WXCL - 与IPC_CREAT搭配使用,如果存在则创建失败
0 - 获取已存在的消息队列
注意: 如果需要创建一个新的消息队列时,那么需要在参数msgflg中指定新队列的权限,如0644
msgsnd函数
函数功能:主要用于将指定的消息发送到指定的消息队列中
第一个参数: 消息队列的ID msgget函数的返回值
第二个参数: 消息的首地址(消息从哪里来)
struct msgbuf {
long mtype; /* 消息的类型, must be > 0 */
char mtext[1]; /* 消息的数据内容,可以是其他的结构 */
};
第三个参数: 消息的大小 主要是指消息内容的大小,不包括消息的类型
第四个参数: 发送消息的标志,直接给0即可
msgrcv 函数
函数功能: 从指定的消息队列中接受消息
第一个参数: 消息队列的ID msgget函数的返回值
第二个参数: 消息的首地址(将接受的消息存到哪里去)
第三个参数: 消息的大小
第四个参数: 消息的类型
0 表示读取消息队列中的第一条消息
>0 表示读取消息队列中第一个类型为msgtyp的消息
<0 表示读取消息队列中消息类型 <=msgtyp 绝对值的类型最小的消息
第五个参数: 消息的标志,给0即可
返回值: 成功返回实际读取数据的大小,失败返回-1
msgctl 函数
函数功能: 表示对指定的消息队列执行指定的操作
第一个参数: 消息队列的ID msgget函数的返回值
第二个参数: 具体的命令
IPC_RMID 删除消息队列,此时第三个参数给NULL即可
第三个参数: 结构体指针
相关的命令
使用 ipcs -q 命令 表示察看当前系统中存在的消息队列
使用 ipcrm -q ID 命令 表示删除指定的消息队列
4.使用管道实现进程间通信
(1)概念和分类
概念: 本质上是以文件作为媒介来实现进程间的通信,该文件比较特殊,叫做管道文件
管道分为: 有名管道 和 无名管道
有名管道: 一般采用命令创建管道文件,实现任意两个进程之间的通信
无名管道: 一般采用系统函数来创建管道文件,用于父子进程之间的通信
(2)使用有名管道进行进程间的通信
如: touch 文件名(创建普通文件)
使用 mkfifo 管道文件名.pipe => 创建管道文件
例子: mkfifo a.pipe =>创建管道文件a.pipe
使用 echo hello > a.pipe =>写入数据到管道文件中
例子: cat a.pipe => 读取管道文件的数据
注意: 管道文件本身并不会存储传递的数据
(3)使用无名管道实现进程间的通信
pipe 函数 创建无名管道
函数功能: 主要用于创建管道文件,利用参数返回两个文件描述符,其中pipefd[0]关联管道的读端,pipefd[1] 关联管道的写端.
注意: 管道是比较古老的通信方式,现在很少使用(了解)
5.使用共享内存实现进程间通信
(1)概念:
本质上是由内核维护的一块内存区域,共享在两个/多个进程之间,然后两个/多个进程分别对该内存区域进行读写操作,从而实现通信,是最快的IPC的通信方式
(2)共享内存通信的基本流程
a.获取key值 使用ftok函数
b.创建/获取共享内存,从而得到ID,使用 shmget 函数
c.挂接共享内存,使用 shmat 函数
d.使用共享内存,进程读写操作等
e.脱接共享内存,使用 shmdt 函数
f.如果不再使用,使用 shmctl 函数删除共享内存
(3)相关函数解析
shmget 函数
函数功能: 主要用于创建/获取一个共享内存,得到ID
第一个参数: key值,ftok函数的返回值
第二个参数: 共享内存的大小
第三个参数: 操作的标志
IPC_CREAT - 创建
IPC_EXCL - 与IPC_CREAT 搭配使用,存在则创建失败
0 - 获取已经存在的共享内存
注意: 当创建新的共享内存时,需要指定权限
shmat 函数
函数功能: 主要用于将指定的共享内存挂接到正在运行的进程的地址空间中,成功返回共享内存的地址,失败返回-1.
第一个参数: 共享内存的ID,也就是 shmget 的返回值
第二个参数: 共享内存的挂接地址,给NULL由系统选择
第三个参数: 挂接标志,直接给 0 即可
shmdt 函数
函数功能: 表示按照参数指定的共享内存的地址进行脱节,参数传递shmat函数的返回值即可
shmctl 函数
函数功能: 主要用于对指定的共享内存执行指定的操作
第一个参数:共享内存的ID,也就是 shmget 的返回值
第二个参数:执行的操作命令
IPC_RMID 删除共享内存
第三个参数:给NULL即可
相关的基本命令
ipcs -m 察看系统中已经存在的共享内存
ipcrm -m 共享内存ID 删除指定的共享内存
6.使用信号量集实现进程间通信
(1)信号量和信号量集概念
信号量概念:
信号量本质就是一个计数器,用于控制同时访问共享资源的进程数/线程数,也就是说解决有限资源的分配问题.
信号量集概念:
信号量集就是表示信号量的集合,也就是多个计数器的集合,用于控制同时访问多种共享资源的进程数/线程数.
(2)信号量工作方式
a.先把信号量进行初始化,初始化为最大值
b.如果有进程,申请到共享资源,那么信号量的值减1
c.当信号量的值为0时,终止进程对共享资源的申请,让申请共享资源的进程进入阻塞状态
d.如果有进程释放资源,那么信号量的值加1
e.当信号量的值大于0时,阻塞的进程可以继续抢占资源,抢占不到资源的进程继续进入阻塞状态等等
(3)使用信号量集通信流程
a.获取key值 使用ftok函数
b.创建/获取信号量集 使用semget函数
c.初始化信号量集 使用semctl函数
d.操作信号量集 使用semop函数
e.如果不再使用则删除 使用semctl函数
(4)相关函数解析
semget函数
函数功能: 创建/获取信号量集的ID
第一个参数:key值 ,ftok函数的返回值
第二个参数:信号量集的大小,也就是信号量的个数
第三个参数:操作的标志
IPC_CREAT 创建
IPC_EXECL 与IPC_CREAT 搭配使用,存在则创建失败
0 获取已经存在的信号量集
注意: 当创建新的信号量集是,需要指定权限,如0644
semctl函数
函数功能: 主要用于对信号量集实现各种控制操作
第一个参数: 信号量集的ID 使用semget函数的返回值
第二个参数: 信号量集的下标,从0开始
第三个参数: 具体的操作命令
IPC_RMID 删除信号量集,参数semnum被忽略,不需要第四个参数
SETVAL 使用第四个参数的值给信号量集中下标为semnum的信号量进行初始化
第四个参数: 可变长参数,是否需要取决于cmd
semop 函数
函数功能: 对信号量集中指定的信号量进行操作
第一个参数: 信号量集的ID,semget函数的返回值
第二个参数: 结构体指针
结构体参数包含:
unsigned short sem_num; /* 信号量的下标 */
short sem_op; /* 信号量的操作,正数增加,负数减少 */
short sem_flg; /* 操作的标志,给0即可 */
第三个参数: 表示第二个参数所指定的结构体数组的元素的个数
基本的命令
ipcs -s 察看系统中存在的信号量集
ipcrm -s 信号量集的ID ,删除指定的信号量集
ipcs -a 察看系统中所有的IPC结构
八、linux系统下的网络编程
1. 网络的基本常识
七层网络协议
由ISO 将网络从逻辑上划分为七层:
应用层 主要将数据交给应用程序
表示层 主要将数据按照统一的格式进行封装 应用层 TELNET FTP WWW
会话层 主要控制对话的开始和结束等
传输层 主要使用具体的传输协议检查和传输数据 TCP和UDP
网络层 主要进行路径的选择和传递数据 IP和路由
数据链路层 主要将数据转换为高低电平信号 网卡驱动
物理层 主要借助网卡驱动,交换机设备等传输
常见的协议
A.TCP协议 - 传输控制协议,一种面向连接的协议,类似打电话
B.UDP协议 - 用户数据报协议,一种非面向连接的协议,类似发短信
C.IP协议 - 互联网协议,是TCP/UDP的底层协议 IPX 协议
IP 地址
IP 地址 - 是互联网中唯一的地址标识,本质上来讲,就是一个32位的整数,使用(IPV4)目前也有IPV6(128位二进制)
日常生活中采用点分十进制表示法来描述一个具体的IP地址,也就是说,将每一个字节,计算出一个十进制整数,多个十进制之间采用小数点分隔
IP 地址主要分为两个部分:网络地址+主机地址
IP 地址分以下4类
A类: 0 +7 位网络地址 + 24位主机地址
B类: 10 + 14 位网络地址 + 16位主机地址
C类: 110 + 21 位网络地址 + 8位主机地址
D类: 1110 + 28 位多播地址
查看IP的命令
Window 系统下 ipconfig ipconfig/all
Unix/linux操作系统 ifconfig /sbin/ifconfig
子网掩码
一般与IP地址搭配使用,主要用于指定一个IP地址中具体的网络地址和主机地址,也就是说判断两个IP地址是否在同一个子网中0~255
如: IP地址 172.40.5.210 子网掩码 255.255.255.0
把IP和子网掩码按位与 &
172.40.5 - 网络地址
210 - 主机地址
物理地址
物理地址又叫做Mac地址,本质上就是硬件网卡的地址
主要用于通过绑定Mac地址来实现上网设备的控制
端口号
IP地址 主要用于定位具体的某一台主机
端口号 主要用于定位该主机中具体的某一个进程
端口号的数据类型: unsigned short类型 ,取值范围是:0 ~ 65535 其中 0 ~1024 之间的端口由系统占用,自己指定端口号从 1025 以上开始使用
字节序
如: 0x12345678
小端系统: 低位内存地址存放低位数据的系统叫做小端系统
小端 地址从小到大 0x78 0x56 0x34 0x12
大端系统:低位内存地址存放高位数据的系统叫做高位系统
大端 地址从小到大 0x12 0x34 0x56 0x78
网络字节序
主要描述数据在网络中传递时的字节顺序,而网络字节序本质上就是大端字节序
主机字节序
主要描述数据根据在本地系统中存放的字节顺序,
当发送时,由主机字节序转化为网络字节序时候发送,
当接收时,由网络字节序转化为主机字节序.
2.基于socket的一对一通信模型
(1)基本概念
socket 英文中是插座的意思
socket 网络通信的载体,使用socket可以实现了两个进程/两个主机之间的通信
(2)通信的模型
服务器端:
a.创建socket 使用socket函数
b.准备通信地址 使用结构体类型
c.绑定socket和通信地址 使用bind函数
d.进行读写操作 使用read/write函数
e.关闭socket 使用close函数
客户端:
a.创建socket 使用socket函数
b.准备通信地址 使用结构体类型
c.连接socket和通信地址 使用connect函数
d.进行读写操作 使用read/write函数
e.关闭socket 使用clode函数
(3)相关函数解析
socket 函数
函数功能: 主要用于创建一个通信点,成功返回一个描述符
第一个参数: 表示 域/协议族,决定本地通信还是网络通信
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7) 本地通信
AF_INET IPv4 Internet protocols ip(7) 基于IPV4的网络通信
AF_INET6 IPv6 Internet protocols ipv6(7) 基于IPV6的网络通信
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK Appletalk ddp(7)
AF_PACKET Low level packet interface packet(7)
第二个参数: 类型,决定采用何种协议进行通信
SOCK_STREAM 数据流的方式,基于TCP的通信协议
SOCK_DGRAM 数据报的方式,基于UDP的通信协议
第三个参数:特殊协议的意思,给0即可
通信地址的数据类型
a. 通用的通信地址
struct sockaddr
{
sa_family_t sa_family; //协议族
char sa_data[14]; //数据内容
}
注意: 给通信地址通常用于函数的参数类型,用于各种通信地址之间的转换
b. 本地通信的结构体类型
#include<sys/un.h>
struct sockaddr_un
{
sa_family_t sun_family; //地址族,和socket参数一致即可
char sun_path[]; //socket文件的路径和文件名
};
c. 网络通信的结构体类型
#include<netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IP地址
};
struct in_addr
{
in_addr_t s_addr;
};
bind 函数 函数功能: 主要用与绑定指定的socket和通信地址
第一个参数:socket的描述符,就是socket函数的返回值
第二个参数:结构体类型的指针(&通信地址)
第三个参数:通信地址的大小
connect 函数 函数功能:主要用于socket和通信地址之间的连接
第一个参数:socket的描述符,就是socket函数的返回值
第二个参数:结构体类型的指针(&通信地址)
第三个参数:通信地址的大小
字节序的转换函数
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); 主要将unsigned int 的主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort); 主要将unsigned short的主机字节序转换位网络字节序
uint32_t ntohl(uint32_t netlong); 主要将unsigned int的网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort); 主要将unsigned short的网络字节序转换为主机字节序
IP地址的转换函数
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
函数功能:主要将字符串类型的IP地址转换为整数
3.基于TCP的协议的通信模型
(1)创建通信模型
服务器:
a创建socket ,使用socket函数
b准备通信地址,使用结构体类型
c绑定socket和通信地址,使用bind函数
d进行监听,使用listen函数
e响应客户端的连接请求,使用accept函数
f进行通信,使用read/write函数
g关闭socket使用close函数
客户端:
a 创建socket 使用socket函数
b准备通信地址
c链接socket 使用sonnect函数
d进行通信
e关闭socket 使用close函数
(2)函数解析
listen 函数
函数功能: 主要用于监听指定socket上的连接个数
第一个参数:socket描述符,socket函数的返回值
第二个参数: 排队等待被响应的最大连接数
accept 函数
函数功能: 主要用于响应客户端的连接请求
第一个参数:socket描述符,socket函数的返回值
第二个参数: 结构体指针,用于存储客户端的通信地址
第三个参数:指针,通信地址的大小
返回值: 成功返回用于通信的描述符,失败返回-1
socket 函数返回的描述符主要用于监听使用
inet_ntoa 函数
函数功能: 主要将结构体类型的IP地址转换为字符串类型
4.基于UDP协议的通信模型
(1)创建通信模型
服务器:
a创建socket 使用socket函数
b准备通信地址 使用结构体类型
c绑定socket和通信地址 使用bind函数
d进行通信 使用sendto()/recvfrom()/send()/recv()函数
e关闭socket函数 使用close函数
客户端:
a创建socket 使用socket函数
b准备通信地址 服务器的地址
c进行通信 使用sendto()/recvfrom()/send()/recv()函数
d关闭socket 使用close函数
(2)函数解析
发送消息的函数
函数功能: 主要用于向指定的地址发送指定的数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
第一个参数:socket描述符,socket函数的返回值
第二个参数:被发送数据的首地址
第三个参数:发送数据的大小
第四个参数:发送标志,该0即可
第五个参数:目标地址
第六个参数:目标地址的大小
返回值:成功返回实际发送数据的大小,失败返回-1
接收消息的函数
函数功能:表示接受指定数据并提供来电显示功能
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
第一个参数:socket描述福
第二个参数:首地址(把接受到的数据存到哪里去)
第三个参书:接收的的数据大小
第四个参数:接收的标志,给0即可
第五个参数:保存发送方的通信地址(数据从哪里来)
第六个参数:发送方通信地址的大小
返回值: 成功返回接受到的数据大小,失败-1
TCP协议和UDP协议的比较
TCP协议
(1)概念: TCP 传输控制协议,面向连接的协议,传递的是数据流.
建立连接=> 传递数据=> 断开连接
(2)优点: 可以重发一切数据,可一保证数据的完整性和准确性,接收方可以通知发送方控制数据量
(3)缺点: 服务器压力比较大,资源占用率比较高
UDP协议
(1)概念: UDP 用户数据报协议,非面向连接的协议,发送数据报
(2)优点: 不需要全程保持连接,服务器压力比较小,资源占用率比较低
(3)缺点: 不会重发错误数据,不保证数据的准确性和完整性,接受方不会通知发送方进行数据量的控制
九、linux系统下的多线程编程
1.基本概念
目前主流的操作系统支持多进程,而在每一个进程的内部,可以支持多线程,也就是说线程是进程中的程序流
进程是重量级单位,每个进程都需要独立的内存空间,启动新的进程对资源的是很大的消耗,而线程是轻量级单位,线程共享所在的进程的内存空间,但是每个线程都有一块很小的独立栈区.
2.线程的相关函数
pthread_create 函数
函数功能: 主要用于曾在运行的进程中启动新的线程
第一个参数:用于存储新线程的ID
第二个参数: 线程的属性,给NULL即可,表示默认属性
第三个参数: 函数指针,表示新线程执行的处理函数
第四个参数: 作为第三个参数函数指针的参数
返回值: 成功返回0,失败直接返回错误编号
注意:
a. 编译链接时,记得加上选项 -pthread
b. 当启动新线程之后,子线程和主线程各自独立运行,每个线程内部的代码按照次序执行,也就是多线程之间相互独立,又互相影响,主线程结束,导致进程结束,而进程结束导致进程中所有的子线程随之结束
pthread_self 函数
函数功能: 主要用于获取正在运行的线程ID,通常都是成功的,通过返回值返回获取到的ID
pthread_join函数
函数功能: 主要用于等待一个线程的结束,并且获取退出码
第一个参数: 线程的ID
第二个参数: 二级指针,用于获取线程的退出码
注意: 该函数根据参数thread指定的线程进行等待,将目标线程终止时的退出状态信息拷贝到 *retval 这个参数指定的位置上
pthread_detach函数
函数的功能: 主要将参数指定的线程标记为分离状态,对于分离状态的线程来说,当该线程终止后,会自动将资源释放给系统,不需要其他线程的加入/等待,也就是说分离的线程无法被其他线程使用prhread_join进行等待
建议: 对于新启动的线程来说,要么使用pthread_detach设置为分离状态,要么使用pthread_join设置为可加入状态
pthread_exit 函数
函数的功能: 主要用于终止正在运行的线程,通过参数retval来带出线程的退出状态信息,在同一个进程中的其他线程可以通过调用pthread_join函数来获取退出状态信息
pthread_cancel 函数(线程的取消函数)
函数功能: 主要用于对参数指定的线程发送取消的请求,目标线程是否会被取消以及何时被取消,主要依赖两个属性:setcancelstate 和setcanceltype
pthread_setcancelstate 函数
函数功能: 主要用于设置新的取消状态,返回之前的取消状态
第一个参数: 新的取消状态
PTHREAD_CANCEL_ENABLE 允许被取消(默认状态)
PTHREAD_CANCEL_DISABLE 不允许被取消
第二个参数: 获取之前的取消状态,不想获取给NULL即可
pthread_setcanceltype 函数
函数功能: 主要用于设置新的取消类型,获取之前取消的类型
第一个参数:设置新的取消类型
PTHREAD_CANCEL_DEFERRED 延迟取消(默认的取消类型)
PTHREAD_CANCEL_ASYNCHRONOUS 立即取消
第二个参数:获取旧的取销类型,不获取则给NULL即可
3.线程的同步问题
(1)基本概念:
多线程共享进程中的资源,多个线程同时访问相同的共享资源时,需要相互的协调以避免出现数据的不一致和混乱问题,而线程之间的协调和通信叫做线程的同步问题
(2)线程同步的思想:
多线程访问共享资源时,应该进行串行,而不是并行
(3)线程同步的解决方法:
线程中提供了互斥量(互斥锁)的机制来实现线程的同步
(4)互斥量的使用步骤
a.定义互斥量
pthread_mutex_t mutext;
b.初始化互斥量
pthread_mutex_init(&mutex,0);
c.使用互斥量进行加锁
pthread_mutex_lock(&mutext);
d.使用共享资源
e.使用互斥量进行解锁
pthjread_mutex_unlock(&mutext);
f.如果不再使用,则销毁互斥量
pthread_mutex_destroy(&mutext);
4.使用信号量 实现线程的同步问题
(1)基本概念
信号量 - 本质就是一个计数器,用于控制同时访问共享资源的进程数/线程数
当信号量的值是1时,效果等同于互斥量.
(2)信号量的使用流程
a. 定义信号量
sem_t sem;
b. 初始化信号量
sem_init(&sem,0,最大值);//值 0 表示控制控制线程的个数
c. 获取一个信号量(减1)
sem_wait(&sem);
d. 使用共享资源
e. 释放一个信号量
sem_post(&sem);
f. 如果不再使用,则销毁信号量
sem_destroy(&sem);
5.使用条件变量实现线程的同步问题
生产者和消费者模型(重点掌握)
总结
以上是linux下的c开发的基础知识,使用的是linux系统下的API函数接口,熟练使用这些接口,可以实现相应的简单功能.