linux系统编程
- 1. gcc四个阶段
- 2. 动态库 静态库
- 2.1 制作静态库
- 2.2 头文件守卫
- 2.3 制作动态库
- 3. gdb调试工具
- 基础指令
- 其他指令
- 4. Makefile
- 最终成果
- 一个小作业
- 5. 系统编程阶段
- open函数
- read write函数
- 阻塞和非阻塞
- lseek函数设置文件读写偏移量
- 传出参数和传入参数(c常用)
- 5.2 文件系统操作
- 目录项dentry(dir entry )和inode
- stat获取文件属性(文件对应的inode中的属性)
- link和unlink及隐式回收
- dup(int fd1)和dup2(int fd1, int fd2)做文件重定向
- 6.3 父子进程
- exec族函数
- 孤儿进程
- 僵尸进程
- waitpid回收子进程
- 6.4进程通信(PLC)
- pipe+fd管道通信
- fifo管道通信
- 通过mmap进行通信
- 5.5 信号
- 常用信号
- 阻塞信号集和未决信号集
- 学习kill,alarm,settimer
- 学习signal,sigaction
- 6.6 多线程(待补)
- 6. 杂项
- (杂)一个进程在内存中的内存分布图
- (杂)c语言野指针问题
- (杂)c语言函数指针
- (杂)关于man
1. gcc四个阶段
hello.c 预处理—(-E) —>hello.i -编译—(-S)—> hello.s 汇编—(-c)—>hello.o链接—(无参数)—>hello.out
-
预处理:展开头文件,删除空白行
-
编译:检查语法规范,把c变成汇编语言(耗时最长,因为需要一行一行翻译)
-
汇编:将汇编语言翻译成机器指令
-
链接:数据段合并,地址回填
- *.c源文件(c语言)
- *.i经过整合后的源文件(c语言)
- *.s(经过编译后的汇编语言)
- *.o(机器指令,又称o结尾为目标文件)
ps:-o是指定文件名,不是链接
其他编译参数 | 对应参数 |
---|---|
指定头文件所在目录(若源码与头文件在同级目录不需要 ) | -I |
预处理、编译、汇编 | -c |
预处理、编译 | -S |
添加gdb的调试文件 | -g |
给程序中动态注册宏定义 | -D |
显示所有warning信息 | -Wall |
ps:动态注册宏有啥用
#ifdef HELLO//如果定义了HELLO这个宏
#define HI = 20
#endif
int main()
{
printf("------%d", HI);//假设这是我们的一个打印的调试信息
}
//如果我们编译的是时候是 gcc hello.c -o hello -D HELLO(=任何数子)
//那么我们这行打印信息就可以正常输出
//加入我们写了几万行代码,有很多用来调试的打印信息,我们可以debug的时候—D HELLO,发布的时候不加,那么经过预处理后程序中就不会调试信息,这样省去我们一个一个删除
2. 动态库 静态库
原理上,动态库比静态库的的运行速度低(动态库的程序运行的时候还要去内存里面找要调用的函数),但是更加节省资源(程序的体积小)。
- 应用场景
动态库: 时间要求低,空间要求高
静态库: 时间要求高,空间要求低的核心程序中
2.1 制作静态库
使用静态库可以省去程序编译时间(这部分是耗时最长的),但其实现在编译器都比较快不缺这点时间。
ps:静态库命名规则 lib库名.a(.a就是静态库,.so是动态库)
将.c生成.o文件
- 使用ar工具制作静态库
ar rcs lib库名.a 制作成库的目标文件.o
# 例如:ar rcs libmymath.a div.o add.o
# 可见静态库其实是通过.o文件组合而成,因此静态库就是二进制文件
-
将自己制作的静态库链接进源代码中
gcc test.c libmymath.a -o test # 注意顺序 源码在前,库在后,事实上我测试了一下库在前也行
-
测试是否可以运行
./test
2.2 头文件守卫
如果我们的源代码中没有声明静态库中的函数
#include <stdio.h>
int main()
{
int a = 10, b = 10;
printf("%d+%d=%d\n", a, b, add(a, b));
printf("%d/%d=%d", a, b, div(a, b));
return 0;
}
- 新版编译器已经不支持隐式声明
error: call to undeclared function 'add'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
- 并且隐式声明只能声明返回值是int类型的函数
但是如果每次在我们自己源代码里面加头文件会很麻烦
#include <stdio.h>
int add();
int div();
int main()
{
int a = 10, b = 10;
printf("%d+%d=%d\n", a, b, add(a, b));
printf("%d/%d=%d", a, b, div(a, b));
return 0;
}
因此我们一般这样解决
#include <stdio.h>
#include "mymath.h"
int add();
int div();
int main()
{
int a = 10, b = 10;
printf("%d+%d=%d\n", a, b, add(a, b));
printf("%d/%d=%d", a, b, div(a, b));
return 0;
}
- mymath.h头文件中的内容
#ifndef _MYMATH_H_ //如果未定义宏_MYMATH_H_则执行以下语句
#define _MYMATH_H_ //定义宏_MYMATH_H_
int add(int, int);
int div(int, int);
#endif
综上所述:制作静态库不仅仅是制作库文件,还要帮忙制作头文件
2.3 制作动态库
动态库因为是动态引用的,在内存中的地址是延迟绑定的,而静态库的函数可以直接在生成目标文件的时候拥有自己的地址,比如main = 1000
,add = main + 100
,在内存中的时候加入main = 1000
那么 add = 1000 + 100
;
我们可以看一下汇编文件:
# 静态库中的函数add
callq 400604<add>
# 动态库中的函数printf, @plt就是我们需要
callq 400410<printf@plt>
官方介绍@plt
@plt(Procedure Linkage Table)是一个特殊的数据结构,用于在动态链接库(shared library)中保存函数调用的地址。它是在Linux和Unix-like系统中使用的一种机制。
当程序运行时,如果需要调用动态链接库中的函数,它并不直接使用函数的地址,而是通过@plt引用间接调用该函数。@plt表中存储了函数调用的入口点,也就是函数的PLT条目。每个PLT条目实际上是一个跳转指令,其作用是在第一次调用时将控制权转移到动态链接器(dynamic linker),由动态链接器负责解析函数的地址并更新PLT条目,以便后续的函数调用可以直接跳转到函数的地址。
@plt的存在有以下几个好处:
- 延迟绑定(Lazy Binding):由于PLT条目最初是指向动态链接器的跳转指令,而不是具体的函数地址,所以函数的地址直到真正被调用时才会被解析和更新。这样可以延迟函数地址的解析,提高程序启动速度。
- 共享库的更新:如果共享库发生变化,例如被更新或替换,那么只需要更新动态链接器中的PLT条目即可,无需重新编译可执行文件。
- 符号重定位(Symbol Relocation):如果函数的地址发生变化,例如共享库被加载到不同的内存地址,动态链接器会负责更新PLT条目中的函数地址,以确保后续的函数调用可以正确执行。
总结来说,@plt是一个用于实现动态链接调用的机制,它提供了一种灵活的方式来处理函数的地址解析和更新,以支持动态链接库的使用。
结论
生成动态库和生成静态库所有步骤都一样,唯一不一样的是要生成@plt的函数地址
如何生成带@plt
的目标文件呢?
.c文件生成.o文件的时候 指定一个参数 -fPLC
来生成与位置无关的代码
gcc -c add.c -o add.o -fPIC
官方介绍-fPIC
gcc-fPIC是GCC编译器的一个选项,用于生成位置无关代码(Position Independent Code,简称PIC)。在编译动态链接库(.so文件)时,使用该选项可以确保代码可以被加载到任意内存位置而不影响其正确性。该选项会生成适用于共享库的代码,而不是生成可执行文件。
具体来说,-fPIC选项告诉GCC生成与位置无关相关的代码,这样代码中的数据地址引用将使用相对地址而不是绝对地址。这样,在将共享库加载到内存时,操作系统可以在内存的任意位置加载它,并通过重定位表来解析代码中的地址引用,从而使得共享库可以在不同进程之间共享
-
# 制作目标文件 gcc -c add.c -o add.o -fPIC gcc -c div.c -o div.o -fPIC
-
# 制作动态库(这里用gcc工具制作动态库而不是ar工具) gcc -shared -o lib库名.so add.o div.o
-
# 链接(注意这里是库名mymath而不是libmymath.so ) gcc test.c -o a.out -l 库名 -L 动态库所在的目录 -I 函数声明头文件所在目录
-
# 运行 ./a.out
-
# a.out运行后会报错说找不到动态库 我们 -l -L 是 链接器 干的事情 但程序运行的时候有 程序运行链接器 链接器 和 程序运行链接器 毫无关系 为了让执行./a.out的时候能找到这个动态库,我们需要 : 方法1: export LD_LIBRARY_PATH=动态库所在目录(建议使用绝对路径) ps:为了让环境变量LD_LIBRARY_PATH永久生效,我们需要将其写入终端配置文件.bashrc 方法2: 拷贝自定义动态库到 /lib(标准C库所在的目录位置, 滥竽充数一把) 方法3: 1)sudo vim /etc/ld.so.conf 在文件末尾追加上动态库所在的目录 || echo“目录”>>/etc/ld.so.conf 2)sudo ldconfig -v #使配置文件生效
ldd a.out 查看文件内含有的动态库文件以及文件所在的目录
3. gdb调试工具
基础指令
前置条件: gcc -g main.c -o main
获得含有调试表的可执行程序1
进入gdb:gdb a.out
- list (l)列出源码,根据源码指定行号设置断点。eg:l 1, 从第一行开始列出,之后一直l即可
- b:设置断点,eg:b 20在20行的位置设置断点
- run®:运行程序(如果出现段错误,也可以用run来找)
- next(n):执行下一条代码(会越过函数)
- step(s):执行下一条代码(会进入函数)
- print§ + i:查看变量i的值
- continue:继续执行后续代码知道遇到下一个断点
- quit:退出当前gdb调试
其他指令
-
start:和run不同,run会在断点处停下,start会一行一行执行源代码
-
finish:结束当前函数,假如进入了库函数无法退出来(因为库函数步骤很多而且相互调用),可以用finish退出
-
until + 行号:直接跳到某一行,也可以用来退出库函数
-
set + args:run或者start前这个命令可以设置程序的main函数的参数
1 ./a.out aa bb cc # 一般情况下这样设置main函数参数 2 gdb a.out # 静入gdb后 set aa bb cc # 设置main函数参数 3 run aa bb cc # 在程序运行到主函数之前 运行这个命令就可以设置main函数参数
-
Info(i) b:查看断点的信息
-
b 41 if i = 5:条件断点,如果变量i=5的时候才在41行设置断点
-
ptype + 变量名:查看变量的类型
-
backtrace(bt) 查看函数的调用栈帧和层级关系(其实就是查看当前的各个函数的栈帧)
-
frame(f) + 栈帧编号:切换gdb指令的context,方便指令的执行。例如:ptype在solve函数的时候查看不了main函数内的变量,所以需要切换栈帧
类似需要context的命令还有print -
display + 变量名:设置跟踪变量,一直跟踪打印变量的值,因为使用print只能打印一次,每次都要执行一边print很麻烦
-
undisplay + 变量的编号:取消跟踪变量
4. Makefile
-
makefile最基本的三个规则:
-
(一个规则的)格式
目标文件:依赖文件 (tab)为了生成目标要执行的命令
-
目标文件最后修改的时间必须晚于依赖条件最后生成的时间,否则就会执行更新目标文件的操作(这样使目标文件是由最新的依赖条件生成的)
-
依赖条件如果不存在,会在文件中寻找新的规则产生依赖
-
-
makefile的默认制定文件的第一行的目标文件为终极目标,也可以人为使用ALL指定终极目标
6 ALL:test # 人为指定终极目标,否则makefile默认test.o为终极目标,只会执行一个命令gcc -c test.c -o test.o 5 test.o:test.c 4 gcc -c test.c -o test.o 3 test:test.o add.o div.o 2 gcc test.o add.o div.o -o test 1 div.o:div.c 0 gcc -c div.c -o div.o 1 add.o:add.c 2 gcc -c add.c -o add.o
-
两个函数
-
wildcard (通配符)
-
patsubst(将变量里面的字符串含第一个参数的部分替换成第二个参数)
src = $(wildcard *.c)# add.c div.c test.c obj = $(patsubst %.c, %.o, $(src))# add.o div.o test.o
-
-
make clean
ALL:test src = $(wildcard *.c) obj = $(patsubst %.c, %.o, $(src)) test.o:test.c gcc -c test.c -o test.o test:$(obj) gcc $(obj) -o test div.o:div.c gcc -c div.c -o div.o add.o:add.c gcc -c add.c -o add.o clean: # 这个目标 没有 依赖文件 -rm -rf $(obj) # -rm的目的是及时这个命令没有找到文件也不要显示报错信息顺序执行即可
-
3个自动变量:
$@:在规则的命令中,表示规则中的目标
$^:在规则的命令中,表示规则中依赖条件中的所有条件
$<:在规则的命令中,表示规则中依赖条件中的第一个条件(在模式规则中它可以将依赖条件列表中依赖依次取出来,套用模式规则,类似while循环)
$(obj):./obj/%.o:./src/%.c gcc -c $< -o $@ -I $(myinc) $(myargs)
-
伪目标:.PHONY
告诉make我不用你去生成clean文件,这是一个伪目标,但是你要帮我实现这个目标下面的命令
当makefile所在目录下存在clean或者all文件后,运行
make clean
会显示make: `clean' is up to date.
因此我们需要在规则中加上一个.PHONY:clean ALL
这样当前目录下是否存在clean文件都会执行clean规则下的命令-rm -rf $(obj) test
-
模式规则
./obj/%.o:./src/%.c gcc -c $< -o $@ -I $(myinc) $(myargs)
-
静态模式规则
$(obj):./obj/%.o:./src/%.c gcc -c $< -o $@ -I $(myinc) $(myargs)
-
make命令其他参数
-n:模拟执行make或者makeclean命令
-f:指定文件执行make命令
make -f 我自己起的文件名字
直接加文件名:make会将makefile里面的终极目标目标
ALL
替换成make 后面跟着的文件名
最终成果
ALL:test
src = $(wildcard ./src/*c)
obj = $(src:./src/%.c=./obj/%.o)# 也可以用函数obj = $(patsubst ./src/%.c, ./obj/%.o $(src)),但是我觉得前者更好用
myargs = -g -Wall
myinc = ./inc
$(obj):./obj/%.o:./src/%.c
gcc -c $< -o $@ -I $(myinc) $(myargs)
test:$(obj)
gcc $^ -o $@
clean:
-rm -rf $(obj) test
.PHONY:clean ALL
ps:脚本文件里面的变量都是字符串形式
一个小作业
SRC=$(wildcard *.c)
OBJ=$(SRC:%.c=%)
ALL:$(OBJ)
$(OBJ):%:%.c
gcc $< -o $@
clean:
-rm -rf $(OBJ);
.PHONY:clean ALL
5. 系统编程阶段
我们使用的函数都是系统内核中的函数封装的, 比如系统内核真正打开文件的函数是sys_open
但是给我们系统编程的时候用的是open
函数, open
函数上面再封装一层就是c语言里面用的fopen
函数
open函数
通过manpage(在vim界面用2K调到函数的定义)查看open函数的用法
#include<unistd.h> // 以前是需要这个头文件的,但是我macos上看不需要也可以
#include <stdio.h>
#include<fcntl.h>
int main()
{
int fd;
fd = open("f.txt", O_RDONLY|O_CREAT, 0611);// 通过2K或者man 2 open查看open函数的参数
//int
//open(const char *path, int oflag, ...);
printf("fd = %d", fd);
return 0;
}
#include <stdio.h>
#include<fcntl.h>
#include <sys/errno.h>// errno是系统自带的, 会记录当前程序的error类型, 还可以通过char *strerror(errno)
int main()
{
int fd;
fd = open("f.txt", O_RDONLY);
printf("fd = %d, erronumber = %d", fd, errno);
//printf("\nerrro信息为%s:", strerror(errno));
return 0;
}
read write函数
实现自己的cp命令
用read函数读,write函数写
read返回值:读入的字节数
write返回值:写入的字节数
perror可以改变程序运行错误的时候的输出语句,当然系统也有自带的
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int size = 0;
int fd1 = open(argv[1], O_RDONLY);
if (fd1 == -1)
{
//perror("读取文件失败"); exit(1);
}
int fd2 = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, 0664);
if (fd2 == -1)
{
perror("打开写入文件失败"); exit(1);
}
char buf[1024];
while((size = read(fd1, buf, 1024)) != 0)
{
if (size < 0)
{
perror("写入文件失败"); break;
write(fd2, buf, size);
}
}
close(fd1);
close(fd2);
return 0;
}
经过测试, 当设置一个一个字符读入的时候不一定有库函数fread快,因为在内存和内核中都有一个缓冲区用来预读入缓输出(这是操作系统优化的机制),这个缓冲区无用户定义的,用户自己定义的是char buf[1024]
这种.
-
预读入:系统在读的时候会尽量多读, 把缓冲区弄满
-
缓输出:系统会在cpu空闲的时候进行输出操作
-
?那什么时候使用自己定义的库函数呢,当用户需要及时的输出的时候,就不能只在cpu空闲的时候才输出,需要立即输出的时候用自己写的逻辑.
#define N = 1
char buf[N];
while((size = read(fd1, buf, N)) != 0)
{
if (size < 0)
{
perror("写入文件失败"); break;
write(fd2, buf, size);
}
}
0 1 2分别对应stdin stdout stderror
所以我们打开的文件fd都是从3开始的,并且一个程序最多打开的文件的数量上限是1024个文件(下标是1023)
阻塞和非阻塞
读写一个常规文件不会阻塞(只是时间问题而已),读写网络文件或设备文件会阻塞(最常见的设备文件tty-终端文件,如何取消阻塞
通过open("fileName", O_NONBLOCK|O_RDONLY)
打开即可
-
open函数关闭设备文件的阻塞功能,自己模拟阻塞
int fd1 = open("/dev/tty", O_NONBLOCK|O_RDONLY);//设置非阻塞 try again: int n = read(STDIN_FILENO, buf, 10);//STDIN_FILENO,标准输入是1 if (n < 0) { if (errno!=EAGAIN)//EWOULDBLOCK也行 { perror("这不是阻塞error这是打开文件error"); exit(1); } else { perror("这是阻塞error"); write(STDOUT_FILNO, "please try again", strlen("please try again")); goto tryagain;//重新尝试 } } close(fd1);
-
fcntl函数设置文件状态
int flag = fcntl(fd1, F_GETFL); if (flag == -1) perror("获取文件信息失败"), exit(1); flag |= O_NOBLOO_;flag是一串二进制数字 int ret = fcntl(fd1, F_SETFL, flag); if(ret == -1) perror("设置文件属性失败"), exit(1);
lseek函数设置文件读写偏移量
off_t lseek(int fd, off_t offset, int whence)
fd: 文件描述符。
offset: 偏移量,以字节为单位。
whence: 用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):
SEEK_SET:读写偏移量将直接指向 offset 字节位置处(从文件头部开始算);
SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处, offset 可以为正、也可以为负,如果是正数表示往后偏移, 如果是负数则表示往前偏移;
SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果 是负数则表示往前偏移。
返回值:成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生错误将返回-1,并设置errno值
-
linux下文件的读和写共用一个读写偏移量
#include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> int main() { int fd1 = open("lseek1.txt",O_RDWR|O_CREAT, 0644); char str[]="1111111122ssss"; char buf[100]; int ret1 = write(fd1, str, strlen(str)); //printf("写入lseek1文件%d字节\n", ret1); lseek(fd1, 0, SEEK_SET);//经过写以后读写偏移量移到了文件fd1的末尾,这里需要充值读写偏移量为0否则read就读不到任何东西 int ret2 = read(fd1, buf, ret1); //printf("读入了%d\n", ret2); int ret3 = write(STDOUT_FILENO, buf, ret2); //printf("写入了%d\n", ret3); close(fd1); return 0; }
-
lseek扩容
lseek(fd1, 9, SEEK_END); int ret1 = write(fd1, "\0", 1);//只偏移不写上一个字符是不能扩容的
-
lseek获取文件大小(野路子,但很多人这么写)
int size = lseek(fd1, 0, SEEK_END);//lseek返回值:成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置; printf("%d", size);
正常扩展文件大小: int truncate(const char* path, off_t length),成功返回值为0
int ret = truncate("test.txt", 10); printf("%d", ret);
path是const类型,说明该路径是只读的
-
od-tcx查看文件的十六进制形式, od-tcd查看文件的十进制形式
传出参数和传入参数(c常用)
传出参数
int a(int * p, struct stat *p, struct student *p)//借用指针,c一个函数可以返回很多参数
b()
{
int ret1;
int ret2;
struct stat ret3;
struct student ret4;
ret1 = a(&ret2, &ret3, &ret4);//这a函数可以给b返回4个结果, ret2,3,4就称为返回参数,即参数充当返回值,但是他并没有替代返回值
}
传入参数
char *strcpy(const char *src, char *dst)//src就是一个传入参数,用const修饰,只能读取这个参数里面的值, 不可以修改
5.2 文件系统操作
目录项dentry(dir entry )和inode
回顾:
file1是源文件,file2是新建的链接文件
软链接ln -s file1 file2.path
file2中记录的是file1的文件路径,因此如果想随意移动文件file2建议链接的是绝对路径,可以通过readlink
读取一个软连接文件的内容
硬链接ln file1 file2.path
file2 在系统中的的inode号和file1的inode号一样,当一个inode被链接数为0的时候就会被覆盖重写
(是重写不是被清空,因此我们格式化了手机手机里的数据还是可以找回的,只要将文件名和inode链接上即可,但我们格式化后再下载别的东西手机里的数据就找不回来了,inode已经被重写)
inode:存储文件信息以及文件在磁盘中存放位置的结构体,
dentry目录项:也是一个结构体{文件名, inode号}; 文件里面的东西是数据,目录里面的就是目录项z
stat获取文件属性(文件对应的inode中的属性)
struct stat { /* when _DARWIN_FEATURE_64_BIT_INODE is defined */ dev_t st_dev; /* ID of device containing file */ mode_t st_mode; /* Mode of file (see below) */ nlink_t st_nlink; /* Number of hard links */ ino_t st_ino; /* File serial number */ uid_t st_uid; /* User ID of the file */ gid_t st_gid; /* Group ID of the file */ dev_t st_rdev; /* Device ID */ struct timespec st_atimespec; /* time of last access */ struct timespec st_mtimespec; /* time of last data modification */ struct timespec st_ctimespec; /* time of last status change */ struct timespec st_birthtimespec; /* time of file creation(birth) */ off_t st_size; /* file size, in bytes */ blkcnt_t st_blocks; /* blocks allocated for file */ blksize_t st_blksize; /* optimal blocksize for I/O */ uint32_t st_flags; /* user defined flags for file */ uint32_t st_gen; /* file generation number */ int32_t st_lspare; /* RESERVED: DO NOT USE! */ int64_t st_qspare[2]; /* RESERVED: DO NOT USE! */ };
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
int main(int argc, char *argv[])//argc是命令行总的参数个数;argv[]是argc个参数,其中第0个参数是程序的全名,以后的参数。命令行后面跟的用户输入的参数。
{
struct stat a;//这个结构体包含了文件inode中的信息
int ret = stat(argv[1], &a);//返回0代表运行成功
printf("%s文件的大小是%lld",argv[1], a.st_size);//获取文件大小的正规军
return 0;
}
lstat和stat功能一样,但是lstat不具备穿透符号链接的功能,比如我想查看一个链接文件file1的信息,stat会查询它链接的文件file2的inode,而lstat可以查看file1文件的信息
link和unlink及隐式回收
-
link就是硬链接命令的函数形式
-
link和unlink实现mv命令
link(arg[1], arg[2]); unlik(arg[1]);
-
unlink只是让文件具备了被释放的条件,如果该文件的硬链接数=0,没有目录项对应后,才会被清除,但清除也不是立即清除,操作系统会等到所有打开该文件的进程结束后才会关闭该文件,并且挑一个时间将该文件释放.
//常人写法-------------- int fd1 = open("1.TXT", O_RDWR|O_CREAT, 0644); write(fd1, "1234567", 7); //发生阻塞,类似字符串常量更改 p[3] = 'H'; unlink(fd1);//程序的末尾再进行文件的回收和关闭工作 close(fd1); //真正的写法---------------- int fd1 = open("1.TXT", O_RDWR|O_CREAT, 0644); unlink(fd1); write(fd1, "1234567", 7);//上面unlink了但是这里依旧可以写入,此时写入的是系统的写入缓冲区/ //fd1依旧存在的原因是操作系统会等到所有打开该文件的进程结束后才会关闭该文件,这里还没有close(fd1),fd1还有效 //发生阻塞,类似字符串常量更改 p[3] = 'H'; close(fd1); //问题:如果段错误那么fd1没回收,文件描述符最大就是1023怎么办,后面的程序就没有fd用了怎么办? //答:系统会在程序结束的时候帮我们回收,你看我们平常STDIN_FILNO和STDOUT_FILENO,STDERROR_FILENO也没有回收 //原因:程序发生了段错误,后面的close虽然无法执行,但是程序运行结束了,系统会进行隐式回收,所有该进程打开的文件会被关闭,申请的内存空间会被释放(malloc函数); //需要注意的是,隐式回收只有在程序结束系统才会帮我们回收,因此我们平常不能借助这个特性写代码,因为服务器上的代码是24小时不宕机的
-
隐式回收见上面
-
利用opendir + readdir + stat实现ls命令
#include <stdio.h> #include <sys/stat.h> #include <unistd.h> #include <dirent.h> #include <string.h> void isFile(char *name); void read_dir(char *dir, void (*func)(char *name)) { char path[256]; struct dirent *sdp; DIR *dp = opendir(dir); if (dp == NULL){ perror("open file error");return;} while ((sdp = readdir(dp)) != NULL) { if (strcmp(sdp->d_name, ".") != 0 && strcmp(sdp->d_name, "..") != 0) { sprintf(path, "%s/%s", dir, sdp->d_name); (*func)(path); } } closedir(dp); return; } void isFile(char *name) { int ret = 0; struct stat *sd; ret = stat(name, sd); if (ret != 0) { perror("get file stat error"); return; } if (S_ISDIR(sd->st_mode)) { read_dir(name, isFile); } printf("%10s\t\t%lld\n", name, sd->st_size); return ; } int main(int argc, char *argv[]) { if (argc == 1) isFile("."); else isFile(argv[1]); return 0; }
dup(int fd1)和dup2(int fd1, int fd2)做文件重定向
int fd2 = dup(fd1);//把fd1指向的文件复制给fd2,即fd2现在也指向fd1所指向的那个文件了,返回的fd2是当前可用文件描述符的最小值 int fd3 = dup2(fd1, fd2);// 把fd1所指向的文件复制给fd2,即fd2现在也指向fd1所指向的那个文件了.返回值fd3 = 传的参数fd2 int fd1 = open(argv[1], O_RDWR); dup2(fd1, STDOUT_FILENO); printf("-----------8");//不会输出到终端,而是输出到fd1所指向的文件argv[1]里.
dup(int fd1)和dup2(int fd1, int fd2)做文件重定向
int fd2 = dup(fd1);//把fd1指向的文件复制给fd2,即fd2现在也指向fd1所指向的那个文件了,返回的fd2是当前可用文件描述符的最小值
int fd3 = dup2(fd1, fd2);// 把fd1所指向的文件复制给fd2,即fd2现在也指向fd1所指向的那个文件了.返回值fd3 = 传的参数fd2
int fd1 = open(argv[1], O_RDWR);
dup2(fd1, STDOUT_FILENO);
printf("-----------8");//不会输出到终端,而是输出到fd1所指向的文件argv[1]里.
- fcntl函数实现dup
int fcntl(int fd, int cmd,......args); cmd:F_DUPFD; int fd2 = open(argv[1], O_RDWR); int newfd = fcntl(fd2, F_DUPFD, 0);//文件描述符0被占用,返回>0的最小可用的文件描述符 int newfd2 = fcntl(fd2, F_DUPFD, 7);//文件描述符7未被占用,返回的newfd2 = 7; write(newfd, "123", 3);//可以直接写入文件argv[1]
6.3 父子进程
从这章开始就是速通了,我感觉你要用到的时候再去学,不然学了就忘了,等以后用到再来看
https://www.yuque.com/docs/share/431f5331-68c4-40c7-9956-b2303e4723fc?# 《01 Linux相关编程》
https://www.yuque.com/docs/share/5bf472ac-ff22-4195-8c04-f41715ce8f24?# 《02 文件与目录》
https://www.yuque.com/docs/share/49c16316-465c-4477-81b5-a2401109d4c3?# 《03 进程与PIC》
https://www.yuque.com/docs/share/bfa0b7e2-3c08-435b-9b3f-e30f941cc962?# 《04 信号与多线程》
前置知识:
fork后子进程和父进程的全局变量全部共享,但是如果对全局变量进行修改的时候子进程会自己复制一份自己的
即 读时共享,写时复制.
exec族函数
- bash下执行./a.out其实就是fork一个子进程 去执行./a.out
exec后会进入另一个程序 text data,但是进程pid不变,因此后续代码一旦执行就说明报错了
孤儿进程
- 父进程死亡后,子进程变成孤儿进程后归init进程管,ctrl c无法关闭子进程,因为ctrl c是把命令发给shell,但是子进程现在在后台不归shell管。
僵尸进程
- 子进程变成僵尸进程后父进程一直没结束,子进程无法关闭,并且因为父进程还在子进程也无法归init管,不然init就可以回收这个进程。ps -aux后僵尸进程会用中括号括起来(看电影的时候演员名字也会用中括号括起来)
waitpid回收子进程
- WNOHANG wait_pid 父进程不会阻塞等待子进程结束继续运行后续代码,返回0表示没有子进程被回收
6.4进程通信(PLC)
前置知识:linux万物皆文件,其中普通文件、目录、链接真正占用磁盘空间,其他字符 块 管道 套接字是伪文件占用内存空间
pipe+fd管道通信
有血缘关系的进程通过fork共享同一个pipe的fd2,因此可以用同一个fd[2]变量去读写管道文件.
但是没有血缘关系的进程就要通过fifo来通信, 但是fifo使用和普通文件一样,只不过fifo是伪文件在内存里面,IO速度比普通文件快
fifo管道通信
- 通过mkfifo在主存上创建一个管道文件,进程们直接通过这个管道文件进行通信,但是fifo是伪文件在内存里面,IO速度比普通文件快
- 读管道文件的时候有阻塞特性,但是用普通文件实现plc的时候read并不会阻塞,第一次读到0就是0,并不会等待别的进程往普通文件中写数据
- 阻塞是文件的特性,并不是read的特性
通过mmap进行通信
- mmap作用是把磁盘上的一个文件映射到内存的堆上,IO变快
- mmap的坑很多,建议上网cv,能用就行
- 记得挂载完再取消挂载
- mmap也可以匿名通信,我们发现经常要新建一个文件再删除这个文件很麻烦,不如直接创建一个匿名文件进行通信,但是匿名文件通信无血缘关系的进程无法拿到相同的fd(文件描述符),因此只适用于有血缘关系的进程
5.5 信号
信号这一章的要求:
了解常用的信号,每个信号对应的名字和产生信号的事件,信号对应的事件产生后会导致信号的发送,但不一定保证信号可以递达
常用信号
阻塞信号集和未决信号集
- 可以操作阻塞信号集,但是不能操作未决信号集,但可以打印未决信号集
- 如何在一个进程中操作阻塞信号集
学习kill,alarm,settimer
学习signal,sigaction
6.6 多线程(待补)
6. 杂项
(杂)一个进程在内存中的内存分布图
上网搜,很多
(杂)c语言野指针问题
//stat的第二个参数均为传出参数,作用相当于返回值
struct stat *sd;
ret = stat(name, sd);
//和
struct stat sd;
ret = stat(name, &sd);
//的区别,为啥后者可以正常运行
存在着重要的区别。
第一行定义了一个指向struct stat类型的指针sd,但并没有为它分配内存空间。因此,当你尝试通过stat函数给它赋值时,这个指针变量指向的位置是未知的,这会导致程序发生未定义行为,很可能导致程序崩溃或出现"trace trap"错误。
第二行定义了一个struct stat类型的变量sd,并使用&操作符获取了它的地址。这样就可以将stat函数获取到的文件状态信息存储到这个变量所代表的内存位置中。
因此,第二种写法是正确的,因为它为struct stat类型的变量sd分配了内存空间,并将其地址传递给了stat函数,使得stat函数可以将文件状态信息存储到正确的位置
char c;//一个字符
char c[];//一个字符数组,相当于一个字符串(但是c中没有string类型,因此用char[]表示一个字符串),单独一个c是其字符数组的首地址
char *c;//指向一个字符的地址,字符数组单独一个c其实就是一个char类型的指针
char **c;//一个字符串数组,相当与char[][]和char* c[];
//可以这么理解, 一个二级指针c指向一个一级指针char*数组的首地址,
// 一个一级指针指向一个字符数组的首地址,
// 字符数组相当于一个字符串
总结:看到char*就是char[],看到char[]就是一个字符串
一级指针和二级指针
#include <stdio.h>
int main()
{
int a = 1, b = 2;
int *p = &a;
//p指针有自己的地址&p, 但是一级指针变量p存的是普通变量a的地址,*p是去 指针变量p存的值(a的地址)对应的内存空间 取出相应的数据(即a的值)
int **p2 = &p;
//p2指针有自己的地址&p2, 但是二级指针变量p2存的是一级指针变量p的地址,*p2是去 指针变量p2存的值(p的地址)对应的内存空间 取出相应的数据(即a的地址)
printf("a的地址=%p\np的地址%p\np2的地址%p\np的内容%p\np2的内容%p\n*p的内容是:%d\n*p2的内容是:%p\n**p2的内容是:%d\n", &a, &p, &p2,p, p2,*p, *p2, **p2);/
//*p2= &b; //改变p2指针指向的地址空间存的数据(即把p指针的值变成b的地址)
//printf("p指针现在指向的地址是%p\n, p);(查看p指针的值是否变成b的地址)
*p = 3;//改变p指针指向的地址空间存的数据(即把变量a的所在地址的存储空间的值改成3,即把a的值改成3)
printf("a现在的值是%d\n", a);//查看变量a的值是否被改变
char c[] = "12334";
printf("%p\n", c);
printf("%p", c + 1);
return 0;
}
(杂)c语言函数指针
语法:
//1.基本语法介绍
int (*p)(int,int) //有参数,有返回值的函数
void (*p)(int,int) //有参数,无返回值的函数
void (*p)() //无参数,无返回值的函数
void (*p)(void)
//2. 自己定义一个函数指针的 变量类型
typedef void (*sig_t)(int);//定义sig_t代表 void(*)(int)类型
//sig_t是一个我们自定义的变量类型,它是一个函数指针,该类型的变量存储着一个函数的地址(因此才叫指针嘛),该函数返回值为void类型,参数列表:int
//3. 复杂版本函数声明(一般不这么写)
void (*signal(int signum, void (*sig_t)(int)))(int);
//signal是最外层的函数名
//signal函数的返回值是一个函数指针,
//返回的函数指针指向一个函数A,函数A的返回值为void,函数A的参数列表:int
//signal的参数列表:int,sig_t
//其中sig_t是一个函数指针,它指向一个函数B,函数B的返回值为void,函数B的参数列表:int
//4. 复杂版本函数声明(一般这么写)
typedef void(*sig_t)(int);
+
void (*signal(int signum, sig_t handler))(int);
本质上就是 signal(int, signum, sig_t hadler)
signal的返回值是一个函数指针罢了
//5.练习
typedef int *(*Pointer)(int,int);//看到第一个括号里面有*就说明这是一个函数指针
//定义了一个名为Pointer的函数指针类型,该函数指针指向的地址的函数需要两个int参数,并且该函数的返回值为int*!!!即返回值是一个int类型的指针罢了
总结:函数回调本质为函数指针作为函数参数,函数调用时传入函数地址,这使我们的代码变得更加灵活,可复用性更强
(杂)关于man
系统调用函数:man2
库函数:man3
execve是系统调用函数,其他exec函数族都是是在他基础上封装的
如果没有加-g也可以通过进入gdb后file+ 一个含有调试表的程序 来导入入调试表 ↩︎