大连某211大学的李教授继续延续一贯的光辉传统,依旧让每一届所带的本科班写线程并发的拷贝程序。而且越来越多要求,在我读本科的时候,允许用多种语言完成这个程序,然后开始规定只允许在Linux编程。现在还加上必须在管道的基础上,完成这个程序。而且,这个管道还不能直接调用Linux系统中已经封装好的管道,是需要自己根据Linux管道的机制,写成的管道,大致上如下所示:


我早已经在《【Linux】管道的Helloworld》(点击打开链接)揭示过管道的本质,又在《【Java】线程管道通讯》(点击打开链接)批判过管道这种传输数据的方式非常复杂,突然抛出一个概念会让别人觉得你的程序云里雾里,无法读懂。你传输数据可以用一个全局变量,或者在文件之间传递数据之类。或许在线程之间,利用有名管道FIFO传输还有点用,但是在大数据的今天,甚至连线程的概念都日趋过时,更何况是70年代Unix诞生之初数据传输的基本方式呢?

李教授估计是恼羞成怒了,每一年总有奇怪的新型偏门题目商家,但要写的程序却有越来越没有意义,就是越来越繁琐罢了。他教育目标已经让人无法理解到底是为了什么。每一年都有层出不同的要求。似乎教育的目的,不是让人明白一些简单的东西,不可以让全班所有同学都知道答案,绝对不能直接找到一份完整版,然后按步就班模仿出来,掌握这东西。就是要整一道为难学生的题目。

我对他这种方面早就无语了。针对他出的题目,不忍心莘莘学子在网上找不到资料和方向,已经和李教授玩了2年,比如上年《【Linux】线程并发拷贝程序》(点击打开链接)和前年《【Java】线程并发拷贝程序》(点击打开链接),今天有人告诉我,大连某211大学的李教授,必须要求学生模拟出管道。-_-!这老头怎么还不退休呢,都已经60+还不化,反正估计我写完今天这篇,真的没有什么时间继续帮后来的师弟师妹了,也对他提出的程序兴趣殆尽。

先直接贴出这个程序的运行结果,再给大家剖析这程序是如何做的,大家能否忽悠到李教授不要紧,最后操作系统多少分也不要紧,及格拿到学位就行,反正出来工作之后根本就不会有人再看你本科多少分,关键是希望通过我这篇文章,彻底明白Linux如何玩,管道和线程是什么:


大家可以看到,这是在纯正Linux命令行系统跑通的程序。在a这个文件夹里面,有1.txt,2.txt,并且又有一个a文件夹(以此证明文件夹也能被完美复制),而在a/a里面又有一些东西,b文件夹里面一开始是什么都没有的,经过臭名远著的“线程并发的管道模拟拷贝程序”之后,b得到a文件夹里面的所有东西,同时按照李教授的要求,在各个文件名加上一个前缀。

首先,李教授要求,还要给管道设计一个数据结构是不对的。我本想这样做,但某度N下之后,发现各个网站都这样告诉我:


是的,管道就是一个通过《【Linux】利用文件标识符进行文件的读写》(点击打开链接)操作的一个Linux文件,毕竟Linux规定无论是一般的文件、还是管道,甚至设备都是文件。它是一个实打实,如果在图形界面,就是可以直接点开的文件,在命令行也能用《【Linux】vi/vim的使用》(点击打开链接)直接读,又不是在内存,是那些线性表、堆栈、树和图什么的,那还能写出管道的数据结构的吗?

我清楚网上有份以讹传讹的实验报告,在某度搜索“线程并发拷贝程序”会被这份早被转载了N次的程序刷屏,如下图所示:


大家还争相模仿,甚至有人还在某些知道网站提出如下的疑问:


-_-!有人帮你就真的是开国际玩笑了,就只有李教授才会让学生写一些这么无聊和无意义的程序,其它人怎么可能对此有所研究了。

更何况管道的本质是文件,而不是什么结构体。这个程序还睁大眼睛说瞎话,第30行,居然还用一个char *指向一个文件,说要模拟管道文件?就只有李教授才会信这些鬼东西的。要操作文件,你应该老老实实用文件相关的变量,谢谢。

这破程序自然是跑不通的,如下图所示,他连一些基本的函数用法,都用错:

更何况本身还有语法错误,如下图所示:


都不知道怎么会被多次转载,信为真理?

好了,你应该这样写,下面就是我自己写的程序,并且刚才大家也看到是跑通的:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<dirent.h>//输出文件信息
#include<sys/stat.h>//判断是否目录
#include<pthread.h>//使用线程
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>//文件控制open,close等用到
#define MAXSIZE 1024//定义各种存东西的数组(也叫缓冲区)大小
char *source_arr[MAXSIZE];//存放源文件路径的数组
char *destination_arr[MAXSIZE];//存放目标文件路径的数组
int source_arr_index=0;//存放源文件路径的数组的索引,就是for(int i=xx;..;..)那个i
int destination_arr_index=0;//存放目标文件路径的数组的索引
pthread_mutex_t mutex;//声明一个互斥锁mutex
int i=0;//多个线程函数用到这个i,用于记录是否复制完毕,因此作为全局变量处理~
const char* PIPEPATH="/tmp/pipe.txt";//管道对应的实体文件
char* PREFIX="教授要我加前缀";//李教授要在每一个文件名之前加的前缀
/*字符串处理函数*/
int endwith(char* s,char c){//用于判断字符串结尾是否为“/”与“.”
	if(s[strlen(s)-1]==c){
		return 1;
	}
	else{
		return 0;
	}
}
char* str_contact(char* str1,char* str2){//字符串连接
	char* result;
	result=(char*)malloc(strlen(str1)+strlen(str2)+1);//str1的长度+str2的长度+\0;
	if(!result){//如果内存动态分配失败
		printf("字符串连接时,内存动态分配失败\n");
		exit(1);
	}
	strcat(result,str1);
	strcat(result,str2);//字符串拼接
	return result;
}

/*遍历函数*/
int is_dir(char* path){//判断是否是目录
	struct stat st;
	stat(path,&st);
	if(S_ISDIR(st.st_mode)){
		return 1;
	}
	else{
		return 0;
	}
}
void read_folder(char* source_path,char *destination_path){//复制文件夹
	if(!opendir(destination_path)){
		if (mkdir(destination_path,0777))//如果不存在就用mkdir函数来创建
		{
		    printf("创建文件夹失败!");
		}
	}
	char *path;
	path=(char*)malloc(512);//相当于其它语言的String path="",纯C环境下的字符串必须自己管理大小,这里为path直接申请512的位置的空间,用于目录的拼接
	path=str_contact(path,source_path);//这三句,相当于path=source_path
	struct dirent* filename;
	DIR* dp=opendir(path);//用DIR指针指向这个文件夹
	while(filename=readdir(dp)){//遍历DIR指针指向的文件夹,也就是文件数组。
		memset(path,0,sizeof(path));
		path=str_contact(path,source_path);
		//如果source_path,destination_path以路径分隔符结尾,那么source_path/,destination_path/直接作路径即可 
		//否则要在source_path,destination_path后面补个路径分隔符再加文件名,谁知道你传递过来的参数是f:/a还是f:/a/啊?
		char *file_source_path;
		file_source_path=(char*)malloc(512);
		file_source_path=str_contact(file_source_path,source_path);
		if(!endwith(source_path,'/')){			
			file_source_path=str_contact(source_path,"/");
		}
		char *file_destination_path;
		file_destination_path=(char*)malloc(512);
		file_destination_path=str_contact(file_destination_path,destination_path);
		if(!endwith(destination_path,'/')){			
			file_destination_path=str_contact(destination_path,"/");			
		}		
		//取文件名与当前文件夹拼接成一个完整的路径
		file_source_path=str_contact(file_source_path,filename->d_name);
		
		if(is_dir(file_source_path)){//如果是目录
			if(!endwith(file_source_path,'.')){//同时并不以.结尾,因为Linux在所有文件夹都有一个.文件夹用于连接上一级目录,必须剔除,否则进行递归的话,后果无法想象!
				file_destination_path=str_contact(file_destination_path,filename->d_name);//对目标文件夹的处理,取文件名与当前文件夹拼接成一个完整的路径
				read_folder(file_source_path,file_destination_path);//进行递归调用,相当于进入这个文件夹进行遍历~
			}		
		}
		else{//否则,将源文件于目标文件的路径分别存入相关数组
			//对目标文件夹的处理,取文件名与当前文件夹拼接成一个完整的路径
			file_destination_path=str_contact(file_destination_path,PREFIX);//给目标文件重命名,这里示意如何加个前缀~^_^
			file_destination_path=str_contact(file_destination_path,filename->d_name);
			source_arr[source_arr_index]=file_source_path;
			source_arr_index++;
			
	
			destination_arr[destination_arr_index]=file_destination_path;
			destination_arr_index++;
			
		}
	}	
}

/*模拟管道的复制函数*/
void SIMpipecopy(char* source_path,char *destination_path){
    int fd[2];//文件标识符,用于模拟管道的两端
    char buffer[MAXSIZE+1];//用来接字符的缓冲区
	int size;//读入的文件长度
    if((fd[0]=open(PIPEPATH,O_CREAT|O_TRUNC|O_RDONLY,0777))<0){//匿名管道的第0个位置一定是读位置
		//O_CREAT如果指定文件不存在,则创建这个文件,O_EXCL如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
		//O_APPEND每次写操作都写入文件的末尾,O_TRUNC如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
		//O_RDONLY只读模式,O_WRONLY只写模式,O_RDWR读写模式
		//0777为最高权限
        perror("读管道创建失败!");
        exit(1);
    }
    if((fd[1]=open(PIPEPATH,O_CREAT|O_TRUNC|O_WRONLY,0777))<0){//匿名管道的第1个位置一定是写位置
		//0777为最高权限
        perror("写管道失败!");
        exit(1);
    }
	FILE *in,*out;//定义两个文件流 
    if((in=fopen(source_path,"r"))==NULL){//打开源文件的文件流  
        printf("源文件不存在,请检查路径输入是否存在!\n");  
        exit(1);  
    }  
    if((out=fopen(destination_path,"w"))==NULL){//打开目标文件的文件流  
        printf("创建目标文件流失败!\n");  
        exit(1);  
    }
	while((size=fread(buffer,1,1024,in))>0){//从源文件中读取数据,并写入管道,同时从管道读出数据,写入新文件  
		if((write(fd[1],buffer,size))<0){
			perror("写入管道失败!");
			exit(1);
		}
		if((size=read(fd[0],buffer,MAXSIZE))<0){
			perror("读入管道的内容失败!");
			exit(1);
		}else{
			buffer[size]='\0';//字符串数组封口	 
		}
        fwrite(buffer,1,size,out);//将缓冲区的数据写到目标文件中  
    }
    if(close(fd[0])<0||close(fd[1])<0) {
        perror("关闭管道失败!");
        exit(1);
    }	
	unlink(PIPEPATH);//删除管道
}

/*线程执行函数*/
void *thread_function(void *arg){
	while(i<destination_arr_index){  
		if(pthread_mutex_lock(&mutex)!=0){//对互斥锁上锁,临界区开始  
			printf("%s的互斥锁创建失败!\n",(char *)arg);  
			pthread_exit(NULL);  
		}  
		if(i<destination_arr_index){
			SIMpipecopy(source_arr[i],destination_arr[i]);//复制单一文件
			printf("%s复制%s到%s成功!\n",(char *)arg,source_arr[i],destination_arr[i]);
			i++;
			sleep(1);//该线程挂起1秒  
		}  
		else{//否则退出  
			pthread_exit(NULL);//退出线程  
		}  
		pthread_mutex_unlock(&mutex);//解锁,临界区结束  
		sleep(1);//该线程挂起1秒  
	}  
	pthread_exit(NULL);//退出线程  
}  

/*主函数*/
int main(int argc,char *argv[]){
	if(argv[1]==NULL||argv[2]==NULL){
		printf("请输入两个文件夹路径,第一个为源,第二个为目的!\n");
		exit(1);
	}
	char* source_path=argv[1];//取用户输入的第一个参数
	char* destination_path=argv[2];//取用户输入的第二个参数
	DIR* source=opendir(source_path);
	DIR* destination=opendir(destination_path);
	if(!source||!destination){
		printf("你输入的一个参数或者第二个参数不是文件夹!\n");
	}
	read_folder(source_path,destination_path);//进行文件夹的遍历
	/*线程并发开始*/
	pthread_mutex_init(&mutex,NULL);//初始化这个互斥锁  
	//声明并创建三个线程  
	pthread_t t1;  
	pthread_t t2;  
	pthread_t t3;  
	if(pthread_create(&t1,NULL,thread_function,"线程1")!=0){  
		printf("创建线程失败!程序结束!\n");  
		exit(1);  
	}  
	if(pthread_create(&t2,NULL,thread_function,"线程2")!=0){  
		printf("创建线程失败!程序结束!\n");  
		exit(1);  
	}  
	if(pthread_create(&t3,NULL,thread_function,"线程3")!=0){  
		printf("创建线程失败!程序结束!\n");  
		exit(1);  
	}
  
	pthread_join(t1,NULL);  
	pthread_join(t2,NULL);  
	pthread_join(t3,NULL);  
	//三个线程都完成才能执行以下的代码  
	pthread_mutex_destroy(&mutex);//销毁这个互斥锁  
	/*线程并发结束*/
	return 0;
}
这个程序是在的基本思想是上年的《【Linux】线程并发拷贝程序》( 点击打开链接)改来的,只是加入管道的模拟,大致是如下图所示:


是的,李教授就是这么无聊,明明将一个快速拷贝的、又是能由系统直接调配资源的程序,弄到大家都在挤一个管道在拷贝。

而这个管道是什么?大家在第17行可以看到了,实质上就是/tmp/pipe.txt,在/tmp/目录的一个txt!各个线程将自己要拷贝的内容写入这个pipe.txt,然后,另一方面的各个线程,将这个pipe.txt里面的东西,拷贝到要目的地。就是这么简单。

至于流程图:


我能画的就是这么多,像连程序都跑不通,还能画出各种各样的流程图,我从未就见过如此厚颜无耻之人!还有什么管道控制块?管道控制表。连某度表示都不知道这是什么!因为根本就没这东西!


因此,这个程序就这样,如果李教授非要认为自己的是权威,我也帮不了各位,只是希望大家能了解清楚什么是管道。

同时,也不妨提提题外话,你要写Linux的程序,可以直接在VMWare装个Ubuntu,具体见《【Linux】Ubuntu12.04的下载与安装》(点击打开链接)和《【Linux】在Ubuntu12.04安装VMware Tools》(点击打开链接)。然后可以在上面直接写。或者,如果Linux用不惯,可以在Windows用Notpad++或者直接记事本来写Linux的C,再用Winscp这玩意将你写好的东西扔进Linux,具体见《【Linux】用Winscp远程访问无图形界面的Linux系统》(点击打开链接),之后用gcc编译运行,Linux里面自带C语言的编译环境,但你要写Linux的C语言。

在实验室一般他会给你一个IP地址,用户名与密码连接Linux,你也可以在自己的虚拟机上实验一下,在VMWare开着Linux,一般情况可以不在Linux装sshd进程,就能用Putty直接连上了,然后命令行操作。其实Linux的命令行,你只要记住cd进入目录,ls读目录,gcc编译,直接打程序名运行程序就行,其余通通交给Winscp。反正我是这样玩的,真看不去命令行编程,除了写得慢,对我来说有什么好处?

好吧,祝大家能顺利通过李教授的评审,反正我能给大家做的就是这么多,以后也未必有时间和有兴趣和李教授玩下去了。

GitHub 加速计划 / li / linux-dash
6
1
下载
A beautiful web dashboard for Linux
最近提交(Master分支:3 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐