shell程序补全
接源码(截图版本):
以上加了export环境变量的功能(不成功),
下面修改代码
到环境变量的时候可能会浅拷贝,在循环刷新时刷掉,导致子进程没有拿到相应的字符串,所以在进程替换之前将环境变量拷贝之后再传。
完整代码
//myshell.c
#include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 #include<unistd.h>
5 #include<sys/wait.h>
6 #include<sys/types.h>
7
8 #define NUM 1024
9 #define SIZE 32
10 #define SEP " "
11
12 char cmd_line[NUM];
13 char *g_argv[SIZE];
14
15 char g_myval[64];
16 int main()
17 {
18 extern char**environ;
19 while(1)
20 {
21 printf("[lifan@localhost myshell]");
22 fflush(stdout);
23 memset(cmd_line,'\0',sizeof cmd_line);
24 if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
25 {
26 continue;
27 }
28 cmd_line[strlen(cmd_line)-1]='\0';
29 g_argv[0]=strtok(cmd_line,SEP);
30 int index=1;
31 if(strcmp(g_argv[0],"ls")==0)
32 {
33 g_argv[index++]="--color=auto";
34 }
W> 35 while(g_argv[index++]=strtok(NULL,SEP));
36 if(strcmp(g_argv[0],"export")==0&&g_argv[1] !=NULL)
37 {
38 strcpy(g_myval,g_argv[1]);
39 putenv(g_myval);
40 continue;
41 }
42 if(strcmp(g_argv[0],"cd")==0)
43 {
44
45 if(g_argv[1]!=NULL)chdir(g_argv[1]);
46 continue;
47 }
48 pid_t id=fork();
49 if(id==0)
50 {
51 printf("下面功能让子进程进行的\n");
52 //execvpe(g_argv[0],g_argv,environ);
53 execvp(g_argv[0],g_argv);
54 exit(1);
55 }
56 int status=0;
57 pid_t ret=waitpid(id,&status,0);
58 if(ret>0) printf("exit code:%d\n",WEXITSTATUS(status));
59 }
60 }
mytest.c
复习C文件IO相关操作
问:当前路径怎么理解?
答:当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径
C文件接口
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
- size:一次读取的字节大小
- count:一次读取多少个 size
- stream:流
- ptr:目标内存块
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
//同理不多赘述
FILE *fopen( const char *filename, const char *mode );
- filename:文件的路径
- mode:打开模式
注意:
- 文件是否打开成功
- 关闭文件
- 文件指针置空
feof 函数
函数返回值:如果没有到文件尾,返回0;到达文件尾,返回一个非零值。
ferror 函数
函数返回值:无错误出现时返回0;有错误出现时,返回一个非零值。
输出信息到显示器,你有哪些方法(未校验)
#include <stdio.h>
#include <string.h>
int main()
{
char line[64];
//fgets->C->s(string)->会自动在字符结尾添加\0
while(fgets(line,sizeof(line),fp)!=NULL)
{
fprintf(stdout,"%s",line);
}
return 0;
}
stdin & stdout & stderr
- C默认会打开三个输入输出流,分别是stdin, stdout, stderr
- 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
总结
- fopen的方式
“r”:以只读的形式打开文本文件(不存在则出错)
“w”:以只写的形式打开文本文件(若不存在则新建,反之,则从文件起始位置写,覆盖原内容)
“a”:以追加的形式打开文本文件(若不存在,则新建;反之,在原文件后追加)
“r+”:以读写的形式打开文本文件(读时,从头开始;写时,新数据只覆盖所占的空间)
“wb”:以只写的形式打开二进制文件
“rb”:以只读的形式打开二进制文件
“ab”:以追加的形式打开一个二进制文件
“rb+”:以读写的形式打开二进制文件。
“w+”:首先建立一个新文件,进行写操作,然后从头开始读(若文件存在,原内容将全部消失)
“a+”:功能与”a”相同。只是在文件尾部追加数据后,可以从头开始读
“wb+”:功能与”w+”相同。只是在读写时,可以由位置函数设置读和写的起始位置
“ab+”:功能与”a+”相同。只是在文件尾部追加数据之后,可以由位置函数设置开始读的起始位置
认识文件相关系统调用接口
接口介绍
open
man open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);//读取的时候用
int open(const char *pathname, int flags, mode_t mode);//创建文件的时候用,(因为要分配权限)
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
mode_t理解:直接 man 手册,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
write read close lseek ,类比C文件相关接口。
umask
#include<sys/types.h>
#include<sys/stat.h>
mode_t umask(mode_t mask);
fopen, fread, fwrite, fseek, fclose等函数的使用
要求:
- 使用代码打开当前路径下的“bite”文件(如果文件不存在在创建文件),向文件当中写入“linux so easy!”.
- 在从文件当中读出文件当中的内容, 打印到标准输出当中; 关闭文件流指针
mytest.cc
C库函数:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
FILE *fp = fopen("./bite", "w+"); // 打开文件时清空文件,然后再写
if (fp == NULL)
{
perror("fopen error");
return -1;
}
// 进行文件操作
const char *s1 = "linux so easy!"; // 不要+1,\0结尾是C语言的规定,文件用遵守吗?文件要保存的是有效数据
// size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ssize_t ret = fwrite(s1, 1,strlen(s1), fp);
if (ret != strlen(s1))
{
perror("fwrite error!");
return -1;
}
// 当向文件中写入数据后,想要重新读取到数据,要么需要关闭文件重新打开,要么
// 就要跳转读写位置到文件起始位置,然后再开始读取文件数据
fseek(fp, 0, SEEK_SET); // 跳转读写位置到,从文件起始位置开始偏移0个字节
char buff[1024] = {0};
// size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ret = fread(buff, 1, 1023, fp); // 因为设置读取块大小位1,块个数为1023因此fread返回值为实际读取到的数据长度(预留一个位置给\0)
if (ret == 0)
{
if (ferror(fp)) // 判断上一次IO操作是否正确
printf("fread error\n");
if (feof(fp)) // 判断是否读取到了文件末尾
printf("read end of file!\n");
return -1;
}
printf("%s", buff);
fclose(fp);
return 0;
}
系统接口:
#include <stdio.h>
#include <unistd.h>//是close, write这些接口的头文件
#include <string.h>
#include <fcntl.h>//是 O_CREAT 这些宏的头文件
#include <sys/stat.h>//umask接口头文件
int main()
{
//将当前进程的默认文件创建权限掩码设置为0--- 并不影响系统的掩码,仅在当前进程内生效(就近原则)
umask(0);
//int open(const char *pathname, int flags, mode_t mode);
int fd = open("./bite", O_CREAT|O_RDWR, 0664);//110 110 100(rw-rw-r--)(第一个0表示采用8进制)
if(fd < 0) {
perror("open error");
return -1;
}
char* data = "I like Linux!\n";
//ssize_t write(int fd, const void *buf, size_t count);
ssize_t ret = write(fd, data, strlen(data));
if (ret < 0) {
perror("write error");
return -1;
}
//off_t lseek(int fd, off_t offset, int whence);
lseek(fd, 0, SEEK_SET);
char buf[1024] = {0};
//ssize_t read(int fd, void *buf, size_t count);
ret = read(fd, buf, 1023);
if (ret < 0) {
perror("read error");
return -1;
}else if (ret == 0) {
printf("end of file!\n");
return -1;
}
printf("%s", buf);
close(fd);
return 0;
}
文件:
1.被进程打开的文件(内存文件)
2.没有被打开的文件(磁盘上,文件=内容+属性) (磁盘文件)
文件描述符fd
概念:
- 在一个进程中打开一个文件,会在进程内生成文件的描述信息结构,并将其地址添加到PCB中的文件描述信息数组中,最终返回所在位置下标作为文件描述符。
- 文件流指针是标准库IO操作句柄,是一个FILE*结构体指针,其中内部封装有文件描述符,其对应的操作接口有:fopen, fread, fwrite, fseek, fclose…
- open是系统调用接口,返回的是文件描述符,并非文件流指针
细节:
- 进程数据独有,各自有各自的文件描述信息表,因此各自打开文件会有自己独立的描述信息添加在各自信息表的不同位置,因此fd各自也相互独立
- 两个进程打开同一个文件,但是各有各的文件描述信息以及读写位置,互不影响,因此多个进程同时读写有可能会造成穿插覆盖的情况。
ps:(原子性操作,被认为是一次性完成的操作,操作过程中间不会被打断,通常以此表示操作的安全性)
- 删除文件实际上只是删除文件的目录项,文件的数据以及inode并不会立即被删除,因此若进程已经打开文件,文件被删除时,并不会影响进程的操作,因为进程已经具备文件的描述信息(可以编写代码进行尝试,在文件打开后,外界删除文件,然后看进程中是否还可以继续写入或读取数据)
- 如果仅仅是读取文件内容,两个不同进程其实都有自己各自的描述信息和读写位置,因此可以同时读取文件数据而不会受到对方的影响。
- 因为文件内容的修改是直接反馈至磁盘文件系统中的,因此当文件内容被修改,其他进程因为也是针对磁盘数据的操作,因此可以立即感知到(可以写代码尝试一个进程打开文件后,等其他进程修改了内容后然后再读取文件数据进行测试)
通过对open函数的学习,我们知道了文件描述符就是一个小整数
0&1&2
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是:
- 标准输入0
- 标准输出1
- 标准错误2.
- 0,1,2对应的物理设备一般是:键盘,显示器,显示器
在bash中,在一条命令后加入”1>&2”意味着()
在这条语句中,>是重定向符号 &2是在重定向使用时标准错误的一种特殊写法
因此 1>&2 被理解为: 将标准输出重定向到标准错误
所以输入输出还可以采用如下方式
#include <stdio.h>
#include <sys/types.h>#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf)); if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
文件描述符的分配规则
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出发现是fd:3
关闭0或者2,在看
#include <stdio.h>
#include <sys/types.h>#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
发现是结果:fd:0或者fd:2可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
那如果关闭1呢?看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile当中,其中,fd=1。这种现象叫做输出重定向,常见的重定向有:>,>>,<
那么重定向的本质是什么呢?
使用dup2系统调用
int dup2(int oldfd, int newfd);
- 函数功能为将newfd描述符重定向到oldfd描述符,相当于重定向完毕后都是操作oldfd所操作的文件
- 但是在过程中如果newfd本身已经有对应打开的文件信息,则会先关闭文件后再重定向(否则会资源泄露)
函数原型如下:
#include<unistd.h>
int dup2(int oldfd,int newfd);
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd=open("./log",O_CREAT|O_RDWR);
if(fd<0)
{
perror("open");
return 1;
}
close(1);
//将标准输出,重定向到文件,这样则写往标准输出的数据会被写入到文件中,而不是被打印
dup2(fd,1);
for(;;)
{
char buf[1024]={0};
ssize_t read_size=read(0,buf,sizeof(buf)-1);
if(read_size<0)
{
perror("read");
break;
}
//printf内部操作的是stdout标准输出文件流指针,而文件流指针本质上内部包含的是1号描述符成员
//printf的打印就是向标准输出写入数据,因为标准输出已经被重定向,因此数据会被写入文件中,而不是直接打印
printf("%s",buf);
fflush(stdout);
}
return 0;
}
例子1.在minishell中添加重定向功能:
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <fcntl.h>
# define MAX_CMD 1024
char command[MAX_CMD];
int do_face()
{
memset(command, 0x00, MAX_CMD);
printf("minishell$ ");
fflush(stdout);
if (scanf("%[^\n]%*c", command) == 0) {
getchar();
return -1;
}
return 0;
}
char **do_parse(char *buff)
{
int argc = 0;
static char *argv[32];
char *ptr = buff;
while(*ptr != '\0') {
if (!isspace(*ptr)) {
argv[argc++] = ptr;
while((!isspace(*ptr)) && (*ptr) != '\0') {
ptr++;
}
}else {
while(isspace(*ptr)) {
*ptr = '\0';
ptr++;
}
}
}
argv[argc] = NULL;
return argv;
}
int do_redirect(char *buff)
{
char *ptr = buff, *file = NULL;
int type = 0, fd, redirect_type = -1;
while(*ptr != '\0') {
if (*ptr == '>') {
*ptr++ = '\0';
redirect_type++;
if (*ptr == '>') {
*ptr++ = '\0';
redirect_type++;
}
while(isspace(*ptr)) {
ptr++;
}
file = ptr;
while((!isspace(*ptr)) && *ptr != '\0') {
ptr++;
}
*ptr = '\0';
if (redirect_type == 0) {
fd = open(file, O_CREAT|O_TRUNC|O_WRONLY, 0664);
}else {
fd = open(file, O_CREAT|O_APPEND|O_WRONLY, 0664); }
dup2(fd, 1);
}
ptr++;
}
return 0;
}
int do_exec(char *buff)
{
char **argv = {NULL};
int pid = fork();
if (pid == 0) {
do_redirect(buff);
argv = do_parse(buff);
if (argv[0] == NULL) {
exit(-1);
}
execvp(argv[0], argv);
}else {
waitpid(pid, NULL, 0);
}
return 0;
}
int main(int argc, char *argv[]){
while(1) {
if (do_face() < 0)
continue;
do_exec(command);
}
return 0;
}
FILE/缓冲区
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢?./hello>file,我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwite
我们发现printf和fwrite(库函数)都输出了两次,而write只输出了一次(系统调用)。为什么呢?肯定和fork有关!
缓冲区刷新策略:
- 立即刷新
- 行刷新(行缓冲)
- 满刷新(全缓冲)
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf,fwrite库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写实拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据
- write没有变化,说明没有所谓的缓冲。
综上:printf fwrite库函数会自带缓冲区,而write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢?printf,fwrite是库函数,write是系统调用,库函数在系统调用的"上层",是对系统调用的"封装",但是write没有缓冲区,而printf,fwrite有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
如果有兴趣,可以看看FILE结构体:
typedef struct _IO_FILE FILE;在/usr/include/stdio.h
在/usr/include/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE };
模拟实现FILE:
myfile.c
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<stdlib.h>
#define NUM 1024
struct MyFILE_{
int fd;
char buffer[1024];
int end; //当前缓冲区的结尾
};
typedef struct MyFILE_ MyFILE;
MyFILE *fopen_ (const char *pathname, const char *mode)
{
assert(pathname);
assert(mode);
MyFILE *fp=NULL;
if(strcmp(mode,"r")==0)
{
}
else if(strcmp(mode,"r+")==0)
{
}
else if(strcmp(mode,"w")==0)
{
int fd=open(pathname,O_WRONLY|O_TRUNC|O_CREAT,0666);
if(fd>=0)
{
fp=(MyFILE*)malloc(sizeof(MyFILE));
memset(fp,0,sizeof(MyFILE));
fp->fd=fd;
}
}
else if(strcmp(mode,"w+")==0)
{
}
else if(strcmp(mode,"a")==0)
{
}
else if(strcmp(mode,"a+")==0)
{
}
else
{
//什么都不做
}
return fp;
}
void fputs_(const char *message,MyFILE *fp)
{
assert(message);
assert(fp);
strcpy(fp->buffer+fp->end,message);
fp->end+=strlen(message);
//for debug
printf("%s\n",fp->buffer);
//暂时没有刷新,刷新策略由谁来执行呢?用户通过执行C标准库中的代码逻辑,来完成刷新动作
//这里效率提高,体现在哪里呢?因为C提供了缓冲区,那么我们就通过策略,减少了IO的执行次数(不是数据量)
if(fp->fd==0)
{
//标准输入
}
else if(fp->fd==1)
{
//标准输出
if(fp->buffer[fp->end-1]=='\n')
{
fprintf(stderr,"fflush: %s",fp->buffer);
write(fp->fd,fp->buffer,fp->end);
fp->end=0;
}
}
else if(fp->fd==2)
{
//标准错误
}
else
{
//其他文件描述符
}
}
void fclose_(MyFILE *fp)
{
assert(fp);
fflush_(fp);
close(fp->fd);
free(fp);
}
void fflush_(MyFILE *fp)
{
assert(fp);
if(fp->end!=0)
{
//暂且认为刷新了-其实是把数据写到了内核
write(fp->fd,fp->buffer,fp->end);
syncfs(fp->fd);//将数据写入到磁盘
fp->end=0;
}
}
int main()
{
close(1);
MyFILE *fp=fopen_("./log.txt","w");
if(fp==NULL)
{
printf("open file error");
return 1;
}
fputs_("one: hello world",fp);
sleep(1);
fputs_("two: hello world\n",fp);
sleep(1);
fputs_("three: hello world",fp);
sleep(1);
fputs_("four: hello world\n",fp);
sleep(1);
fclose_(fp);
}
myshell实现缓冲区:
#include<stdio.h> #include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#define SIZE 32
#define NUM 1024
//保存完整的命令行 char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];
//写一个环境变量的buffer,用来测试
char g_myval[64];
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0
int redir_status=NONE_REDIR;
char *CheckRedir(char *start)
{
assert(cmd_str);
char *end=start+strlen(start)-1;//abcd\0
while(end>=start)
{
if(*end=='>')
{
if(*(end-1)=='>')
{
redir_status=APPEND_REDIR;
*(end-1)='\0';
end++;
break;
}
redir_status=OUTPUT_REDIR;
*end='\0';
end++;
break;
}
else if(*end=='<')
{
//cat <myfile.txt输入
redir_status=INPUT_REDIR;
*end='\0';
end++;
break;
}
else
{
end--;
}
}
if(end>=start)
{
return end;//要打开的文件
}
else
{
return NULL;
}
}
int main()
{
//0.命令行解释器,一定是一个常驻内存的进程,不退出
while(1)
{
//1.打印出提示信息
printf("[root@localhost myshell]#");
fflush(stdout);
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入[输入的是各种指令和选项:"ls -a -l -i"]
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
continue;
}
cmd_line[strlen(cmd_line-1)]='\0';//替换掉输入缓冲区的换行
//2.1:分析是否有重定向,“ls -a -l>log.txt"->
//"ls -a -l\0log.txt"
//"ls -a -l -i\n\0"
char *sep=CheckRedir(cmd_line);
//printf("echo:%s\n",cmd_line);
//3.命令行字符串解析
g_argv[0]=strtok(cmd_line," ");
//char *strtok(char *str,const char*delim);//第一次调用,要传入原始字符串
int index=1;
if(strcmp(g_argv[0],"ls")==0)
{
g_argv[index++]="--color=auto";
}
while(g_argv[index++]=strtok(NULL," "));//第二次,如果还要解析原始字符
//for debug
//for(index=0;g_argv[index];index++)
//printf("g_argv[%d]:%s\n",index,g_argv[index]);
//4.TODO内置命令,让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令
//内建命令本质其实就是(shell)中的一个函数调用
if(strcmp(g_argv[0],"cd")==0)
{
if(g_argv[1]!=NULL) chdir(g_argv[1]);//cd path,cd ..
continue;
}
//5.fork()
pid_t id=fork();
if(id==0)//child
{
if(sep!=NULL)
{
int fd=-1;
//重定向工作
switch(redir_status)
{
case INPUT_REDIR:
fd=open(sep,O_RDONLY);
dup2(fd,0);
break;
case OUTPUT_REDIR:
fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666);
dup2(fd,1);
break;
case APPEND_REDIR:
fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666)
dup2(fd,1);
break;
default:
printf("bug?\n");
break;
//子进程替换时只替换页表的地址,代码和数据,其他的结构不受影响(打开的文件)
}
}
printf("下面功能让子进程进行的\n");
execvp(g_argv[0],g_argv);
exit(1);
}
//father
int status=0;
pid_t ret=waitpid(id ,&status,0);
if(ret>0) printf("exit code:%d\n",WEXITSTATUS(status));
}
}
磁盘级别文件:
问:
我们学习磁盘级别文件,侧重点在哪?
单个文件角度:文件在哪里?这个文件多大?这个文件的其他属性是什么?
站在系统角度:一共有多少个文件?各自属性在哪里?如何快速找到?我还可以存储多少个文件?如何快速的找到指定的文件?
inode v s \textcolor{blue}{vs} vs 文件名
找到文件: inode编号(依托于目录结构)->分区特定的bg->inode->属性->内容
- 一个目录下,是不是可以保存很多文件,但是这些文件没有重复的文件名
- 目录是文件吗?是->有自己的inode,有自己的data block
- 文件名:inode 编号的映射关系
创建文件,操作系统做了什么?
删除文件,操作系统做了什么?
查看文件,操作系统做了什么?
理解文件系统
我们使用ls -l 的时候看到了除了看到文件名,还看到了文件元数据。
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.c
每行包含7列:
- 模式
- 硬链接数
- 文件所有者
- 组
- 大小
- 最后修改时间
- 文件名
ls,-l读取存储在磁盘上的文件信息,然后显示出来
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800
上面的执行结果有几个信息需要解释
inode
为了能解释清楚inode我们先简单了解一下文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- i节点表:存放文件属性如文件大小,所有者,最近修改时间等
- 数据区:存放文件内容
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc 263466 abc
为了说明问题,我们将上图简化:
创建一个新文件主要有以下4个操作:
-
存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
-
存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
-
记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
-
添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
理解硬链接
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。其实在linux中可以让多个文件名对应于同一个
inode。 [root@localhost linux]# touch abc [root@localhost linux]# ln abc def [root@localhost linux]# ls -1i abc def 263466 abc 263466 def
-
abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 263466 的硬连接数为2。
-
我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。
-
软链接
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法
263563 -rw-r–r–. 2 root root 0 9月 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9月 15 17:53 abc.s -> abc
263563 -rw-r–r–. 2 root root 0 9月 15 17:45 def
acm
下面解释一下文件的三个时间:
- Access 最后访问时间
- Modify 文件内容最后修改时间
- Change 属性最后修改时间
动态库与静态库
- 我如果想写一个库?
- 如果我把库给别人,别人怎么用呢?
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
测试程序
/add.h/ #ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif // __ADD_H__
/add.c/ #include "add.h"
int add(int a, int b)
{
return a + b;
}
/sub.h/ #ifndef __SUB_H__
#define __SUB_H__
int sub(int a, int b);
#endif // __SUB_H__
/add.c/ #include "add.h"
int sub(int a, int b)
{
return a - b;
}
///main.c #include <stdio.h>
#include "add.h"
#include "sub.h"
int main( void )
{
int a = 10;
int b = 20;
printf("add(10, 20)=%d\n", a, b, add(a, b)); a = 100;
b = 20;
printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}
生成静态库
[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r–r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r–r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息
[root@localhost linux]# gcc main.c -L. -lmymath
-L 指定库路径
-l 指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行。
库搜索路径
从左到右搜索-L指定的目录。
由环境变量指定的目录(LIBRARY_PATH)
由系统指定的目录
- /usr/lib
- /usr/local/lib
生成动态库
- shared: 表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
示例:
[root@localhost linux]# gcc -fPIC -c sub.c add.c [root@localhost linux]# gcc -shared -o libmymath.so *.o
[root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o
使用动态库
编译选项
- l:链接动态库,只要库名即可(去掉lib以及版本号)
- L:链接库所在的路径
示例: gcc main.o -o main -L -lhello
运行动态库
- 拷贝.so文件到系统共享库路径下,一般指/usr/lib
- 更改LD_LIBRARY_PATH
[root@localhost linux]# export LD_LIBRARY_PATH=.
[root@localhost linux]# gcc main.c -lmymath
[root@localhost linux]# ./a.out
add(10, 20)=30
sub(100, 20)=80
- ldconfig配置/etc/ld.so.conf.d/,ldconfig更新
[root@localhost linux]# cat /etc/ld.so.conf.d/bit.conf
/root/tools/linux
[root@localhost linux]# ldconfig
使用外部库
系统中其实有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses库)
#include <math.h>
#include <stdio.h>
int main(void)
{
double x = pow(2.0, 3.0);
printf("The cubed is %f\n", x); return 0;
}
gcc -Wall calc.c -o calc -lm
-lm表示要链接libm.so或者libm.a库文件
库文件名称和引入库的名称
如:libc.so -> c库,去掉前缀lib,去掉后缀.so,.a
代码示例:
创建一个自己libhello.a的静态库
myprint.c
mymath.h
makefile:
makefile修改:
库的用法:
动态库部分
既形成动态库又形成静态库的makefile
ps:没有听进去没有整理,动静态库细节还需要再整理一遍,大概在做项目的时候用(doge)