LinuxDay01 檔案&程式基礎

無在無不在發表於2020-12-14

0x01 檔案

1.檔案的基本操作

1.1檔案描述符號

C中對應檔案的是 檔案指標

C++中對應檔案的是檔案流物件

windows 中對應檔案的是 檔案控制程式碼

Linux 檔案描述符號(一個整數) 對應 檔案

標準輸入輸出裝置 在linux上也是檔案,他們的檔案描述符號分別為0,1,2

標準輸入裝置:0

標準輸出裝置:1

標準錯誤輸出裝置:2

Linux上開啟任何一個檔案,都會得到一個檔案描述符號,如果成功開啟,則檔案描述符號為正數

如果開啟失敗,則為負數

1.2檔案操作的函式

open

read

write

lseek

close

mmap 檔案內容對映

man open 檢視open的幫助文件,預設為檢視對應命令的幫助文件

引數 2 表示檢視標準C庫函式

標準庫函式必須加標頭檔案:<stdio.h>

man 2 函式名,例如 fopen fwrite fread這些C的函式

標準C庫函式與作業系統無關,因此程式的可移植性好

引數 3 表示檢視系統庫函式 man 3 open

系統庫函式必須加標頭檔案:<unistd.h> 即 unix std

以上提到的linux系統的檔案操作函式,都是系統庫函式,即使用者程式無權呼叫,

必須通過訪管指令,引起訪管中斷,使得CPU從目態進入管態,來呼叫這些系統庫函式

然後,從管態回到目態,繼續執行使用者程式

以上過程便是系統呼叫

因為open為linux的系統庫函式,所以用man 3 open去檢視幫助文件

int open(const char* path,int oflag,int mode);
/**
*開啟一個檔案
*返回該檔案的檔案描述符
*@const char* path 檔名
*@int oflag 檔案開啟方式
*@int mode 檔案許可權
*/

PS:Linux系統中%m通配標準的錯誤輸出

ssize_t pwrite (int fd, const void * buf, size_t count,off_t offset); 
/**
*向檔案中寫入資料
*@int fd 檔案的描述符號
*@const void * buf 要寫入的資料的地址
*@size_t count 位元組數
*@off_t offset 偏移量
*/
ssize_t write (int fd, const void * buf, size_t count); 
/**
*向檔案中寫入資料
*@int fd 檔案的描述符號
*@const void * buf 要寫入的資料的地址
*@size_t count 位元組數

*/

demo:

向檔案中寫入一個結構體:

如果用cat a.txt 檢視檔案中的內容,當然是亂碼

所以還必須寫一個程式從a.txt中將寫入的內容讀出來

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

struct Student{
	char name[20];
	int age;
	double score;
};

int main(){
	int fp = open("./a.txt",O_RDWR | O_CREAT,0777);
	if(-1 == fp){
		printf("開啟檔案失敗:%m\n");
		return -1;
	}
	printf("建立檔案成功!\n");
	struct Student s = {"Brad",20,88.88};
	write(fp,&s,sizeof(s));
	close(fp);

	return 0;
}

從檔案中讀取結構體:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

struct Student{
	char name[20];
	int age;
	double score;
};

int main(){
	int fd = open("./a.txt",O_RDWR|O_EXCL,0777);
	if(-1 == fd){
		printf("開啟檔案失敗:%m\n");
	}
	printf("開啟檔案成功!\n");
	struct Student* p = (struct Student*)malloc(sizeof(struct Student));
	read(fd,p,sizeof(struct Student));
	printf("%s %d %.2lf\n",p->name,p->age,p->score);
	return 0;
}

exit(-1)結束一個程式

//開啟a.bat 每隔3個字元列印一個字元到終端
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

int main(){
	int fd = open("./a.bat",O_RDONLY | O_EXCL,0666);
	if(-1 == fd){
		printf("開啟失敗:%m\n");
		exit(-1);
	}
	printf("開啟成功!\n");

	int r;
	char c;
	while(1){
		r = read(fd,&c,1);
		if(r>0){
			printf("%c\n",c);
		}else{
			break;	
		}
		lseek(fd,3,SEEK_CUR); 
	}
	close(fd);
	return 0;
}

2.檔案鎖

linux 系統是多工多使用者的,所以可能兩個程式在同時操作同一個檔案

#檔案鎖的分類:

1.lockf 加建議性鎖

即防君子不防小人,只是在檔案上做了一個標記,表示該檔案有程式在操作。

其他程式要動這個檔案,看一下如果有標記,最好就不要動,但是如果想動的話還是可以動。

2.fcntl 加強制性鎖 並且 加建議性鎖 並且 加記錄鎖

#fnctl函式的使用

 #include <fcntl.h>

     int
     fcntl(int fildes, int cmd, ...);

/**
*@int fildes 檔案描述符
*@int cmd 操作命令,如果操作命令為F_SETLK F_GETLK 第三個引數為鎖的資訊結構體
*@struct flock{
    l_type, 鎖的型別(F_RDLCK讀鎖/共享鎖 即多個程式可以同時讀,F_WRLCK寫鎖/互斥鎖即多個程式不能同時寫一個檔案)
    l_whence,    加鎖的起始位置 建議為SEEK_SET,即檔案頭  
    l_start,    相對偏移量,建議為0
    l_len,      加鎖的長度  
    l_pid       加鎖的程式id 誰持有這把鎖
}
*/

獲取檔案上鎖的情況:(Mac上總是獲取失敗)

// 判斷a.bat是否有加鎖,如果加了,判斷是什麼鎖
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(){
	int fd = open("./a.bat",O_RDWR | O_EXCL,0666);
	if(-1 == fd){
		printf("開啟檔案失敗:%m\n");
		exit(-1);
	}
	printf("開啟檔案成功!\n");
	struct flock lock = {0};
	lock.l_type = 0;
	lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 15;

	// 獲取a.bat的鎖的資訊,儲存到lock結構體中
	int ret = fcntl(fd,F_GETLK,&lock);
	if(-1 == ret){
		printf("獲取鎖情況失敗:%m \n");
		close(fd);
		exit(-1);
	}
	printf("獲取鎖情況成功!");
	if(lock.l_type != F_UNLCK){//如果已經上了鎖
		if(F_RDLCK == lock.l_type){
			printf("當前檔案上了一把讀鎖");
		}
		if(F_WRLCK == lock.l_type){
			printf("當前檔案上了一把寫鎖");
		}
		printf("上鎖的程式的id為:%d\n",lock.l_pid);
	}else{
		printf("當前檔案沒有上鎖");
	}
	close(fd);

	return 0;
}

給檔案加一個讀鎖:

// 給a.bat加一把讀鎖,然後收到控制資訊後,解除鎖
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(){
	int fd = open("./a.bat",O_RDWR | O_EXCL,0666);
	if(-1 == fd){
		printf("開啟檔案失敗:%m\n");
		exit(-1);
	}
	printf("開啟檔案成功!\n");
	struct flock lock = {0};
	// 設定讀鎖
	lock.l_type = F_RDLCK;
	lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 15;
	int ret;
	ret = fcntl(fd,F_SETLK,&lock);
	if(-1 == ret){
		printf("上讀鎖失敗:%m\n");
		close(fd);
		exit(-1);
	}
	printf("上讀鎖成功!\n");
	getchar();
	//解除讀鎖
	lock.l_type = F_UNLCK;
	ret = fcntl(fd,F_SETLK,&lock);
	if(-1 == ret){
		printf("解除讀鎖失敗:%m\n");
		close(fd);
		exit(-1);
	}
	printf("解除讀鎖成功!\n");
	return 0;
}

給檔案加上一個寫鎖:

// 給a.bat加一把寫鎖,收到控制資訊後解除鎖
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>

int main(){
	int fd = open("./a.bat",O_RDWR | O_EXCL,0666);
	if(-1 == fd){
		printf("開啟檔案失敗:%m\n");
		exit(-1);
	}
	printf("開啟檔案成功!\n");
	struct flock lock = {0};
	// 設定寫鎖
	lock.l_type = F_WRLCK;
	lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 15;
	int ret;
	ret = fcntl(fd,F_SETLK,&lock);
	if(-1 == ret){
		printf("上寫鎖失敗:%m\n");
		close(fd);
		exit(-1);
	}
	printf("上寫鎖成功!\n");
	getchar();
	//解除讀鎖
	lock.l_type = F_UNLCK;
	ret = fcntl(fd,F_SETLK,&lock);
	if(-1 == ret){
		printf("解除寫鎖失敗:%m\n");
		close(fd);
		exit(-1);
	}
	printf("解除寫鎖成功!\n");
	return 0;
}

3.io多路複用

以後講

0x02 程式 

#程式的組成:

1.資料:全域性變數,已經初始化的靜態變數,只讀變數,位初始化的靜態變數

2.程式碼:程式的原始碼

3.堆疊:一個程式有獨屬於它的堆和棧,例如:普通變數,手動記憶體分配

#程式的模式:

使用者模式(目態)

核心模式(管態)

#程式的執行:

方式一:手動執行

方式二:排程程式,例如:中斷,調整程式優先順序

ps top 檢視程式 ps -all 檢視所有程式

nice renice 調整程式的優先順序

kill -9 程式id  結束程式

crontab 定時任務

bg 將前臺程式變成後臺程式

 

#init程式

Linux下所有程式都是init程式的子程式

所有程式資源都在 /proc 目錄下

獲取程式id和父程式id

#include <unistd.h>
#include <stdio.h>

int main(){
	printf("當前程式id:%d\n",getpid());
	printf("當前程式的父程式的id:%d\n",getppid());
	while(1);
	return 0;
}

#建立程式的方式

fork:以克隆clone(系統庫函式)當前程式的方式來建立一個子程式

子程式擁有父程式一樣的資源和程式上下文

vfork:父子程式共享同一塊實體記憶體

copy-on-write 寫時拷貝

exec函式族:

execl execv execle execlp execvp

 

fork建立程式:

當父程式執行到fork函式時,會拷貝出一份和自己完全一樣的子程式

也從fork函式開始執行,之不過父程式fork函式的返回值大於0

子程式fork函式的返回值等於0

#include <stdio.h>
#include <unistd.h>

int main(){
	printf("這是父程式,當前程式id:%d\n",getpid());
	for(int i = 0;i<5;i++){
		printf("run ----\n");
		sleep(1);
	}
	int ret;
	ret = fork();
	if(-1 == ret){
		printf("fork函式執行出錯!\n");
	}else if(ret > 0 ){
		printf("這是父程式:%d\n",getpid());
		while(1){
			printf("------\n");
			sleep(1);
		}
	}else{
		printf("這是子程式:%d,其父程式id:%d",getpid(),getppid());
		while(1){
			printf("-----\n");
			sleep(1);
		}
	}
	return 0;
}

#程式的結束:

exit: 釋放程式佔據的資源後再呼叫_exit; exit(退出碼)

_exit: 直接結束程式,不管程式佔據的資源有沒有釋放

#殭屍程式:

如果父程式先於子程式結束,那麼子程式結束後,其佔據的資源就可以無人釋放,子

程式就變成了殭屍程式。如何避免殭屍程式呢?

最好讓父程式晚於子程式結束,如果不能讓父程式晚於子程式結束

那麼就子程式不傳送SIGCHILD訊號

wait(接收退出碼的地址):子程式結束的時候會向父程式傳送一個訊號,wait函式就是一直阻塞直到等這個訊號。

返回值為 子程式的pid

waitpid:

SIGCHILD:子程式結束前會向父程式傳送的訊號

SIGHUP:子程式掛起的時候向父程式傳送的訊號

#程式組:

一個程式組中有多個程式,一個程式組有一個組長,一般為父程式

#會話:

一個會話中有多個程式組

#守護程式:

守護程式是一個獨立的程式,用來記錄作業系統的執行情況(用來記日誌檔案)

檢視當前系統上的守護程式:

ps axj

tpgid為-1的是守護程式

一般來說系統中重要的程式都有其守護程式,來為他寫日誌

一個會話中至少要有一個守護程式

##建立守護程式

1.把其父程式幹掉

2.建立新的會話,擺脫原有會話,原有程式組的控制

3.擺脫終端的控制:

方法一:關閉 0  1 2,即關閉標準輸入輸出裝置,讓終端無法控制他,例如無法在終端上通過ctrl c關閉掉守護程式

方法二:重定向 到 黑洞裝置 /dev/NULL

4.繼承自父程式的檔案描述符 要遮蔽掉,因為子程式是父程式的拷貝,所以在父程式建立子程式前開啟檔案獲得的

檔案描述符號,子程式也會擁有。因為守護程式並不需要這些檔案描述符,所以需要遮蔽掉。

##建立守護程式的程式設計模型

方式一:

1.建立新的會話,setsid,建立者自動成為新會話的組長

2.改變當前目錄 chdir  建議改變當前目錄為 / 根目錄,防止當前目錄被刪除

3.重設檔案許可權掩碼 umask

4.關閉標準輸入輸出裝置 close(0) close(1) close(2)

方式二:

0.重設檔案許可權掩碼 umask

1.建立子程式 fork

2.讓父程式結束

3.建立新的會話

4.防止子程式成為殭屍程式:忽略 SIGCHILD SIGHUP signal

5.更改當前工作目錄

6.重定向檔案描述符號 open 黑洞裝置 dup(fd,0) dup(fd,1)

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(){
	//讓子程式成為守護程式
	//去除檔案許可權
	umask(0);
	if(fork()){
		//結束父程式
		printf("父程式結束!\n");
		exit(0);
	}else{
		printf("當前程式id:%d,父程式id為:%d\n",getpid(),getppid());
		//建立新的會話,讓子程式成為新的會話的組長
		setsid();
		//忽略SIGCHILD SIGHUP
		signal(SIGCHLD,SIG_IGN);
		signal(SIGHUP,SIG_IGN);
		//修改當前工作目錄為 / 
		chdir("/");

		//重定向標準輸入輸出裝置的檔案描述符 到 黑洞裝置
		int fd = open("/dev/NULL",O_RDWR);
		dup(0);
		dup(1);
	}
	while(1){
		//守護程式要做的事情
		sleep(1);
	}
	return 0;
}

 

#程式間通訊:IPC Inter Process Communication

分類:

1.父子程式之間通訊

2.同一主機上的程式通訊

3.不同主機上的程式通訊(網路程式設計)

 

unix系統:

匿名管道 (pipe)

有名管道(FIFO)

訊號(signal)

System V 版本:

訊息佇列

共享記憶體

訊號量(旗語)

Posix 版本

訊息佇列

共享記憶體

訊號量

## 匿名管道pipe:

pipe 本質是一個我們看不到的檔案,但是有兩個檔案描述符號,一個檔案描述符號

用來讀,一個檔案描述符號用來寫。用於父子程式之間的通訊

pipe程式設計模型:

1.建立pipe描述符號

2.建立父子程式框架:先建立pipe,再建立父子程式,因為子程式是父程式所有資源的拷貝,所以子程式就會繼承父

程式的pipe的檔案描述符號

3.使用pipe通訊

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

int main(){
	//1.建立pipe描述符號
	int fd[2];
	int r = pipe(fd);
	if(-1 == r){
		printf("建立pipe描述符號失敗:%m\n");
		exit(-1);
	}
	printf("建立pipe描述符號成功!\n");

	//2.建立子程式
	if(fork()){
		//父 讀
		sleep(1);//讓子程式先寫
		char buff[1024];
		int r;
		while(1){
			//fd[0]專門用於讀
			r = read(fd[0],buff,1023);
			if(r>0){
				buff[r]=0;
				printf(">>%s\n",buff);
			}
		}

	}else{
		//子 寫
		char buff[1024];
		int n = 0;
		while(1){
			sprintf(buff,"父程式你好:%d\n",n++);
			write(fd[1],buff,strlen(buff));
			sleep(1);
		}
	}

}

## 有名管道 fifo緩衝區

有名管道 本質是一個使用者可以看到的檔案,一個具備先入先出特性的緩衝區

有名管道是單工的,即只能單方面工作,即只能一方寫或者一方讀,不能一方的同時一方在讀

有名管道程式設計模型:

1.建立管道 mkfifo

在程式A或者程式B中建立管道均可

-------------------------------------------------------------

程式A                                  程式B

2.開啟管道                          2.開啟管道

3.使用管道                          3.使用管道

4.關閉管道                          4.關閉管道

---------------------------------------------------------------

5.刪除管道

注意:需要兩邊都開啟才能開啟,如果只有程式A開啟管道檔案,open函式會阻塞

只有讀寫兩個程式都開啟open才會結束

linux 命令 | 使用的就是管道,雖然該命令建立的管道檔案的位置我們不知道

fifoA.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(){
	//1.建立有名管道
	int r = mkfifo("./fifo.dat",0666);
	printf("mkfifo %m\n");
	//2.開啟有名管道
	int fd = open("./fifo.dat",O_RDONLY);
	if(-1 == fd){
		printf("開啟管道失敗:%m\n");
		unlink("./fifo.dat");
	}
	printf("開啟管道成功!");
	//3.使用有名管道
	char buff[1024];
	while(1){
		r = read(fd,buff,1023);
		if(r>0){
			buff[r]=0;
			printf(">>%s\n",buff);
			if(strcmp("exit1\n",buff) == 0) break;
		}
	}
	//4.關閉有名管道
	close(fd);
	//5.刪除有名管道
	unlink("fifo.dat");
	return 0;
}

fifoB.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(){
	//1.建立有名管道
	int r = mkfifo("./fifo.dat",0666);
	printf("mkfifo %m\n");
	//2.開啟有名管道
	int fd = open("./fifo.dat",O_RDWR);
	if(-1 == fd){
		printf("開啟管道失敗:%m\n");
		unlink("./fifo.dat");
	}
	printf("開啟管道成功!");
	//3.使用有名管道
	char buff[1024];
	while(1){
		memset(buff,0,1024);
		//向標準輸出裝置中寫 相當於printf("請輸入:")
		write(1,"請輸入:",strlen("請輸入:"));
		//從標準輸入裝置 0 去讀,相當於scanf
		r = read(0,buff,1023);

		if(r>0){
			write(fd,buff,strlen(buff));
			//往管道內寫入
			if(0 == strcmp("exit1\n",buff)) break;
		}
	}
	//4.關閉有名管道
	close(fd);
	printf("write fifo end!\n");

	return 0;
}

#訊號:

使用者模式下 用來模擬 核心模式 下的中斷機制

訊號的本質:本質是個整數

訊號的分類:

1.不可靠訊號:非實時的 kill -l 可以去檢視有多少種訊號 kill 命令就是用來傳送訊號的 1-31是不

可靠訊號。 不可靠訊號 相當於佇列 訊息,可靠訊號相當於非佇列訊息

2.可靠訊號:實時傳送的 32-64

訊號產生 ---> 訊號傳送----> 訊號處理

以上過程能否被打斷(遮蔽)呢?

有些訊號能被遮蔽

例如:

使用signal()函式可以遮蔽掉這兩個訊號

SIGCHLD :子程式結束時傳送給父程式的訊號

SIGHUP:當關閉終端時,傳送給相關程式的訊號,該訊號一般會預設結束終端中的程式。

signal() 用來處理訊號,即註冊訊號處理函式或者忽略訊號

signaction() 高階訊號處理函式

ctrl+c 本質上是傳送一個SIGINT訊號給終端上的當前程式,Linux作業系統的程式排程機制會

自動處理該訊號。當然我們也可以自己手動處理該訊號,即如果我們註冊了訊號處理函式

那麼以上過程就會被打斷,訊號就會傳送到我們的訊號處理函式來處理

 

    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
/**
*@int signum 我們要處理的訊號
*@sighandler_t handler 訊號處理函式
*/

signal.c

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

void handler(int sigNum){
	if(sigNum == SIGINT){
		printf("接收到一個SIGNINT訊號!\n");
	}
}
int main(){
	int n = 0;
	// 註冊一個訊號處理函式
	signal(SIGINT,handler);

	while(1){
		printf("%d\n",n);
		n++;
		sleep(2);
	}
	return 0;
}

執行截圖:

當我們ctrl c的時候,就會中斷該程式,然後用呼叫我們註冊的訊號處理函式

sigaction

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
/**
*@int signum 要處理的訊號
*@const struct sigaction * act 處理方式
*@struct sigaction *oldact 老的處理方式
*/

結構體

struct sigaction {
    void (*sa_handler)(int); 
    void (*sa_sigaction)(int, siginfo_t *, void *);
    //以上都是訊號處理函式
    sigset_t sa_mask; //遮蔽方式
    int sa_flags; //處理方式
    void (*sa_restorer)(void);
}

有些訊號不能被遮蔽

例如:

SIGKILL 殺死程式的訊號 不能被遮蔽

SIGCOUT 讓以停止的程式繼續執行

1.01

相關文章