课程简介:Unix系统高级编程
业务逻辑:根据业务需求,按照设计好的逻辑规则,处理数据,与系统无关。 系统访问:利用操作系统所提供的各种功能辅助业务逻辑的实现。
标准函数:scanf/printf - 源代码级兼容:与操作系统无关,在不同系统上都可以运行,屏蔽平台差异,在不同的平台上只需要重新编译即可调用对应平台的系统函数,不同平台的系统函数不一样,这样标准函数就 可以实现跨平台运行。有些语言程序可以做到二进制级别,直接在不同平台上面跑 ,但需要虚拟机做支持,例如java,但C与C++不借助虚拟机,可以直接在裸板上跑,不借助运行时系统。
系统函数:read/write - 接口级兼容:与系统相关,不同操作系统提供的系统函数不同
为啥学系统函数编程: 1.环境考虑:操作系统与硬件的不同,提供的接口不同,需要我们调用系统函数来用接口,标准库没这个接口 2 性能考虑:直接用系统函数实现代码的性能优于间接使用统一的标准函数性能 3 功能考虑:标准库函数可能没有例如开启进程,创建线程,网络等功能,需要Uinx系统函数提供这些功能
一、Unix系统简介
1.Unix系统的背景
1961-1969:史前时代 CTSS(Compatible Time-Sharing System,兼容分时系统),以MIT为首的开发小组,小而简单的实验室原型。 Multics(Multiplexed Information and Computing System,多路信息与计算系统),庞大而负责,不堪重负。 Unics(Uniplexed information and Computing System,单路信息与计算系统),返璞归真,走上正道。
1969-1971:创世纪 Ken Thompson,肯.汤普逊,Unix之父,B语言之父,内核用B语言+汇编语言开发, PDP-7,第一个Unix系统核心和简单应用。后来被移植到PDP-11平台,功能更加完善。
1971-1979:出谷纪 Dennis Ritchie,丹尼斯.里奇,C语言之父,用C语言重写了Unix系统内核, 极大地提升了Unix系统的可读性、可维护性和可移植性——Unix V7,第一个真正意义上的Unix系统。
1980-1985:第一次Unix战争 AT&T电报电话贝尔实验室:SVR4版本 加州大学伯克利分校:BSD版本+TCP/IP网络协议 DARPA,ARPANET(INTERNET) IEEE,国际电气电子工程师协会,POSIX为Unix内核和外壳制定了一系列技术标准和规范, 消除系统版本之间分歧,大一统的操作系统。
1988-1990:第二次Unix战争 AT&T+Sun IBM+DEC+HP 比尔.盖茨->Windows
1990-现在 1991,Linus Torvalds创建了Linux系统的内核 1993,Linux已达到产品级操作系统的水准 1993,AT&T将Unix系统卖给Novell 1994,Novell将Unix系统卖给X/Open组织 1995,X/Open将Unix系统捐给SCO 2000,SCO将Unix系统卖给Celdear——Linux发行商 Linux就是现代版本的Unix。
2.Linux系统的背景
类Unix操作系统,免费开源。不同发行版本使用相同的内核。 支持多种硬件平台:手机、路由器、视频游戏控制器、个人电脑、大型计算机等等。 隶属于GNU工程。GNU = GNU Not Unix。 受GPL许可证限制:如果发布了可执行的二进制代码,就必须同时发布可读的源代码, 并且在发布任何基于GPL许可证的软件时,不能添加任何限制性条款。
3.Linux系统的版本
早期版本:0.01,0.02,...,1.00 旧计划:1.0.1,...,2.6.0 ---(A.B.C)格式 A - 主版本号,内核大幅更新 B - 次版本号,内核重大修改,奇数测试版,偶数稳定版 C - 补丁序号,内核轻微修改
新计划:A.B.C-D.E D - 构建次数,反映极微小的更新 E - 描述信息 rc/r - 候选版本 smp - 支持对称多处理器 EL - Rad Hat的企业版本 mm - 试验新技术 ... cat /proc/version 命令查看
4.Linux系统的特点:遵循GNU/GPL许可证;开放性;多用户;多任务; 设备无关性;丰富网络功能;可靠的系统安全;良好的可移植性
5.Linux的发行版本
Ubuntu - 大众化,简单易用 Linux Mint - 新潮前位 Fedora - Red Hat之一的桌面版本 openSUSE - 华丽 Debian - 自由开放 Slackware - 朴素简洁,简陋 Red Hat - 经典,稳定,企业应用定制,常用于企业服务器,支持全面
二、GNU编译器(gcc)
1.GCC的基本特点
1)支持多种微处理器的硬件架构的交叉编译 x86-64,Alpha,ARM,PowerPC,SPARC,VAX...
2)支持多种软件操作系统 Unix,Linux,BSD,Android,Mac OS X,iOS,Windows
3)支持多种编程语言 C,C++,Objective-C,Java,Fortran,Pascal,Ada
4)GCC的版本 查看命令gcc -v
2.程序的构建过程
源代码(.c)-预编译(头文件,宏等扩展 ) ->编译(得到汇编码.s) ->汇编(得到目标码.o)- ->链接(自动与标准库链接,默认得到可执行代码a.out)
代码:hello.c vi hello.c - 编写源代码 gcc -E hello.c -o hello.i - 预编译(编译预处理) 输出文件的拓展名称任意取 gcc -S hello.i - 获得汇编代码(hello.s) gcc -c hello.s - 获得目标代码(hello.o) gcc hello.o -o hello - 获得可执行代码(hello) ./hello - 运行可执行代码
3.文件名后缀 .h - C语言源代码头文件 \ .c - 预处理前的C语言源代码文件 > 可读文本 .s - 汇编语言文件 /
.o - 目标文件 \ .a - 静态库文件 > 不可读的二进制 .so - 共享(动态)库文件 / .out - 可执行文件 /
4.编译选项
gcc [选项] [参数] :文件1 文件2 .... 选项与参数可以缺省
-o: 指定输出文件 如:gcc hello.c -o hello ---一步到位法
-E: 预编译,缺省输出到屏幕,用-o指定输出文件 如:gcc -E hello.c -o hello.i
-S: 编译,将高级语言文件编译成汇编语言文件 如:gcc -S hello.c 不指定输出默认获得汇编代码(hello.s)
-c: 汇编,将汇编语言文件汇编成机器语言文件(二进制文件) 如:gcc -c hello.s 不指定输出默认获得目标代码(hello.o)
-Wall:产生全部警告(不加话小问题就不会报警告)---加上更加严谨暴露潜在风险 如:gcc -Wall wall.c 代码:wall.c
#include <stdio.h> foo(){} int main(void) { printf("%d\n", foo()); return 0; }
编辑
-Werror:将警告作为错误处理,得不到编译结果 如:gcc -Werror werror.c 代码:werror.c
#include <stdio.h> int* foo(void) { int local = 10; return &local;//返回野指针 } int main(void) { printf("%d\n", *foo()); return 0; }
编辑
-x: 指定源代码的语言 xxx.c - C语言 xxx.cpp - C++语言 xxx.for - Fortran语言 xxx.java - Java语言 ... gcc -x c++ cpp.c -lstdc++(接到c++库) -o cpp 代码:cpp.c(-x指定源代码后,后缀名无所谓了)
-O0/O1/O2/O3: 指定优化等级,O0不优化,缺省O1优化 在运行速度时间与程序大小空间上权衡
#include <iostream> using namespace std; int main(void) { cout << "Hello, World!" << endl; return 0; }
编辑
5.头文件
1)头文件里写什么?
a 头文件卫士:防止重定义 #ifndef XXX_ #define XXX_ ... #endif
b 包含其它头文件
c 宏定义 #define PI 3.14159
d 自定义类型 struct Circle { double x, y, r; };
e 类型别名 typedef struct Circle C;
f 外部变量声明 extern double e;
g 函数声明 double circleArea(C c); a.h / \ b.c c.c | | b.o c.o (链接成d可执行文件) \ / d - 重定义错误(如果将函数定义写到头文件的话) 因为一个头文件可能会被多个源文件包含,写在头文件里的函数定义也会因此被预处理器扩展到多个包含该头文件的源文件中,并在编译阶段被编译到多个不同的目标文件中,这将导致链接错误:multiple definition,多重定义。
规规矩矩一步一步构建法与一步到位法:
多个c文件变成可执行文件,前提是有并且只能一个文件里面有主函数。并且头文件虽然没有gcc,但在对应的.c文件里面必须包含这个头文件,这是规矩。
2)编译器去哪里找头文件? gcc -I <头文件的附加搜索路径> #include <my.h> 先找-I指定的目录,再找系统目录。 #include "my.h" 先找-I指定的目录,再找当前目录,最后找系统目录。
头文件的系统目录: /usr/include - 标准C库 /usr/local/include - 第三方库 /usr/lib/gcc/i686-linux-gnu/5.4.0/include - 编译器库 代码:calc.h、calc.c、math.c gcc math.c calc.c -o math -I ./
vim cal.h #ifndef _CALC_H #define _CALC_H double add(double, double); #endif // _CALC_H
vim cal.c #include "cal.h" double add(double a, double b) { return a + b; }
#include <stdio.h> #include "cal.h" int main(void) { printf("%g\n", add(1.23, 4.56)); return 0; }
6.预处理指令(编译即以后阶段就消失了,只停留在预处理阶段,节约性能,减轻一上去编译负担)
#include - 将指定的文件内容插至此指令处
#define - 定义宏(文件代码里面定义) -DPI = 3.14 在编译时候通过命令指定宏 #undef - 删除宏 :这条指令以后宏就失效了
#if - 如果 #ifdef - 如果宏已定义 #ifndef - 如果宏未定义 #else - 否则,与#if/#ifdef/#ifndef配合使用 #elif - 否则如果,与#if/#ifdef/#ifndef配合使用 #endif - 结束判定,与#if/#ifdef/#ifndef配合使用
#error - 产生错误,结束预处理 #warning - 产生警告,继续预处理 代码:error.c gcc error.c -DVER=1
#include <stdio.h> #if (VER < 3) #error "版本太低!" #elif (VER > 3) #warning "版本太高!" #endif int main(void) { printf("版本:%d\n", VER); return 0; }
编辑
#line - 指定行号 代码:line.c
#include <stdio.h> int main(void) { int x = 1000; printf("%d\n", __LINE__); #line 100 printf("%d\n", __LINE__); printf("%d\n", __LINE__); return 0; }
编辑
#pragma - 设定编译器的状态或者指示编译器的操作 代码:dep.c、pragma.c 语法:#pragma GCC dependency 被依赖文件 ---(提示你依赖的文件更新了,当前文件要不要修改的提示)
#include <stdio.h> #pragma GCC dependency "dep.c" int main() { return 0; }
编辑
#pragma GCC poison 语法禁忌(自己设定哪些语法不给用,报错)
#include <stdio.h> #pragma GCC poison goto float int main(void) { float f; double f; printf("1\n"); goto label; printf("2\n"); label: printf("3\n"); return 0 }
编辑
#pragma pack(按几字节对齐:1/2/4/8))---影响结构体的对齐策略 语法: #pragma pack() - 按缺省4字节数对齐
#include <stdio.h> #pragma GCC dependency "dep.c" #pragma GCC poison goto float int main(void) { //float f; double f; printf("1\n"); //goto label; printf("2\n"); label: printf("3\n"); struct A { //默认4字节对齐,即每个变量内存字节数至少是4的整数倍 double d; // 8 char c; // 1 int i; // 4 short h; // 2 }; // ddddddddcxxxiiiihhxx printf("%u\n", sizeof(struct A)); // 20 #pragma pack(1) struct B { double d; // 8 char c; // 1 int i; // 4 short h; // 2 }; // ddddddddciiiihh printf("%u\n", sizeof(struct B)); // 15 #pragma pack(2) struct C { double d; // 8 char c; // 1 int i; // 4 short h; // 2 }; // ddddddddcxiiiihh printf("%u\n", sizeof(struct C)); // 16 #pragma pack() struct D { double d; // 8 char c; // 1 int i; // 4 short h; // 2 }; // ddddddddcxxxiiiihhxx printf("%u\n", sizeof(struct D)); // 20 return 0; }
7.预定义宏:无需自行定义,预处理器会根据系统事先设定好的规则将这些宏扩展成其对应的值。
__BASE_FILE__: 正在被处理的源文件名(c文件) __FILE__: 所在文件名(h头文件或者c文件) __LINE__: 所在行的行号 __FUNCTION__: 所在函数的函数名 __func__: 同__FUNCTION__ __DATE__: 预处理日期 __TIME__: 处理时间 __INCLUDE_LEVEL__: 包含层数,从0开始 __cplusplus: 这个预定义宏在C++有定义,C无这个宏定义 因此用来检测按照c还是c++编译的,按照c++编译就会出一个长整型结果
代码:print.h、predef.h、predef.c
vim print.h #ifndef _PRINT_H #define _PRINT_H #include <stdio.h> void print(void) {//z=只有一个头文件包含就可以在头文件里面写函数体 printf("__BASE_FILE__ : %s\n", __BASE_FILE__); printf("__FILE__ : %s\n",__FILE__); printf("__LINE__ : %d\n",__LINE__); printf("__FUNCTION__ : %s\n",__FUNCTION__); printf("__func__ : %s\n",__func__); printf("__DATE__ : %s\n",__DATE__); printf("__TIME__ : %s\n", __TIME__); printf("__INCLUDE_LEVEL__ : %d\n",__INCLUDE_LEVEL__); #ifdef __cplusplus printf("__cplusplus : %ld\n",__cplusplus); #endif } #endif
vim predef.h #ifndef _PREDEF_H #define _PREDEF_H #include "print.h" #endif // _PREDEF_H
#include "predef.h" int main(void) { print(); return 0; }
编辑
8.环境变量:在进程向下文中保存的一些数据:键(功能,是什么)=值(内容)。 env命令查看shell进程环境变量
C_INCLUDE_PATH -------C语言头文件的附加搜索路径,相当于-I选项。 CPATH同C_INCLUDE_PATH
通过export命令设置C_INCLUDE_PATH环境变量值就不用-I选项了,但退出出终端这个设置的环境变量就消失了。可以通过写入用户配置的bash文件就永久有效了
编辑
写入bash
编辑
最后一行加上
同理
CPLUS_INCLUDE_PATH-------C++语言头文件的附加搜索路径,相当于-I选项。
LIBRARY_PATH-------链接库搜索路径
LD_LIBRARY_PATH-------加载库路径
三种搜索路径方法比较 双引号法: #include "xxx.h" 相对路径(当前路径) -I指定法: gcc -I/.../... ... - 推荐 环境变量法:C_INCLUDE_PATH=/.../...:/... - 同名头文件在源文件引用时候易冲突,优先找到第一个环境变量头文件路径
三、库
以前单一模型:将程序中所有功能全部实现于一个单一的源文件内部。编译时间长,不易于维护和升级,不易于协作开发。
现在分离模型:将程序中的不同功能模块划分到不同的源文件中。缩短编译时间,易于维护和升级,易于协作开发。 a.c -> a.o \ foo() | -> ... b.c -> b.o / hum() / 链接不耗时间,编译耗时间,因此分文件编译,编译好的o文件再链接封装成库。否者每次产品更新要把单一模型的c文件编译十分耗时间,拆分成多个c不但易于协作管理也利于编译。 a.o \ b.o | -> 库 .a+ 其它开发者模块.o -> 上线的软件程序 c.o | ... /
1.静态库 静态库的本质就是将多个目标.o文件打包成一个文件。 链接静态库就是将库中被调用的代码复制到当前调用模块中。 使用静态库的程序通常会占用较大的空间,库中代码一旦更新修改,所有使用该库的程序必须重新ar链接进行更新一下。 使用静态库的程序在运行无需依赖库,将库删除了都可以运行,其执行效率高。用户直接下载可执行程序就行。
静态库的形式:libxxx.a 构建静态库: ar -r libxxx.a x.o y.o z.o 使用静态库: gcc ... -lxxx
如果库路径不是缺省的话,就指定路径
gcc ... -lxxx -L<库路径> 或者export LIBRARY_PATH=<库路径>gcc ... -lxxx 或者写入bash