本章节延续linux第一部分继续学习

并非仅学习操作系统,实际上学习的是linux的系统编程,之后学习的是各样式的系统调用

初步认识系统编程

如何学习系统编程

资料推荐:man手册

如何阅读man手册

通过man man命令可以查看如何使用man手册

image-20231127212814235

image-20231127212911553

库函数帮助手册

某些系统可能没有预装库函数,因此需要先安装

输入以下命令并成功安装

$sudo apt install manpages-posix-dev

根据手册页编号我们可以查看对应命令或函数对应手册的描述

比如我们要查看的是库调用中的printf函数(注意手册页,3对应是库函数中的printf函数)

man 3 printf

image-20231127213726982

文件

狭义概念:存储在外部存储设备介质上的数据集合

广义概念:传输速度慢,容量大,可以持久存储

常见的文件类型 :普通文件,目录文件,软链接,字符设备文件(鼠标,键盘等),块设备文件,管道文件,socket(网络通信)

文件使用

当我们想要访问某个存储设备的文件,肯定不能直接访问,而是通过将文件放到内存,再让cpu从内存读入的方式进行,内存中有一块专门的区域和外部设备进行交换数据,称为文件缓冲区

fopen

fopen应当属于库函数,所以我们想要man时需要看的是3号手册

image-20231128190643704image-20231128190650612

  • 根据FILE *fopen一行可知,参数列表有两个const修饰的字符串,返回值为一个FILE *
  • pathname表示路径,mode有r(只读打开),w(只写创建),rw(读写创建)
  • 如果返回的是空指针则有可能发生错误,通过perror函数可以查看报错信息

准备部分-文件报错

我们在执行可执行文件时有时候会加上一个参数,此时我们通过main函数中的argc参数可以用于检查

#include <stdio.h>

int main(int argc,char* argv[])
{
if(argc != 2)
{
fprintf(stderr,"args error!\n");
return -1;
}


return 0;
}

但这样的话我们每次新建一个文件都需要这么长几行,影响代码可读性

我们可以通过某些操作将这一操作通过带参数的宏定义封装进.h文件(宏定义一般放到头文件中)

  • 在/usr/include中创建一个.h文件 ,可以命名为usually.h,意味着用于我们以后可能经常使用

  • 创建后并给他普通用户可写的权限

    image-20231128194131796

  • 转到usually.h进行编辑

    #include <stdio.h> //加入一些常用的头文件
    #include <string.h>
    #include <stdlib.h>

    #define ARGS_CHECK(argc, num) \
    { \
    if (argc != num) \
    { \
    fprintf(stderr, "args error!\n"); \
    return -1; \
    } \
    } \
    //原本的#define是在一行里
    //#define ARGS_CHECK(argc,num){if(argc != num) {fprintf(stderr,"args error!\n");return -1;}}
    //在vscode下选中代码并按快捷键crtl+k,crtl保持按住,然后crtl+f进行自动格式化
  • 返回到fopen.c中,头文件可改为<usually.h>(使用<>引用是因为我们将usually.h放进了include文件夹中,系统根据<>提示优先在/usr/include目录下寻找对应头文件)

    #include <usually.h>

    int main(int argc,char* argv[])
    {
    ARGS_CHECK(argc,2);

    return 0;
    }

此时fopen.c就可以通过ARGS_CHECK代替刚才的操作

我们再加上检查fp是否是NULL的操作

#include <usually.h>

int main(int argc,char* argv[])
{
ARGS_CHECK(argc,2);
FILE* fp = fopen(argv[1],"r");
if(fp == NULL)
{
perror("fopen");//perror打印字符串内指定函数的错误信息
return -1;
}
fclose(fp);

return 0;
}
  • argv[1]代表第一个参数,也就是我们要打开的文件,并以只读的方式打开
  • perror用于打印报错信息
  • 注意的是一个文件通过fopen打开后一定要通过fclose关闭
  1. 通过makefile文件使用make命令创建fopen可执行程序

  2. 执行fopen,并添加任意一个参数(目录下没有的文件),检测perror函数是否正常工作

    image-20231128195900046

  3. 创建一个文件,检测fopen是否可以正常打开该文件

    image-20231128195922366

我们发现perror这一操作也可以放到.h文件当中

#include <stdio.h> //加入一些常用的头文件
#include <string.h>
#include <stdlib.h>

#define ARGS_CHECK(argc, num) \
{ \
if (argc != num) \
{ \
fprintf(stderr, "args error!\n"); \
return -1; \
} \
}
#define ERROR_CHECK(ret, num, msg) \
{ \
if (ret == num) \
{ \
perror(msg); \
return -1; \
} \
}

并且应用于fopen.c中

#include <usually.h>

int main(int argc,char* argv[])
{
ARGS_CHECK(argc,2);
FILE* fp = fopen(argv[1],"r");
ERROR_CHECK(fp,NULL,"fopen");
fclose(fp);

return 0;
}

追加模式

fopen的一种mode模式

  • “a” append 只写追加(默认从文件结尾写入)
  • “a+” 读写追加

日志系统

一种不可修改旧文件,只允许新加记录的系统

我们可以通过fopen.c的代码模拟一个日志系统

#include <usually.h>

int main(int argc,char* argv[])
{
ARGS_CHECK(argc,2);
FILE* fp = fopen(argv[1],"a");//追加模式
ERROR_CHECK(fp,NULL,"fopen");

//const void* _ptr,size_t _size,size_t _n,FILE* _stream
//指针指向的内容(例如字符串,数组),单位大小,几个单位,写到哪个文件
fwrite("how are you",1,sizeof("how are you"),fp);

fclose(fp);

return 0;
}

通过make再次生成可执行文件后进行测试

image-20231128202007384

发现test文件中多了一行how are you

再次执行发现确实是追加模式

image-20231128202046444

  • 如果我们切换成a+模式,即读写追加模式

进行读操作

#include <usually.h>

int main(int argc,char* argv[])
{
ARGS_CHECK(argc,2);
FILE* fp = fopen(argv[1],"a+");
ERROR_CHECK(fp,NULL,"fopen");

char buf[11] = {0};
fread(buf,1,sizeof("how are you"),fp);
//追加模式的文件存储了三种指针,base,end,和_ptr
//_ptr在每次读写之后都会自动后移,使其符合流的特征
printf("%s",buf);

fclose(fp);

return 0;
}

正常打印出了how are you

image-20231128203124065

如果我们再把原来的写操作加上就会发现

读入时从文件开始读入,写入时跳到文件末尾写入

此处不再演示

fseek,ftell

fseek相当于一种游标,改变_ptr的位置

ftell就是告诉你_ptr的位置

#include <usually.h>

int main(int argc,char* argv[])
{
ARGS_CHECK(argc,2);
FILE* fp = fopen(argv[1],"a+");//追加模式
ERROR_CHECK(fp,NULL,"fopen");

//const void* _ptr,size_t _size,size_t _n,FILE* _stream
//指针指向的内容(例如字符串,数组),单位大小,几个单位,写到哪个文件
char buf[20] = {0};
fread(buf,1,sizeof("howareyou"),fp);
printf("%s",buf);

//记录一下游标目前的位置
printf("before fseek,locate = %ld\n",ftell(fp));
//改变游标的位置到开头
fseek(fp,0,SEEK_SET);
//查看一下游标目前为止
printf("after fseek,locate = %ld\n",ftell(fp));
fwrite("howareyou",1,sizeof("howareyou"),fp);
//查看一下在改变游标后再写入,游标是在文件末尾还是在改变游标+"how are you"的位置
printf("after fwrite,locate = %ld\n",ftell(fp));
fclose(fp);

return 0;
}
  • fseek中的参数SEEK_SET通过man可知是start of file的意思

image-20231128205311417

从结果可得出,即使进行了fseek,追加模式的写入依然会在文件末尾进行

改变文件权限

命令 chmod

查看命令都有哪些手册可以man -f 命令查看

image-20231202154147328

可以看到chmod不只是shell命令也可以是系统调用函数

可以通过man查看他的库函数是sys/stat.h

image-20231202154326884

在我们的usually.h头文件再添加此头文件

查看chmod使用

  • 通过说明可以知道他是用来改变文件权限的
  • 通过参数可知const char* pathname为文件路径,mode_t类型为unsigned int,简要来说只需9bit即可分别代表ugo的读写执行权限
  • 返回值 0成功,-1失败,并可以通过perror查看报错信息

使用chmod函数

#include <usually.h>

int main(int argc,char* argv[])
{
//命令行参数为777 dir
ARGS_CHECK(argc,3);

//传入的777是字符串,需要变为mode_t类型
mode_t mode;
sscanf(argv[1],"%o",&mode);//8进制输入进mode里

int it = chmod(argv[2],mode);
ERROR_CHECK(it,-1,"chmod");

return 0;
}

make后执行发现dir目录文件改为777

image-20231202160417457

再次修改依然可行

image-20231202160528980

getcwd

通过man查看说明

获取当前工作目录

image-20231202160659508

参数解析

image-20231202160806107

  • 库函数为unistd.h
  • 返回值为字符串,buf为传入的字符串参数,传参时长度信息会丢失,因此需要一个size
  • 当buf不为空时返回值为buf
  • 当返回NULL时报错,通过perror查看错误信息

getcwd使用

首先将头文件加入到usually.h头文件当中

代码

#include <usually.h>

int main()
{
char buf[30] = {0};
char* ret = getcwd(buf,sizeof(buf));
ERROR_CHECK(ret,NULL,"getcwd");
printf("ret = %p,ret = %s\n",ret,ret);
printf("buf = %p,buf = %s\n",buf,buf);

return 0;
}

执行结果

image-20231202163248562

如果传入的参数为NULL,实际上getcwd会自动申请堆空间,此时返回值同样会获得当前目录,但申请的堆空间可能没有释放需要自己进行释放掉

#include <usually.h>

int main()
{
char* ret = getcwd(NULL,0);
printf("%s\n",getcwd(NULL,0));
free(ret);

return 0;
}

image-20231202163521902

chdir

man手册查看说明

改变当前工作目录

image-20231202165208607

参数解析

  • 库函数unistd.h
  • 传入参数为要转到哪个路径下
  • 成功返回失败返回-1,通过perror查看报错信息

chdir使用

#include <usually.h>

int main(int argc,char* argv[])
{
ARGS_CHECK(argc,2);

char* ret = getcwd(NULL,0);
ERROR_CHECK(ret,NULL,"getcwd");
printf("before chdir,cwd = %s\n",ret);

int ret2 = chdir(argv[1]);
ERROR_CHECK(ret2,-1,"chdir");

ret = getcwd(NULL,0);
ERROR_CHECK(ret,NULL,"getcwd");
printf("after chdir,cwd = %s\n",ret);

return 0;
}

image-20231202170056800

我们发现结果输出表示目录已经改变,但实际上我们用户所在的目录并没有改变

这是因为当前工作目录为进程的属性,可以把每一个程序当做一个进程

目前有一个写shell命令的进程,另一个是通过./chdir执行出来的一个进程,输出shell命令进程的孩子,孩子目录更改了,不影响当前的目录,只会影响自己和子进程