大连某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。反正我是这样玩的,真看不去命令行编程,除了写得慢,对我来说有什么好处?
好吧,祝大家能顺利通过李教授的评审,反正我能给大家做的就是这么多,以后也未必有时间和有兴趣和李教授玩下去了。