一、GNU编译器(gcc)
GNU = GNU Not Unix
GCC的基本特点
-
支持多种硬件架构
x86-64
Alpha
ARM
Motorola 68000
MIPS
PDP-10/11
PowerPC
System/370-390
SPARC
VAX -
支持多种操作系统
Unix
Linux
BSD(Unix一个版本)
Android
Mac OS X
iOS
Windows -
支持多种编程语言
C
C++
Objective-C
Java
Fortran
Pascal
Ada -
GCC的版本
早期:GCC = GNU C Compiler
现代:GCC = GNU Compiler Collection
gcc -v
构建过程
从源代码到可执行程序的构建过程
源代码(.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 - 运行可执行代码
文件名后缀
.h - C语言源代码头文件
.c - 预处理前的C语言源代码文件 > 可读文本
.i - 预处理后的C语言源代码文件 /
.s - 汇编语言文件 /
.o - 目标模块文件
.a - 静态库文件 > 不可读的二进制
.so - 动态库(共享库)文件 /
.out - 可执行文件 /
习惯上可执行文件不带后缀.
编译选项
gcc [ 选项 ] [参数] 文件1 文件2 …
例:gcc -E -o hello.i hello.c
| | | |
选项 选项 -o选项的参数 被处理文件
-o: 指定输出文件,如:gcc hello.c -o hello
-E: 预编译,缺省输出到屏幕,用-o指定输出文件,如:gcc -E hello.c -o hello.i
-S: 编译,将高级语言文件编译成汇编语言文件,如:gcc -S hello.c
-c: 汇编,将汇编语言文件汇编成机器语言文件,如:gcc -c hello.s
-g: 产生调试信息
-Wall:报告所有警告,如:gcc -Wall wall.c
-Werror:将警告作为错误处理,如:gcc -Werror werror.c
-x: 指定源代码的语言
xxx.c - C语言
xxx.cpp - C++语言
xxx.for - Fortran语言
xxx.java - Java语言
其他…
如:gcc -x c++ cpp.c -lstdc++ -o cpp
-O0/O1/O2/O3: 指定优化策略
O0:表示不优化,
O1:缺省优化,在空间和时间上选择尽可能折中的优化处理
O2: 牺牲空间换取时间
O3: 牺牲时间换取空间
头文件
头文件里写什么?
-
头文件卫士
a.h
/
b.h c.h -钻石包含
\ /
d.c
在编译d.c 的过程中会对a.h中的宏或数据类型等报重定义错
#ifndef _XXX
#define _XXX
只被编译器处理一次的代码
…
#endif
或:
#pragma once -
包含其它头文件
common.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
a.c
#include “common.h”
b.c
#include “common.h”
c.c
#include “common.h” -
宏定义
#define PI 3.14159 -
自定义类型
struct Circle {
double x, y, r;
}; -
类型别名
例如:
typedef struct Circle C;
typedef unsigned long long ULL;
typedef int *(*PFOO)(char *,short const *); -
声明外部变量
例:
global.c
double e = 2.71828;
…
global.h
extern double e /* = 2.71828 加注释*/;
… -
函数声明
double circleArea(double r);
一个头文件(.h)可能会被多个源文件(.c)包含,写在头文件里的函数定义也会因此被预处理器扩展到多个包含该头文件的源文件中,
并在编译阶段被编译到等多个不同的目标文件中,这将导致链接错误:multiple definition,多重定义。因此一般情况下,要
避免将函数定义写到头文件中。
例:
a.h
int foo(int a,int b){
return a+b;}
a.h
/ \
b.c c.c
| |
b.o c.o
\ /
d - 重定义报错,multiple definition ,因此一般情况下,要避免将函数定义写到头文件中。
对的做法
a.c
int foo(int a,int b){return a+b;}
a.h
extern int foo(int a,int b);
去哪里找头文件?
通过gcc -I<头文件的附加搜索路径>
头文件位于:
系统目录、/usr/include等目录
当前目录、执行gcc命令的目录
附加目录:通过gcc的-I (大写i) 选项指定的目录
尖括号包含:#include <calc.h> 搜索规则:附加目录 -> 系统目录
双引号包含:#include “calc.h” 搜索规则:附加目录 -> 当前目录 ->系统目录
头文件的系统目录:
/usr/include - 标准C库
/usr/local/include - 第三方库 如:QT 解压
/usr/lib/gcc/i686-linux-gnu/<版本号>/include - 编译器相关库
/usr/include/c++/<版本号> -标准C++:
代码框架:calc.h(声明)、calc.c 定义(实现)、math.c 调用(使用)
预处理指令
#include - 将指定的文件内容插至此指令处
#define - 定义宏
#undef - 删除宏
#if - 如果
#ifdef - 如果宏已定义
#ifndef - 如果宏未定义
#else - 否则,与#if/#ifdef/#ifndef配合使用
#elif - 否则如果,与#if/#ifdef/#ifndef配合使用
#endif - 结束判定,与#if/#ifdef/#ifndef配合使用
#error - 产生错误,提前终止预处理过程
#warning - 产生警告,继续预处理
#line - 显示指定下一行的行号
#pragma - 设定编译器的状态或者指示编译器的操作
#pragma once 一次性编译
#pragma GCC dependency 被依赖文件 -指定文件依赖,如果被依赖文件比包含该预处理指令文件新,则产生警告 a<-b
#pragma GCC poison <语法禁忌> 如果在代码中出现语言禁忌,则产生错误。例:#pragma GCC poison goto
#pragma pack(按几字节对齐:1/2/4/8) -字节对齐
#pragma pack() - 按缺省字节数对齐
代码:error.c演示error和warning
/*执行命令 gcc -o error error.c -DVERSION=4*/
#include <stdio.h>
//#define VERSION 3
#if (VERSION<3)
#error "版本太低!"
#elif (VERSION>3)
#warning "版本太高"
#endif
int main(void)
{
printf("版本:%d\n",VERSION);
return 0;
}
代码:line.c
#include <stdio.h>
int main(void){
int a =123,b = 456;
printf("%d+%d=%d\n",a,b,a+b);
FILE *fp = fopen("file1","r");
if(!fp){
#line 100
fprintf(stderr,"%d>无法打开文件!\n",__LINE__);
return -1;
}
fclose(fp);
if(!(fp = fopen("file2","r"))){
#line 200
fprintf(stderr,"%d>无法打开文件!\n",__LINE__);
return -1;
}
fclose(fp);
return 0;
}
预定义宏
无需自行定义,预处理器会根据事先设定好的规则将这些宏扩展成其对应的值。
__BASE_FILE__: 正在被编译的源文件名
__FILE__: 所在文件的文件名
__LINE__: 所在行的行号(受 #line影响)
__FUNCTION__: 所在函数的函数名
__func__ : 同__FUNCTION__
__DATE__ : 编译日期
__TIME__ : 编译时间
__INCLUDE_LEVEL__ : 包含层数,从0开始 a - 0
__cplusplus : C++有定义,C无定义
环境变量
C_INCLUDE_PATH(或者C_PATH) : 指定C语言头文件的附加目录:相当于gcc命令的-I 选项
env //查看当前变量
export 环境变量名 = 环境变量值
export 环境变量名 = $环境变量名:追加内容
CPLUS_INCLUDE_PATH :指定C++语言头文件的附加搜索路径,相当g++命令的-I 选项。
LIBRARY_PATH : 指定链接时查找库的路径
LD_LIBRARY_PATH : 运行时查找动态库的路径
定义头文件的三种方式:
#include “/…/…/xxx.h” - 一旦头文件路径发生变化,必须修改源程序,移植性差
export C_INCLUDE_PATH = $C_INCLUDE_PATH:/home/tarena/include
或者export C_PATH = $C_PATH:/home/tarena/include
同时维护多个项目,容易发生版本冲突
C_INCLUDE_PATH/CPATH=/…/…😕… - 易冲突
#include “xxx.h”
gcc -I/…/… … - 推荐
既不需要修改程序,也不会发生版本冲突,推荐使用此方法、
二、静态库和动态库
库
big.c ->big.o ->big
---
text.c ->text.o \
image.c ->image.0 |
video.c ->video.o | -> exe
audio.c ->audio.o |
network.c ->network.o |
encrypt.c ->encrypt.o /
text.c ->text.o \ 实现功能 使用功能
image.c ->image.0 |
video.c ->video.o | -> 库 + main.o <- main.c
audio.c ->audio.o | \ /
network.c ->network.o | ——
encrypt.c ->encrypt.o / exe
foo()
bar()
hum()
main()
单一模型:将程序中所有功能全部实现于一个单一的源文件内部。编译时间长,不易于维护和升级,不易于协作开发。
分离模型:将程序中的不同功能模块划分到不同的源文件中。缩短编译时间,易于维护和升级,易于协作开发。
a.c -> a.o \
foo() | -> ...
bar() |
b.c -> b.o /
hum()
a.o \
b.o | -> 库 + 其它模块 -> ...
c.o |
... /
静态库
-
静态库的本质就是将多个目标文件(.o)打包成一个文件
链接静态库就是将库中被调用的代码复制到调用模块中。
静态库占用较大的空间,库中代码一旦修改,所有使用该库的程序必须重新链接。
使用静态库的程序在运行无需依赖库,其执行效率高。 -
静态库的形式:libxxx.a
-
构建静态库:
.c -> .o -> .a
ar -r libxxx.a xxx.o yy.o z.o...
^ \_________/
|_________|
-
链接静态库:
gcc … -lxxx -L<库路径> …
gcc … -lxxx … <–LIBRARY_PATH
export LIBRARY_PATH=<库路径> -
调用静态库:运行时不需要调用静态库中的代码
操作:
整体制作和使用静态库
代码:static/
make static
步骤一:vim calc.h
#ifndef __CALC_H
#define __CALC_H
extern int add(int,int);
extern int sub(int,int);
#endif
步骤二:vim calc.c
#include "calc.h"
int add(int x,int y){
return x + y;
}
int sub(int x,int y){
return x - y;
}
步骤三:
把calc.h calc.c 看着是一个计算模块,calc.o
gcc -c calc.c
nm calc.o //查看一下内容
步骤四:显示模块 vim show.h
#ifndef __SHOW_H
#define __SHOW_H
extern void show(int,char,int,int);
#endif
步骤五:vim show.c
#include "show.h"
#include <stdio.h>
void show (int x,char o,int y,int z){
printf("%d%c%d=%d\n",x,o,y,z);
}
步骤六: show.c show.h 看着是一个显示模块,show.o
gcc -c show.c
步骤七:
把这两个模块打包成静态库
ar -r libmath.a calc.o show.o
步骤八:制作一个包含所有头文件 vim math.h
#ifndef __MATH_H
#define __MATH_H
#include "calc.h"
#include "show.h"
#endif
步骤九:扮演用户,使用这个库 vim main.c
#include "math.h"
int main(void){
int a =123,b =456;
show(a,'+',b,add(a,b));
show(a,'-',b,sub(a,b));
return 0;
}
步骤十:编译成.o 并链接math库
gcc -c main.c
export LIBRARY_PATH=.
gcc main.o -lmath -o main
运行测试:./main
动态库(共享库)
- 动态库和静态库最大的不同就是,链接动态库并不需要将库中被调用的代码复制到调用模块中,相反被嵌入到调用模块中的仅仅是被调用代码在动态库中的相对地址。
在调用模块实际运行时,再根据动态库的加载地址和被调用代码的相对地址,计算出该代码的绝对地址,读取代码的内容并运行之
如果动态库中的代码同时为多个进程所用,动态库的实例在整个内存空间中仅需一份,因此动态库也叫共享库或共享对象 .so (Shared Object, so)。
使用动态库的模块所占空间较小,即使修改了库中的代码,只要接口保持不变,无需重新链接。
因为在执行过程中,需要计算被调用代码的绝对地址,以及一些附带的额外开销,所以调用动态库会比调用静态库略慢一些。
使用动态库的代码在运行时需要依赖库,执行效率略低。 - 动态库的形式:libxxx.so
- 构建动态库:
编译选项:-fpic ,生成位置无关码
链接选项:-shared, 没有main函数的可执行程序
如:gcc -c -fpic xxx.c -> xxx.o
库内部的函数调用也用相对地址表示
```
gcc -shared -o libxxx.so x.o y.o z.o
^ \_________/
|_________|
```
-
链接动态库:
gcc … -lxxx -L<库路径>
export LIBRARY_PATH=<库路径>
gcc … -lxxx -
调用动态库:
运行时需要调用动态库中的代码,因此动态库必须位于 LD_LIBRARY_PATH环境变量所表示的路径中。
代码:shared/
cp -r …/day01/static/ ./stared
gcc -c -fpic calc.c
gcc -c -fpic show.c
gcc -shared calc.o show.o -o libmath.so //或直接生成gcc -shared -fpic calc.c show.c -o libmath.so
gcc -c main.c
gcc main.o -lmath -L. -o main
export LD_LIBRARY_PATH=.
gcc缺省链接共享库,可通过-static选项强制链接静态库。
因为相较于静态库而言,动态库具有明显的优势,因此gcc的链接器缺省使用动态库版本。如果一定要使用静态库版本参与链接
需要加上-static 链接选项 -
动态库的动态加载
Linux操作系统提供的一个用于动态加载动态库的动态库:dl
#include <dlfcn.h> \ 系统提供的针对动态dl库fuction 组成dlfcn
-ldl / 库的动态加载函数集 -
加载动态库
void* dlopen(const char* filename, int flag); //dl函数原型
成功返回动态库的句柄(handle),失败返回NULL。
------------------------
FILE* fp = fopen(...);
fread(fp...);
fwrite(fp...);
------------------------
filename - 动态库路径,也可以只给文件名,这种情况下函数将根据LD_LIBRARY_PATH环境变量搜索动态库
flag - 加载方式,可取以下值:
1. RTLD_LAZY - 延迟加载,使用动态中的符号时才加载,
2. RTLD_NOW - 立即加载
该函数所成功返回的动态库句柄唯一地标识了系统内核所维护的动态库对象,将作为后续函数调用的参数。
-
获取符号(函数名或比那里地址)
void* dlsym(void* handle, const char* symbol);
成功返回库中指定函数或(全局)变量地址,失败返回NULL。
handle - 动态库句柄
symbol - 符号 即(函数或全局变量)名
该函数所返回的函数指针是void*类型,需要强制类型转换为实际的函数指针类型才能调用。可见所获取符号的实际类型必须事先指定。 -
卸载动态库
int dlclose(void* handle);
成功返回0,失败返回非零。
handle - 动态库句柄
char* dlerror(void);
之前若有错误发生则返回错误信息字符串,否则返回NULL。
所卸载的动态库未必真的会从内存中立即消失,因为其它程序可能还需要使用该库
dlopen(...) -> handle1
dlopen(...) -> handle2
dlopen(...) -> handle3
dlclose(handle2)
动态库实例
引用计数2:
引用计数:0 ->释放内存
该函数一方面解除参数句柄和动态库实例之间的关联,是该句柄失效,另一方面会将动态库实例中的引用计数减1,
当其被减到0时 才会真的释放所占内存资源。
- 获取错误信息
char *dlerror(void);
之前有错误发生则返回指向错误信息字符串的指针,否则返回NULL.
代码:load.c
#include <stdio.h>
#include <dlfcn.h>
int main(void){
/*加载 ./shared/libmath.so动态库*/
void *handle = dlopen("./libmath.so",RTLD_NOW); //./libmath.so 是上一次生成 gcc -shared -fpic calc.c show.c -o libmath.so
if(!handle){
fprintf(stderr,"dlopen:%s\n",dlerror());
return -1;
}
//获取add函数的地址
int (*add)(int,int) = (int (*)(int,int))dlsym(handle,"add");
if(!add){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
//获取sub函数的地址
int (*sub)(int,int) = (int (*)(int,int))dlsym(handle,"sub");
if(!sub){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
//获取show函数的地址
void (*show)(int,char,int,int) = (void (*)(int,char,int,int))dlsym(handle,"show");
if(!add){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
//调用动态库中的函数
int a = 123,b = 456;
show(a,'+',b,add(a,b));
show(a,'-',b,sub(a,b));
//卸载动态库
if(dlclose(handle)){
fprintf(stderr,"dlclose:%s\n",dlerror());
return -1;
}
return 0;
}
编译命令:gcc load.c -ldl -o load
ldd ./load //查看不到动态加载
三、辅助工具
查看符号表:nm
语法:nm 目标模块/可执行程序/静态库/动态库
列出目标文件(.o)、可执行文件、静态库文件(.a)或动态库文件(.so)中的符号(函数或全局变量)
内容本:
T:正文段,函数的代码块,地址
U:正文段,对函数的调用,其代码块位与某个动态库中,无地址
D:数据段,全局变量
…
反汇编:objdump -S
语法:objdump -S 目标模块/可执行程序/静态库/动态库
将目标模块、可执行程序、静态库、或者动态库中的二进制形式的机器代码转换为字符形式的汇编代码,打印在屏幕上
消除冗余信息:strip
语法:strip 目标模块/可执行程序/静态库/动态库
去除目标模块(.o)、可执行程序、静态库(.a)或者动态库(.so)中的符号表、调试信息等非运行所必须的冗余信息,以缩减文件的大小
### 查看动态库依赖:ldd 语法:ldd 可执行程序/动态库 显示可执行程序或动态库所依赖的动态库库信息
错误号和错误信息
例:
FILE *fp = fopen(...);
fread(...,fp); //你不知道fp是否成功,fopen能否打开文件
处理数据
fwrite(...,fp);
fclose(fp);
当错误发生时:逃跑、提示在逃跑、自动纠错。
/ \
是否出错了? 出了什么错?
| |
返回值 错误号
\ /
查阅man手册页
- 通过函数的返回值表达错误
(1) 返回指针的函数:如:malloc、fopen、dlopen等,通常通过返回NULL指针表示错误
(2) 返回值属于特定值域的函数,通常用返回合法值域以外的值表示失败。
int age(char const* name) {
xxx
return 1000;
}
(3) 返回整数的函数:通过返回合法值域以外的值表示错误
不需要通过返回值输出信息的函数:返回0表示成功,返回-1或其他非零值表示失败。如果有数据输出,则通过指针型参数向调用者输出数据
int delete(char const* filename) {
....
return 0;
...
return -1;
}
- 通过错误号和错误信息表示产生错误的具体原因
标准库预定义了全局变量errno ,表示最近一次函数调用的错误编号
#include <errno.h> //声明errno、定义错误号宏
例:
FILE *fp = fopen(...);
if(!fp){
if(errno == EINVAL)
处理无效读写模式错误
else if (errno == ...)
处理... 错误
...
}
全局变量:errno,整数,标识最近一次系统调用的错误
-
将整数形式的错误号转换为字符串
#include <string.h>
char* strerror(int errnum); // 返回与参数错误号相对应的错误描述字符串指针#include <stdio.h>
void perror(const char* s); // 打印最近错误的错误信息,打印到标准错误设备上。
例:perror(“abcde”); //abcde: 错误描述字符串
或
%m // printf函数的%m标记被替换为最近错误的错误信息,%m格式化标志->得到描述字符串
参考代码:errno.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(void){
FILE *fp = fopen("file","r");
//FILE *fp = fopen("file","x");
if (!fp){
printf("fopen: %d\n",errno); //打印 ->fopen: 2
printf("fopen: %s\n",strerror(errno)); //打印 ->fopen: No such file or directory
perror("fopen"); //打印 ->fopen: No such file or directory
printf("fopen: %m\n"); //打印 ->fopen: No such file or directory
return -1;
}
//...
fclose(fp);
return 0;
}
注意:
函数在出错是会将一个大于零的整数作为错误号存放到全局变量errno中,但如果该函数执行成功,通常并不会将errno变量赋值0,
因此不能用errno是否为0作为函数成功失败的判断条件依据,而是根据函数的返回值判断其成功或者失败,只有在确定失败的前提下,才能根据
errno判断具体的失败原因。
返回值 = 函数调用(…);
if (返回值表示函数调用失败) {
根据errno判断发生了什么错误
针对不同的错误提供不同的处理
}
代码:iferr.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
void foo(void){
FILE *fp = fopen("none","r");
if (!fp)
return ;
//...
fclose(fp);
}
int main(void){
foo();
int *p = (int *)malloc(sizeof(int));
//int *p = (int *)malloc(sizeof(int)* 0xFFFFFFFF); //此时malloc报错 Cannot allocate memory
//if(errno){
if(!p){
perror("malloc");
return -1;
}
*p = 1234;
printf("%d\n",*p);
free(p);
return 0;
}
四、环境变量
研究:程序去操作环境变量
环境变量表
每个进程在系统内核中都有一个都有一个数据结构维护与该进程有关的审计信息,称为进程表项,
其中就包含了一张专属于该进程的环境变量表,环境变量表其实就是一个以NULL指针结尾的字符指针数组,
其中每个字符指针类型的数组元素都指向一个以空字符(‘\0’)字符串,该字符串形如:变量名 = 变量值,或“键=值”即一个环境变量。
进程1
进程2
... 用户空间
... 内核空间
进程表
进程表项 1
进程表项 2
各种ID
启动时间 、运行时间、启动命令、有限级、等等
文件描述符表
环境变量表
environ/envp-> * -> PATH= /bin:/usr/bin: ...\0
environ/evnp-> * -> SEHLL=/bin/bash\0
\ /
实际类型char **
...
NULL
env
全局变量:environ,需要自己在代码做外部声明。
environ-> | * |->AAA=aaa\0
| * |->BBB=bbb\0
| * |->CCC=ccc\0
|NULL|
取环境变量值
int main(int argc,char *argv[],char *envp[]){…}
所谓环境变量表就是一个以NULL指针结束的字符指针数组,其中的每个元素都是一个字符指针,指向一个以空字符结尾的字符串,该字符串就是形如”键=值”形式的环境变量。
argv-> * * * NULL
| | |
a.out -c b.c
参考代码:env.c
#include <stdio.h>
#include <stdlib.h>
void penv(char **envp){
printf("-----环境变量表-----\n");
while(envp && *envp)
printf("%s\n",*envp++);
printf("--------------------\n");
}
int main(int argc,char *argv[],char *envp[]){
penv(envp);
//或以下效果一样
extern char **environ;
penv(environ);
return 0;
}
访问调用进程的环境变量
-
根据一个环境变量的名称获取其值
char* getenv(char const* name);
成功返回变量名匹配的变量值,失败返回NULL。
name - 环境变量名,即等号左边的部分
例:printf(“%s\n”,getenv(“PATH”)); //打印PATH 环境变量的值 -
修改或新增环境变量
int putenv(char* string); //int putenv(char name_value);
成功返回0,失败返回-1。
string - 形如“键=值”或 “变量名”=变量值的字符串,若变量名不存在则新增该环境变量,若变量名已存在则修改其值
例:
putenv(“TEACHER=youge”);
penv(environ);
putenv(“TEACHER=Minwei”);
penv(environ);
第二种添加或修改环境变量
int setenv(const char name, const char* value,int overwrite);
成功返回0,失败返回-1。
name - 环境变量名,即等号左边的部分,若改变量名不存在,则新增该环境变量,若存在则根据overwrite 的值或修改或保留原值
value - 环境变量值,即等号右边的部分
overwrite - 当name参数所表示的环境变量名已存在,此参数取0则保持该变量的原值不变,若此参数取非0,则将该变量的值修改为value。
非零则覆盖,零则不覆盖
例:
setenv(“CITY”,“BeiJing”,0);
penv(environ);
setenv(“CITY”,“ShangHai”,0); //0不变
penv(environ);
setenv(“CITY”,“BeiJing”,1); //非零覆盖
penv(environ); -
删除环境变量
int unsetenv(const char* name);
成功返回0,失败返回-1。
name - 环境变量名,即等号左边的部分
例:
unsetenv(“TEACHER”);
penv(environ); -
清空环境变量
int clearenv(void);
成功返回0,失败返回-1。
例:
clearenv();
penv(environ);
printf(“environ: %p\n”,environ);
代码:env.c
#include <stdio.h>
#include <stdlib.h>
void penv(char **envp){
printf("-----环境变量表-----\n");
while(envp && *envp)
printf("%s\n",*envp++);
printf("--------------------\n");
}
int main(int argc,char *argv[],char *envp[]){
extern char **environ;
printf("%s\n",getenv("PATH"));
putenv("TEACHER=youge");
penv(environ);
putenv("TEACHER=Minwei");
penv(environ);
setenv("CITY","BeiJing",0);
penv(environ);
setenv("CITY","ShangHai",0);
penv(environ);
setenv("CITY","BeiJing",1);
penv(environ);
unsetenv("TEACHER");
penv(environ);
clearenv();
penv(environ);
printf("environ: %p\n",environ);
return 0;
}
五、内存
应用程序:根据业务逻辑选择合适的数据结构
STL :容器、迭代器、内存分配器
c++ :new 、delete、delete[] - > 用户层
c : malloc、calloc、realloc 、free /
POSIX :sbrk、brk \ 系统层
Linux :mmap、 munmap /
系统内核 :kmalloc、vmalloc \
硬件驱动 :get_free_page -> 内核层
硬件 :指令集、电路 /
进程映像(Process Maps)
程序时由可执行代码和全局数据组成的磁盘文件,运行程序时,需要将磁盘上的可执行程序文件加载到内存中,已使处理器
可以执行其中的代码,处理其中的数据,形成进程。进程在内存空间中的布局形成进程映像
每个进程都拥有独立的4G字节的虚拟内存,分别被映射到不同的物理内存区域。
内存映射和换入换出都是以页为单位,1页=4096字节。
4G虚拟内存中高地址的1G被映射到内核的代码和数据区,这1个G在各个进程间共享。用户的应用程序只能直接访问低地址的3个G虚拟内存,因此该区域被称为用户空间,而高地址的1个G虚拟内存则被称为内核空间。用户空间中的代码只能直接访问用户空间的数据,如果要想访问内核空间中的代码和数据必须借助专门的系统调用完成。
用户空间的3G虚拟内存可以进一步被划分进程映像地址从低地址到高地状依次为:
------------------ 4G-1
系统内核空间(1G)
高地址 ------------------ 3G-1
命令行参数
和环境变量
------------------
栈区:非静态局部变量
- - - - - - - -
v
3G 浮动变化区间
^
- - - - - - - -
堆区:动态内存分配(malloc函数族)
-----------------
BSS区:不带常属性无初值的全局和静态局部变量,
-----------------
数据区:不带常属性,非const型有初值的全局和静态局部变量
-----------------
只读常量:字面值常量,const型有初值的全局和静态局部变量
-------------------
代码区(正文段 只读):可执行指令 ,字面值常量,带有常属性且被初始化的全局变量和静态局部变量
低地址----------------- 0
参考代码:map.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> //IEEE制定的POSIX标准
const int const_global = 10; //常全局变量
int init_global = 10; //初始化全局变量
int uninit_global; //未初始化全局变量
int main(int argc,char *argv[],char *envp[]){
const static int const_static = 10; //常静态变量
static int init_static =10; //初始化静态变量
static int uninit_static; //未初始化静态变量
const int const_local =10; //常局部变量
int prev_local; //前局部变量
int next_local; //后局部变量
int *prev_heap = malloc(sizeof(int)); //前堆变量
int *next_heap = malloc(sizeof(int)); //后堆变量
const char *literal = "literal" ; //字面值常量
/*打印地址*/
printf("-------------高地址------------\n");
printf("----命令行参数和环境变量-------\n");
printf(" 环境变量:%p\n",envp);
printf(" 命令行参数:%p\n",argv);
printf("-------------栈区------------\n");
printf(" 后局部变量:%p\n",&next_local);
printf(" 前局部变量:%p\n",&prev_local);
printf(" 常局部变量:%p\n",&const_local);
printf(" | \n");
printf(" v \n ");
printf(" 预留空间 \n");
printf(" ^ \n");
printf(" | \n");
printf("-------------堆区------------\n");
printf(" 后堆变量:%p\n",next_local);
printf(" 前堆变量:%p\n",prev_heap);
printf("-------------BSS区------------\n");
printf(" 未初始化全局变量:%p\n",&uninit_global);
printf(" 未初始化静态变量:%p\n",&uninit_static);
printf("-------------数据区------------\n");
printf(" 初始化静态变量:%p\n",&init_static);
printf(" 初始化全局变量:%p\n",&init_global);
printf("-------------代码区------------\n");
printf(" 常静态变量: %p\n",&const_static);
printf(" 字面值常量:%p\n",literal);
printf(" 常全局变量:%p\n",&const_global);
printf(" 函数:%p\n",main);
printf("-------------低地址------------\n");
return 0;
}
六、系统调用
Unix应用的层次结构
(1)Unix系统的大部分功能都是通过系统调用实现的,如open、close等。
(2)Unix的系统调用已被封装成C函数的形式,但它们并不是C语言标准库的一部分。
(3)标准库函数大部分时间运行在用户态,但部分函数偶尔也会调用系统调用进入内核态,如:malloc、free等。
(4)程序员自己编写的代码也可以跳过标准库,直接使用系统调用,如brk、sbrk、mmap、munmap等,与操作系统内核交互,进入内核态。
应用程序--------------+
vi/emacs/gftp/firefox |
| |
标准库、第三方库 |
C/C++/Qt/X11 |
| | 用户态
系统调用<------------+ ------
内存态
系统调用的执行过程
应用程序
| / 将参数压入堆栈
定义在系统调用 - 将系统调用ID放入CPU的EAX寄存器
库中的C函数 \ 触发80H中断
用户时间和系统时间
一个进程在它的运行过程中,时而处于用户态,访问用户空间的资源,时而又会处于内核态,访问内核空间的资源,进程在这两种状态下的时间,分别
称为用户时间和系统时间
进程:
分配内存 ->存入数据 ->打开文件 ->计算数据->将结果写入文件->释放内存->关闭文件
分配内存 - malloc/sbrk/brk/mmap - 内核态(3)
|
存入数据 - *p=10;*q= 20; -用户态(2)
|
打开文件 -fp = fopen(…); - 内核态(1)
|
计算数据 - r = *p+*q; - 用户态(4)
|
睡眠 - sleep(10); - 挂起态(10)
|
将结果写入文件 -fprintf(fp,“%d”,r); - 内核态(5)
|
释放内存 - free/sbrk/brk/munmap -内核态(6)
|
关闭文件 -fclose(fp); -内核态(7)
用户时间 = 2+4 = 6
内核时间 = 3+1+3+6+7 = 22 - 墙钟时间
睡眠时间 = 10 /
命令:time 可执行程序
real --墙钟时间
user --用户时间
sys – 系统时间
总结:
- Linux系统内核提供了一套用于实现各种系统功能的子程序,谓之系统调用。程序编写者可以象调用普通C语言函数一样调用这些系统调用函数,以访问系统内核提供的各种服务。
- 系统调用函数在形式上与普通C语言函数并无差别。二者的不同之处在于,前者工作在内核态,而后者工作在用户态。
- 在Intel的CPU上运行代码分为四个安全级别:Ring0、Ring1、Ring2和Ring3。Linux系统只使用了Ring0和Ring3。
用户代码工作在Ring3级,而内核代码工作在Ring0级。一般而言用户代码无法访问Ring0级的资源,除非借助系统调用,使用户代码得以进入Ring0级,使用系统内核提供的功能。 - 系统内核内部维护一张全局表sys_call_table,表中的每个条目记录着每个系统调用在内核代码中的实现入口地址。
- 当用户代码调用某个系统调用函数时,该函数会先将参数压入堆栈,将系统调用标识存入eax寄存器,然后通过int 80h指令触发80h中断。
- 这时程序便从用户态(Ring3)进入内核态(Ring0)。
- 工作系统内核中的中断处理函数被调用,80h中断的处理函数名为system_call,该函数先从堆栈中取出参数,再从eax寄存器中取出系统调用标识,然后
再从sys_call_table表中找到与该系统调用标识相对应的实现代码入口地址,调用该函数,同时传入参数并获得返回值,设置errno并将处理结果逐层返回到用户代码中。
七、文件
文件系统 = 数据结构+管理软件
FAT/FAT32/NTFS
UFS/JFS/DFS/EXT2/EXT3
文件系统的物理结构
-
硬盘的物理结构:驱动器、盘片、主轴、磁头、控制器
-
磁表面存储器的读写原理
硬盘片的表面覆盖着薄薄的磁性涂层,涂层中含有无数微小的磁性颗粒,谓之磁畴。相邻的若干磁畴组成一个磁性存储元,以其剩磁的极性表示二进制数字0和1。为磁头的写线圈中施加脉冲电流,可把一位二进制数组转换为磁性存储元的剩磁极性。利用磁电变换,通过磁头的读线圈,可将磁性存储元的剩磁极性转换为相应的电信号,表示二进制数。 -
磁道和扇区
磁盘旋转,磁头固定,每个磁头都会在盘片表面画出一个圆形轨迹。改变磁头位置,可以形成若干大小不等的同心圆,这些同心圆就叫做磁道(Track)。每张盘片的每个表面上都有成千上万个磁道。一个磁道,以512字节为单位,分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位。 -
柱面、柱面组、分区和磁盘驱动器
硬盘中,不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数相等。
硬盘上的每个字节需要通过以下参数定位:
磁头号:确定哪个盘面
柱面号:确定哪个磁道 > 磁盘I/O
扇区号:确定哪个区域 /
偏移量:确定扇区内的位置
若干个连续的柱面构成一个柱面组
若干个连续的柱面组构成一个分区
每个分区都建有独立的文件系统
若干分区构成一个磁盘驱动器
文件系统的逻辑结构
磁盘驱动器:| 分区 | 分区 | 分区 |
分区:| 引导块 | 超级块 | 柱面组 | 柱面组 | 柱面组 |
柱面组:
| 引导块 | 柱面组 | i节点映 | 块位图 | i节点表 | 数据块集 |
| 副 本 | 信 息 | 射 表 | | | |
i节点号:431479
i节点
文件元数据
100 | 200 | 300
根据目录文件中记录的i节点编号检索i节点映射表,获得i节点下标,用该下标查i节点表,获得i节点,i节点中包含了数据块索引表,利用数据块索引从数据块集中读取数据块,即获得文件数据。
直接块:存储文件实际数据内容如:100、200、300、500
间接块:存储下级文件数据块索引表
(2)管理软件
文件名->目录(文件)->i节点号 ->i节点映射表 ->i节点索引 ->i节点表 ->i节点 ->数据库索引表 ->数据块
文件类型
Unix 系统文件类型的区分不是通过扩展名,而是通过文件i节点中的元数据,当用ls 命令显示目录中的文件列表时,会用特定的单个字符来
表示文件类型:
-
普通文件(-):可执行程序、代码文件、文本、图片、音频、视频、网页 、静态/动态库等,一个普通文件包含以线性字节组织的数据,通常也称为字节流,
Unix系统的文件没有更进一步的组织结构或格式,因此也不存在类似VMS系统中记录的概念。文件中的任何字节都可以被读或写,这些操作皆
始于某个处于特定位置的字节,该位置即当前文件偏移,亦称读写指针。 -
目录文件(d):该目录中每个硬链接名和i节点号的对应表,目录文件的本质也是一个普通文件,与一般普通文件的区别就是它仅仅存储文件名(字符串)和
i节点号(正整数)的映射。将目录文件中每一个这样的映射称为目录条目,也叫硬链接 -
符号链接文件(l):符号链接文件拥有自己独立的i节点和包含一个被链接文件路径字符串的文件内容。所有针对符号链接文件的读写操作,其实都是在读写
被其所链接的文件 -
管道文件§:有名管道,进程间通信(只有i节点和目录中的硬链接条目,i节点中只有元数据而没有数据块索引表,管道文件其实是一个内存中的内核对象
可用于在不同进程之间交换数据) -
套接字文件(s):也是本地套接字文件,进程间通信,本质上和管道文件没有区别,也是一种进程间通信的方式,只是因为其源自BSD关于套接字的API集,
故与SVR4Unix发生分歧。 -
块设备文件(b):设备驱动将字节数据(块)映射到可寻址设备上,用户程序可以按照任意顺序访问该字节数组中的任意字节,如硬盘。
-
字符设备文件©:设备驱动将一个个字节按顺序写入队列,用户程序从队列中按顺序依次读出一个个字节,如键盘。串行口。
文件的打开与关闭
打开/创建文件:在系统内核中建立一套数据结构,用于访问文件
- 打开已有的文件或创建新文件
#include <fcntl.h>
int open(const char pathname, int flags, mode_t mode);*
成功返回文件描述符,失败返回-1。
参数说明:
pathname - 文件路径
flags - 状态标志,可取以下值:
O_RDONLY - 只读
O_WRONLY - 只写 > 只选其一
O_RDWR - 读写 /
O_APPEND - 追加 可以和O_WRONLY或O_RDWR组合使用
O_CREAT - 创建,不存在即创建,已存在即打开,除非与以下两个标志之一合用,由此标志mode参数才有效。
O_EXCL - 排它,已存在即失败
O_TRUNC - 清空,已存在即清空,同时有O_WRONLY或O_RDWR
O_SYNC - 写同步,在数据被写到磁盘之前写操作不会完成,读操作本来就是同步的,此标志对读操作没有意义
O_ASYNC - 异步,在文件可读写时产生一个SIGIO信号,在对信号的处理过程中读写I/O就绪的文件,只能用于终端设备或网络套接字,而不能用于磁盘文件
O_NONBLOCK - 非阻塞,读操作不会因为无数据可读而阻塞,写操作也不会因为缓冲区满而阻塞,相反会返回失败,并设置特定的errno
mode - 权限模式,三位八进制:0XXX ->0 属主(拥有者)用户 属组(与拥有者同组的)用户 其它用户
__/ | _
拥有者用户 同组用户 其它用户
4: 可读 2: 可写 1: 可执行
例:0754
所创建文件的实际权限除了跟mode参数有关,还受权限掩码的影响。
mode=0666
umask=0002
权限=mode&~umask=0664
---------------------内存中的用户空间------------------------
int 文件描述符 = open(...);
read(文件描述符,...);
------------------------
FILE *文件指针 = fopen(...);
文件指针 ->
fread(文件指针,...);
--------------------- 内存中的内核空间--------------------------
进程表项
...
文件描述符表
|文件描述符标志 | 文件表项指针 | 0
|文件描述符标志 | 文件表项指针 | 1
|文件描述符标志 | 文件表项指针 | 2
... | ^
+-----------------------------+ |
| 文件描述符
v
文件表项
文件状态标志
文件读写位置
v节点指针
... |
+-----+
|
v
v节点
i节点内容
元数据
数据块索引表:数据块索引
...
-----------------磁盘上的文件系统--------------------
v节点
i节点内容
元数据
数据块索引表:数据块索引
关闭:释放打开文件过程中建立的数据结构
FILE* fp = fopen(“reame.txt”, “r”);
fread(fp, …);
创建新文件
int creat(const char* pathname, mode_t mode);
flags: O_WRONLY | O_CREAT | O_TRUNC
打开已有文件
int open(const char* pathname, int flags);
- 关闭文件
#include <unistd.h>
int close(int fd);
成功返回0,失败返回-1。
fd - 文件描述符
参考代码:open.c
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
int main(void){
int fd1 = open("open.txt",O_RDWR|O_CREAT|O_TRUNC,0666) ;
if(fd1 ==-1){
perror("open");
return -1;
}
printf("fd1: %d\n",fd1);
close(fd1);
int fd2 = open("open.txt",O_RDWR) ;
if(fd2 ==-1){
perror("open");
return -1;
}
printf("fd2: %d\n",fd2);
close(fd2);
//close(fd1);
return 0;
}
- I/O重定向
注意:无论是同一个进程中,还是在不同的进程中,多次通过open函数打开同一个文件,系统内核中只会有一个v节点,为
多个不同的文件描述符所共享,但是,每次打开所创建的文件表项却是独立的,对应不同的文件描述符,共享同一个v节点的指针。
对于每个进程,系统都会缺省打开三个文件描述符:
STDIN_FILENO (0) 0 // 标准输入,即键盘
STDOUT_FILENO (1) 1 // 标准输出,终端屏幕,有缓冲
STDERR_FILENO (2) 2 // 标准错误,终端屏幕,无缓冲
UC 标准C C++
文件描述符 文件指针 文件对象
数据类型 int FILE* iostream
标准输入 0 stdin cin
标准输出 1 stdout cout
标准错误 2 stderr cerr
文件描述符是用户程序和系统内核关于文件的唯一联系方式。
< 输入文件路径
1> 输出文件路径
2> 差错文件路径
close(STDIN_FILENO);
open(输入文件路径); //0
close(STDOUT_FILENO);
open(输出文件路径); //1
close(STDERR_FILENO);
open(差错文件路径); //2
参考代码redir.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
void redir(void){
close(STDIN_FILENO);
int fd = open("i.txt",O_RDONLY); //0 ->i.txt
//printf("%d ->i.txt\n",fd);
close(STDOUT_FILENO);
fd = open("o.txt",O_WRONLY|O_CREAT|O_TRUNC,0644); //1->o.txt
//printf("%d ->o.txt\n",fd);
close(STDOUT_FILENO);
fd = open("e.txt",O_WRONLY|O_CREAT|O_TRUNC,0644); //2->e.txt
//printf("%d ->e.txt\n",fd);
}
void resume(void){
close(STDIN_FILENO);
stdin = fopen("/dev/tty","r"); //0 -> 键盘
close(STDOUT_FILENO);
stdout = fopen("/dev/tty","w"); //1 - >屏幕
close(STDERR_FILENO);
stderr = fopen("/dev/tty","w"); //2 -> 屏幕
setbuf(stderr,NULL) ;
}
void doio(void){
int x,y;
scanf("%d%d",&x,&y); //从键盘读取数据,从标准输入设备读取数据。从文件描述符为0的文件读取数据
printf("%d+%d = %d\n",x,y,x+y); //向标准输出设备打印数据,向文件描述符为1的文件打印数据
malloc(0xFFFFFFFFFFFFFFFF);
perror("malloc");
}
int main(void){
doio();
redir();
doio();
resume();
doio();
return 0;
}
文件的读取和写入
-
写入文件:内存->文件 向指定文件写入字节流
#include <unistd.h>
ssize_t write(int fd, const void buf, size_t count);*
成功返回实际写入的字节数(0表示未写入),失败返回-1。
fd - 文件描述符
buf - 写入缓冲区
count - 期望写入的字节数 -
读取文件:内存<-文件
#include <unistd.h>
ssize_t read(int fd, void buf, size_t count);*
成功返回实际读取的字节数(0表示读到文件尾),失败返回-1。如果读写位置已经位于文件尾(文件中最后一个字节的下一个位置,即文件长度)该函数直接返回0
fd - 文件描述符
buf - 读取缓冲区
count - 期望读取的字节数,一般给buf缓冲区的大小
参考代码:wr.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void){
int fd = open("wr.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
if (fd == -1){
perror("open");
return -1;
}
const char *text = "Hello,World";
printf("写入内容: %s\n",text);
size_t towrite = strlen(text) *sizeof(text[0]);
ssize_t written = write(fd,text,towrite);
if(written == -1){
perror("write");
return -1;
}
printf("期望写入%lu字节,实际写入%ld字节.\n",towrite,written);
close(fd);
/*读出*/
if((fd = open("wr.txt",O_RDONLY) == -1)){
perror("open");
return -1;
}
char buf[1024] = {
};
size_t toread = sizeof(buf) - sizeof(buf[0]);
ssize_t readed = read(fd,buf,toread);
if (readed ==-1){
perror("read");
return -1;
}
printf("期望写入%lu字节,实际写入%ld字节.\n",toread,readed);
printf("读取内容: %s\n",buf);
printf("read函数返回%ld.\n",read(fd,buf,toread));
close(fd);
return 0;
}
- 读写二进制文件与读写文本文件
基于read和write函数读写文件都是面向二进制字节流的I/O操作:
内存:10100001 -> HLHLLLLH
文件:NSNSSSSH ->10100001
基于系统调用的文件读写本来就是面向二进制字节流的,因此对二进制读写而言,无需做任何额外的工作。如果要求文件中内容必须是可阅读的,
那么就必须通过格式化和文本解析处理二进制形式的数据和文本字符串之间的转换。
内存:10100001 -格式化 - >“161”
文件:“161” -解析- > 10100001
参考代码:binary.c、text.c
二进制形式存写:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void){
int fd = open("binary.dat",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(fd ==-1){
perror("open");
return -1;
}
char name[256] = "张飞";
if(write(fd,name,sizeof(name)) == -1 ){
perror("write");
return -1;
}
unsigned int age = 38;
if (write(fd,&age,sizeof(age)) ==-1){
perror("write");
return -1;
}
double salary = 20000;
if (write(fd,&salary,sizeof(salary)) ==-1){
perror("salary");
return -1;
}
struct Employee{
char name[256];
unsigned int age;
double salary;
} employee = {
"赵云",25,8000};
if (write(fd,&employee,sizeof(employee)) ==-1){
perror("write");
return -1;
}
close(fd);
if ((fd = open("binary.dat",O_RDONLY)) ==-1){
perror("open");
return -1;
}
if(read(fd,name,sizeof(name)) == -1){
perror("read");
return -1;
}
printf("姓名:%s\n",name);
if(read(fd,&age,sizeof(age)) == -1){
perror("read");
return -1;
}
printf("年龄:%u\n",age);
if(read(fd,&salary,sizeof(salary)) == -1){
perror("read");
return -1;
}
printf("工资:%g\n",salary);
if(read(fd,&employee,sizeof(employee)) == -1){
perror("read");
return -1;
}
printf("员工: %s %u %g\n",employee.name,employee.age,employee.salary);
close(fd);
return 0;
}
查看 binary.dat : hexdump -C binary.dat
以文本方式存写:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void){
int fd = open("text.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(fd ==-1){
perror("open");
return -1;
}
char name[256] = "张飞";
unsigned int age = 38;
double salary = 20000;
char buf[1024];
sprintf(buf,"%s %u %g\n",name,age,salary);
if (write(fd,buf,strlen(buf) * sizeof(buf[0])) == -1){
perror("write");
return -1;
}
struct Employee{
char name[256];
unsigned int age;
double salary;
} employee = {
"赵云",25,8000};
sprintf(buf,"%s %u %g\n",employee.name,employee.age,employee.salary);
if (write(fd,buf,strlen(buf) * sizeof(buf[0])) == -1){
perror("write");
return -1;
}
close(fd);
if ((fd = open("text.txt",O_RDONLY)) ==-1){
perror("open");
return -1;
}
memset(buf,0,sizeof(buf));
if(read(fd,buf,sizeof(buf) - sizeof(buf[0])) == -1){
perror("read");
return -1;
}
sscanf(buf,"%s %u %lf %s %u %lf",name,&age,&salary,employee.name,&employee.age,&employee.salary);
printf("姓名:%s\n",name);
printf("年龄:%u\n",age);
printf("工资:%g\n",salary);
printf("员工: %s %u %g\n",employee.name,employee.age,employee.salary);
close(fd);
return 0;
}
### 顺序访问与随机访问 ABCdef ^ ^ ^ 0 3 6 每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移。 文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int。 打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置。 每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备。 因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有各自的文件读写位置。
B. 随机访问
通过人为调整文件读写位置,以非顺序的方式读写文件的内容
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
成功返回调整后的文件读写位置,失败返回-1。
fd - 文件描述符
offset - 偏移(调整)量 —相对某个位置的字节数
whence:偏移(调整)量的相对位置,可取以下值:
SEEK_SET - 从文件开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从文件尾开始
lseek函数仅仅是修改文件表项中的文件读写位置,并不引发实际的I/O操作,速度很快。
lseek(fd, 10, SEEK_SET);
lseek(fd, -10, SEEK_END);
lseek(fd, 0, SEEK_CUR); // 返回当前读写位置
lseek(fd, 0, SEEK_END); // 返回文件总字节数
lseek(fd, -10, SEEK_SET); // 错误
lseek(fd, 10, SEEK_END); // 允许,空洞部分补0
参考代码:seek.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void){
int fd = open("seek.txt",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd == -1){
perror("open");
return -1;
}
const char *text = "Hello,World!";
if(write(fd,text,strlen(text)*sizeof(text[0])) == -1){
perror("write");
return -1;
}
/*Hello,World*/
if(lseek(fd,-6,SEEK_CUR) == -1){
perror("seek");
return -1;
}
/*Hello,World*/
off_t pos = lseek(fd,0,SEEK_CUR);
if(pos == -1){
perror("lseek");
return -1;
}
printf("当前读写位置:%ld\n",pos);
text = "Linux";
if(write(fd,text,strlen(text)*sizeof(text[0])) == -1){
perror("write");
return -1;
}
if(lseek(fd,8,SEEK_END) == -1){
perror("lseek");
return -1;
}
text = "<-This is a hole.";
if(write(fd,text,strlen(text)*sizeof(text[0])) ==-1){
perror("write");
return -1;
}
/*无法将读写位置至于文件头之前
if(lseek(fd,-8,SEEK_SET) == -1){
perror("lseek") ;
return -1;
}*/
off_t size = lseek(fd,0,SEEK_END);
close(fd);
return 0;
}
标准I/O和系统I/O
系统I/O:通过open、read、write和close等系统调用实现文件I/O。
标准I/O:通过fopen、fread、fwrite和fclose 等标注库函数实现文件I/O。
应用程序----------+
| |
v |
标准(库)I/O |
fopen/fwrite/fclose |
| |
v |
系统(库)I/O |
open/write/close <–+
参考代码:stdio.c、sysio.c
标准I/O
#include <stdio.h>
int main(void){
FILE *fp = fopen("stdio.dat","wb");
if(!fp){
perror("fopen");
return -1;
}
for(int i = 0 ;i<1000000;++i)
fwrite(&i,sizeof(i),1,fp);
fclose(fp);
return 0;
}
系统I/O
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void){
int fd = open("sysio.dat",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(fd == -1){
perror("fopen");
return -1;
}
for(int i = 0 ;i<1000000;++i)
write(fd,&i,sizeof(i));
close(fd);
return 0;
}
比较二个I/O:
查看标准I/O: time stdio
real 0m0.018s
user 0m0.010s
sys 0m0.007s
查看系统I/O : time sysio
real 0m0.973s
user 0m0.235s
sys 0m0.738s
结论:
当系统调用函数被执行时,需要在用户态和内核态之间来回切换,因此频繁执行系统调用函数会严重影响性能。标准库做了必要的优化,
内部维护一个缓存区,只在满足特定条件是才将缓冲区与系统内核同步借此降低执行系统调用的频率,减少进程在用户态和内核态之间
来回切换的次数,提高运行性能。
复制文件描述符
进程表项
文件描述符表
0 |文件描述符标志 | 文件表项指针 | ->文件表项
1 |文件描述符标志 | 文件表项指针 | ->文件表项
2 |文件描述符标志 | 文件表项指针 | ->文件表项
3 |文件描述符标志 | 文件表项指针 | ->文件表项
\—————————————/
复 | 制
V
——————————————
/ \
7 |文件描述符标志 | 文件表项指针 | ->文件表项
通过多次打开同一个文件所得到的可操作同一个文件的不同文件描述符,其各自拥有各自独立的文件表项,因此其中的读写位置也是独立的。但是通过
复制文件描述符所得到的可操作性同一个文件的不同文件描述符,同享同一个文件表项,因此其中的读写位置也是共用的。
#include <unistd.h>
int dup(int oldfd);
成功返回新文件描述符,失败返回-1。
oldfd - 旧(源)文件描述符
dup函数将oldfd参数所对应的文件描述符表条目复制到文件描述符表中第一个空闲条目中,同时返回该条目所对应的新文件描述符。
例:int fd2 = dup(fd); // fd2: 7
进程表项
文件描述符表
|文件描述符标志 | 文件表项指针 | 0
|文件描述符标志 | 文件表项指针 | 1
|文件描述符标志 | 文件表项指针 | 2
|文件描述符标志 | 文件表项指针 | 3 -> 文件表项->v节点
^
|文件描述符标志 | 文件表项指针 | 7 --------+
fd2(7)和fd(3)对应同一个文件表项,访问同一个文件。
dup函数将oldfd参数所对应的文件描述符表项复制到文件描述符表第一个空闲项中,同时返回该表项所对应的文件描述符。
close(fd);
close(fd2);
int dup2(int oldfd, int newfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
newfd - 目标文件描述符
dup2函数在复制oldfd参数所标识的源文件描述符表项时,会首先检查由newfd参数所标识的目标文件描述符表项是否空闲,若空闲则直接将前者复制给后者,否则会先将目标文件描述符newfd关闭,再行复制。
```
fd1 = open(“1.txt”, …); --> 文件表项
> v节点
fd2 = open(“1.txt”, …); --> 文件表项 /
fd1 = open("2.txt", ...); \
> 文件表项 -> v节点
fd2 = dup(fd1); /
```
参考代码:dup.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void){
int fd1 = open("dup.txt",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd1 == -1){
perror("open");
return -1;
}
printf("fd1: %d\n",fd1);
int fd2 = open("dup.txt",O_RDWR);
if(fd2 == -1){
perror("open");
return -1;
}
printf("fd2: %d\n",fd2);
int fd3 = open("dup.txt",O_RDWR);
if(fd3 == -1){
perror("open");
return -1;
}
printf("fd3: %d\n",fd3);
const char *text = "Hello,World!";
if(write(fd1,text,strlen(text)*sizeof(text[0])) ==-1){
perror("write");
return -1;
}
if(