一、實驗目的:
練習用UNIX I/O進行文件讀寫的編程方法,用UNIX I/O函數管理文本信息、二進制數據、結構體數據,掌握UNIX I/O的基本編程方法。練習測時函數使用,通過測量UNIX I/O函數運行時間,建立UNIX I/O API函數基本開銷的概念。
二、實驗內容與要求:
先創建用戶家目錄下創建文件名爲“姓名+學號+04”的子目錄,作爲本次實驗目錄,本次實驗的所有代碼都放到該目錄下,要求將所有源代碼與數據文件打包成文件”學號-姓名-lab4.tar.gz”, 壓縮包與實驗報告分別上傳到指定目錄下。
任務1. 在當前用戶目錄下創建數據文件student.txt,文件的內部信息存儲格式爲Sname:S#:Sdept:Sage:Ssex,即“姓名:學號:學院:年齡:性別”,每行一條記錄,輸入不少於10條學生記錄,其中包括學生本人記錄。調用標準I/O庫編寫程序task41.c,從文件中查找Sdept字段值爲“計算機與網絡安全學院”的文本行,輸出到文件csStudent.txt中,保存時各字段順序調整爲S#:Sname:Sage: Ssex:Sdept。
提示:從終端讀入一個文本行到字符串 char buf[MAXSIZE]可調用函數可調用函數:
“fgets(buf, MAXSIZE, stdin);”,其中stdin是表示鍵盤輸入設備的文件指針。
代碼:
#include<stdio.h>
#include<string.h>
#define MAXSIZE 200
int main()
{
FILE* fp1, * fp2;
char buf[MAXSIZE];
char check[] = "計算機與網絡安全學院";
if ((fp1 = fopen("student.txt", "r")) == NULL)
{
printf(" Open Failed!");
return 0;
}
if ((fp2 = fopen("csStudent.txt", "a+")) == NULL)
{
printf(" Open Failed!");
return 0;
}
while (!feof(fp1))//文本結束時退出循環
{
fgets(buf, MAXSIZE, fp1);//一行行讀入
if (strstr(buf, check))//判斷是否屬於“計算機與網絡安全學院”
{
char* a[6];
char buffer[100];
a[0] = strtok(buf, ":");//切割字符串成五部分
a[1] = strtok(NULL, ":");
a[2] = strtok(NULL, ":");
a[3] = strtok(NULL, ":");
a[4] = strtok(NULL, ":");
a[4][3] = 0;//將換行符去掉
sprintf(buffer, "%s:%s:%s:%s:%s\n", a[1], a[0], a[3], a[4], a[2]);//格式化寫入buffer
fputs(buffer, fp2);//將buffer寫入文本csStudent
}
}
fclose(fp1);
fclose(fp2);
return 0;
}
任務2. 調用Unix I/O庫函數,編寫程序task42.c,從鍵盤讀入5個學生的成績信息,包括學號、姓名、語文、數學、英語,成績允許有一位小數,存入一個結構體數組,結構體定義爲:
typedef struct _subject {
char sno[20]; //學號
char name[20]; //姓名
float chinese; //語文成績
float math; //數學成績
float english; //英語成績
} subject;
代碼:
#include<stdio.h>
#include"wrapper.h"
typedef struct _subject {
char sno[20]; //學號
char name[20]; //姓名
float chinese; //語文成績
float math; //數學成績
float english; //英語成績
} subject;
void main() {
int fd = open("task42.txt", O_RDWR | O_CREAT | O_APPEND, 0777);
subject student[5];
subject stu[3];
int i;
for (i = 0; i < 5; i++) {
scanf("%s%s%f%f%f", student[i].sno, student[i].name, &student[i].chinese, &student[i].math, &student[i].english);
write(fd, &student[i], sizeof(student[i]));
}
lseek(fd, 0, SEEK_SET);
read(fd, &stu[0], sizeof(subject));
lseek(fd, sizeof(subject), SEEK_CUR);
read(fd, &stu[1], sizeof(subject));
lseek(fd, sizeof(subject), SEEK_CUR);
read(fd, &stu[2], sizeof(subject));
for (i = 0; i < 3; i++) {
printf("%s %s %.1f %.1f %.1f\n", stu[i].sno, stu[i].name, stu[i].chinese, stu[i].math, stu[i].english);
}
}
任務3(可選):在Linux環境下,可以調用庫函數gettimeofday測量一個代碼段的執行時間,請寫一個程序task43.c,測量read、write、fread、fwrite函數調用所需的執行時間,並與prof/gprof工具測的結果進行對比,看是否基本一致。並對四個函數的運行時間進行對比分析。
提示:由於一次函數調用時間太短,測量誤差太多,應測量上述函數多次(如10000次)運行的時間,結果纔會準確。
代碼:
#include<stdio.h>
#include"wrapper.h"
//struct timeval{
// long tv_sec; /*秒*/
// long tv_usec; /*微秒*/
//};
int main() {
struct timeval begin, end;
double write_time , fwrite_time , read_time , fread_time;
int i;
char buf[100]="you are good!";
int fd = open("test.txt",O_RDWR|O_CREAT|O_APPEND,0777);
gettimeofday(&begin,NULL);//10000次write
for(i =0;i<10000;i++){
write(fd,buf,1);
}
gettimeofday(&end,NULL);
write_time = end.tv_usec-begin.tv_usec;
printf("10000次write的write_time =%.1lf微秒\n",write_time);
lseek(fd,0,SEEK_SET);
gettimeofday(&begin,NULL);//10000次read
for(i =0;i<10000;i++){
read(fd,buf,1);
}
gettimeofday(&end,NULL);
read_time = end.tv_usec-begin.tv_usec;
printf("10000次read的read_time =%.1lf微秒\n",read_time);
close(fd);
FILE* fp1 = fopen("test.txt","w+");
gettimeofday(&begin,NULL);//10000次fwrite
for(i =0;i<10000;i++){
fwrite(buf,1,1,fp1);
}
gettimeofday(&end,NULL);
fwrite_time = end.tv_usec-begin.tv_usec;
printf("10000次fwrite的fwrite_time =%.2lf微秒\n",fwrite_time);
fclose(fp1);
FILE* fp2 = fopen("test.txt","r+");
gettimeofday(&begin,NULL);//10000次fread
for(i =0;i<10000;i++){
fread(buf,1,1,fp2);
}
gettimeofday(&end,NULL);
fread_time = end.tv_usec-begin.tv_usec;
printf("10000次fread的fread_time =%.2lf微秒\n",fread_time);
return 0;
}
fwrite和fread有自己的緩衝區,直到緩衝區寫滿或者指針指向文本結束位置時,纔會進行一次I/O操作,大大減少了write和read函數在內核空間和用戶空間之間的轉換,這種轉換會帶來非常大的cpu開銷,所以前者的效率更高。(截圖如下:)
附錄: multiply,使用prof/gprof測量程序運行時間
Linux/Unix環境提供了prof/gprof工具來收集一個程序各函數的執行次數和佔用CPU時間等統計信息,使用prof/gprof工具查找程序性能問題,要求編譯命令添加-p選項(prof)或-pg選項(gprof),程序執行時就會產生執行跟蹤文件mon.out(或gmon.out),再運行prof(或gprof)程序讀取跟蹤數據,產生運行報告。現在用gprof對以下程序各函數運行性能(佔用CPU時間)進行測量。先輸入程序源代碼:
#include <stdio.h>
int multiply_quick( int x, int y)
{
return x * y;
}
int multiply_slow(int x,int y)
{
int i, j, k;
for (i = 0, k = 0; i < x; i++)
k = k + y;
return k;
}
int main(int argc, char* argv[])
{
int i, j;
int x, y;
for (i = 0; i < 1000; i++) {
for (j = 0; j < 1000; j++) {
x = multiply_quick(i, j);
y = multiply_slow(i, j);
}
}
printf("x=%d, y=%d\n", x, y);
return 0;
}
multipy_slow花費了0.98s,multipy_quick花費了0.01s
任務4:在Linux系統環境下,編寫程序task44.c,對一篇英文文章文件的英文單詞詞頻進行統計。
(1)以“單詞:次數”格式輸出所有單詞的詞頻(必做)
(2)以“單詞:次數”格式、按詞典序輸出各單詞的詞頻(選做)
(3)以“單詞:次數”格式輸出出現頻度最高的10個單詞的詞頻
例如,若某個輸入文件內容爲:
GNU is an operating system that is free software—that is, it respects users’ freedom.
The development of GNU made it possible to use a computer without software that would trample your freedom.
則輸出應該是:
GNU:2
is:3
it:2
……
代碼:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include<malloc.h>
#include<string.h>
//#include<algorithm>
//#include <unistd.h>
#define MAXSIZE 200
typedef struct Word {
char word[30]; //設單詞最長爲30個字母
int count; //該單詞出現的次數
}WordNode;
typedef struct article {
WordNode Word[MAXSIZE]; //單詞的集合
int num; //這篇文章不同的英文單詞的種數
}Article;
void Insert_word(char* temp, Article* a) {
if (a->num == MAXSIZE) {
printf("單詞表已經滿,輸入失敗\n");
return;
}
if (temp == NULL)
return;
if (a->num == 0) { //情況1:單詞表裏暫時還沒有保存單詞
a->Word[0].word[0] = '\0';
strcpy(a->Word[0].word, temp);
a->Word[0].count = 1;
a->num = 1;
}
else {
int i;
for (i = 0; i < a->num; i++) {
if (strcmp(temp, a->Word[i].word) == 0) {//情況2:該單詞已經存在
a->Word[i].count++;
return;
}
}
a->Word[a->num].word[0] = '\0';
strcpy(a->Word[a->num].word, temp); //情況2:該單詞是首次輸入
a->Word[a->num].count = 1;
a->num++;
}
}
void divide_word(char* temp) {
int i;
if (temp == NULL) {
return;
}
for (i = 0; temp[i] != '\0'; i++) {
if (!((temp[i] >= 'A' && temp[i] <= 'Z') || (temp[i] >= 'a' && temp[i] <= 'z'))) {
temp[i] = ' ';
}
}
}
void paixu(Article* article) {
int i, j;
for (i = 0; i < article->num - 1; i++) {
for (j = 0; j < article->num - 1 - i; j++) {
if (article->Word[j].count < article->Word[j + 1].count) {
WordNode temp = article->Word[j];
article->Word[j] = article->Word[j + 1];
article->Word[j + 1] = temp;
}
}
}
}
void main()
{
Article article = {.num=0};
char temp[1000];
int i;
FILE* fd = fopen("./task44.txt", "r");
while (!feof(fd)) {
char* p;
fscanf(fd, "%s", temp);
divide_word(temp); //有可能一次讀入不止一個單詞,所以要分隔開
p = strtok(temp, " "); //strtok把temp分成多個字符串
do {
Insert_word(p, &article);
} while ((p = strtok(NULL, " ")) != NULL);//合適調整循環跳出條件
}
printf("按詞典序輸出,所有單詞的詞頻如下:\n");
for (i = 0; i < article.num; i++) {
printf("%s:%d\n", article.Word[i].word, article.Word[i].count);
}
printf("\n最高詞頻的前十個單詞:\n");
paixu(&article); //使用穩定排序法,才能符合題目要求
for (i = 0; i < 10; i++) {
printf("%s:%d\n", article.Word[i].word, article.Word[i].count);
}
}
後記
第一次寫C for Linux ,特別是gcc 編譯器特別不習慣,出現了很多沒見過的問題。
第一個就是
段錯誤 (核心已轉儲)
這種字樣,一般就是相當於vs上數組越界,訪問到不合理的內存這種情況,多去留意下自己write和read這些I/O類的函數,一行行仔細比對,一定能找到問題所在。
第二個就是c語言很久沒寫過了,很多語法都不記得了。還犯了在結構體裏初始化char數組的小失誤。
第三個就是某些頭文件在gcc編譯器裏找不到,只能自己編寫成庫文件,否則就只能換一個函數來使用。
收穫:
read和write是要進行內核模式和用戶模式之間的切換的,這種切換會帶來非常大的cpu開銷,所以相比之下,fwrite和fread有自己的緩衝區,緩衝區滿了或文件指針結束了才進行一次讀出/寫入操作,效率更高。(都是二進制的讀寫,不適合文本數據)
fgets和fputs,可以一行行的讀寫,適合文本數據。
當不足n個字符時,讀到\n就停止,會在末尾增添\0。如果剛剛好n個字符,優先讀入\0,不讀入\n。
read和write:(參數)
(int fd, const void *buf, size_t nbyte)
文件描述符,緩衝區,指定I/O字節數
fread和fwrite: (參數)
(const void *ptr, size_t size, size_t nmemb, FILE *stream)
目標元素數組的指針,每個元素大小(字節),元素個數(字節),文本指針
fgets和fputs: (參數)
(char *str, int n, FILE *stream)
字符數組的指針,讀取的最大字符數,文本指針
注意:FILE * 類型和int類型不相等
編譯.c文件時,命令中添加-p選項(prof)或-pg選項(gprof),程序執行時就會產生執行跟蹤文件mon.out(或gmon.out),再運行prof(或gprof)程序讀取跟蹤數據,產生運行報告。
strtok函數可以把字符串切割成若干個字符串。
任務四其實可以用字典樹來做,但是很久沒摸索過c語言了,實在吃力,不得不放棄了。
最後溫馨提醒:以上所有代碼都需要"wrapper.h"這個頭文件,如果需要進行代碼複用,親手嘗試代碼運行結果的同學,可以私信我轉發。