在Linux环境下实现对指定目录下的文本文件进行单词词频的统计。由于可能会涉及到很多文件,因此为了提高统计效率,采用多进程协同合作的方式实现词频统计。
目标
- 实现多个进程之间系统并行运行,保证执行结果的正确及高效
- 进程之间任务是不同的,包含两大类进程,父进程进行任务划分及汇总,子进程负责完成划分的任务
设计思路及实现
程序实现的过程中,一共有11个进程并行执行,其中使用的是10个统计进程和一个父进程,子进程的创建数目可以根据宏定义进行修改。父进程的任务分为两个阶段:
1. 统计开始前,根据输入的路径进行遍历,获取所有的给定路径及其子目录下满足条件的文件,并将这些文件的绝对路径存储在一个任务队列中,用于向子进程分发任务。
2. 统计开始后,等待子进程,若子进程完成所有分配给它的任务并正常退出之后,父进程开始汇总相应退出子进程的统计结果,直到所有的子进程都正常退出并且所有的汇总工作都完成之后,父进程输出汇总结果,结束程序的运行。
子进程的执行过程就比较单一,仅仅是根据父进程分配给自己的文件绝对路径,去打开这些文件并读取和统计,直到所有分配的文件都统计完成之后,该子进程就完成任务正常退出。
根据上述的设计思路,可以看出,关键问题在于任务的分配和父子进程间的同步以及信息交换。
在任务分配中,以文件为基本的分配单位,且假设文件是多于统计进程的数目,那么每个统计进程至少可以分配到1个文件。实现时是通过父进程的遍历,将文件绝对路径存储在一个共享vector变量中,这样所有的子进程都可以访问这个共享向量。父进程最后通过这个共享向量实现任务的均衡划分。
父进程主要完成两个方面的任务:一个是文件的遍历,一个是子进程统计结果的汇总。父进程的执行流程图如下:
任务划分过程代码:task_load 是通过遍历得到的所有的文件数目取整加1.
那么子进程所需要处理的任务数量就是在vector中从i*task\_load
一直到end 之间的所有文件,其中end的取值是所有文件数目与(i+1)*task\_load
中的较小值。
txt_files.size() < (i+1)*task_load ? txt_files.size():(i+1)*task_load
这样就可以保证vector中的所有任务都得到了处理并且不会出现交叠和越界的情况。
示例代码如下:
int task_load = txt_files.size()/PROCESS_NUM + 1;
for(i=0;i<PROCESS_NUM;i++){
//子进程执行代码片段
if(fork() == 0){
int exit_code = i;//getpid();
//每个子进程的任务量为:txt_files向量中位于 i*task_load 到 end之间的文件;
//其中end是(i+1)*task_load与txt_files.size()之间的较小值
int end = txt_files.size() < (i+1)*task_load ? txt_files.size():(i+1)*task_load;
DICT child_dict;
//char *wd;
char wd[WORD_LEN];
char buffer[BUFFER_SIZE + 1];
int r_fd;
int nread;
char get_file_path[FILE_PATH_LEN];
for(int j = i*task_load;j< end;j++){
memset(get_file_path,0,FILE_PATH_LEN);
int cpy_len = txt_files[j].length();
txt_files[j].copy(get_file_path,cpy_len,0);
//cout<<txt_files[j]<<" opened in process "<<i<<endl;
//get_file_path[]末尾补'\0'的操作可要可不要
//get_file_path[strlen(get_file_path)] = '\0';
cout<<"child "<<i<<" "<<get_file_path<<endl;
r_fd = open(get_file_path,O_RDONLY);
if(r_fd == -1 ){
printf("open file failed!\n");
exit(EXIT_FAILURE);
}
memset(buffer,0,BUFFER_SIZE);
nread = read(r_fd,buffer,BUFFER_SIZE);
if(nread < 0){
printf("read failed \n");
exit(EXIT_FAILURE);
}
//new
int index = 0;
int buf_len = strlen(buffer);
int word_len =0;
char temp_word[WORD_LEN];
bool flag = false;
while(nread > 0){
memset(wd,0,WORD_LEN);
while(index < buf_len){
if(flag){
//若出现单词分两次读取的情况
//一般单词被隔断,那么第二部分首字母一般不会是大写
//此处未对大小写进行判断
while(is_letter(buffer[index]) > 0){
if(is_letter(buffer[index]) == 1){
wd[word_len] = buffer[index];
}else
wd[word_len] = tolower(buffer[index]);
word_len++;
index++;
}
wd[word_len] = '\0';
strcat(temp_word,wd);
child_dict.add(temp_word,1);
memset(temp_word,0,WORD_LEN);
word_len = 0;
index++;
flag = false;
}
if(is_letter(buffer[index]) == 1){
//小写字母
wd[word_len] = buffer[index];
index++;
word_len++;
}else if(is_letter(buffer[index]) == 2){
//大写字母
wd[word_len] = tolower(buffer[index]);
index++;
word_len++;
}else{
//不在字母表中
wd[word_len+1] = '\0';
if(strlen(wd) > 0){
child_dict.add(wd,1);
}
memset(wd,0,WORD_LEN);
word_len = 0;
index++;
}
}
index = 0;
memset(buffer,0,BUFFER_SIZE);
nread = read(r_fd,buffer,BUFFER_SIZE);
buf_len = strlen(buffer);
if(word_len != 0){
//buffer数据读完,但是没有到达单词尾部
//单词被分成两次读到buffer中,需要做特殊处理
flag = true;
strcat(temp_word,wd);
word_len = 0;
}
}
close(r_fd);
nread = 0;
r_fd = -1;
//wd = NULL;//old
memset(wd,0,WORD_LEN);
}
//输出子进程的词频结果
printf("child %d result: ",i);
child_dict.display();
//统计完需要完成的所有文件内容自后,将统计内容序列化到临时文件;
//文件名从全局变量temp_files向量中获取
int c_len = temp_files[i].length();
//将temp_files[i