OS-Experiment for NUAA

文章目录

    • 文件读写
      • myecho
      • mycat
      • mycp
    • 多进程
      • mysys
      • sh3
    • 多线程
      • pi1
      • pi2
      • sort
      • pc1
      • pc2

文件读写

myecho

1.实现功能

myecho.c的功能与系统echo程序相同,接受命令行参数,并将参数打印出来。

2.实现思路

main函数有两个参数:argc和argv,其中argc记录了main 函数的命令行参数总个数,包括可执行程序名;argv[] 是一个字符串数组,每个元素指向一个参数,在命令行输入的字符串会被自动分割为字符串数组,其长度为argc,argv[0]=可执行程序名,argv[1…argc-1]=可执行程序参数,argv[argc] = NULL。

利用main函数的两个参数可以实现echo功能:首先根据argc的值判断是否有需要打印的字符串,如果argc==1,说明只有可执行程序名,没有参数,直接输出 Character not received! 并返回;

/* user haven't input any char */
if(argc == 1){
	printf("Character not received!\n");
	return 0;
}

如果不为0,输出 argv 字符串数组中的值即可。需要注意的是:argv[0]中存储的是可执行程序名称,不应该被输出,所以下标应从 1 开始:

/* print char from argv */
for(int i = 1 ;i < argc ; i++)
	printf("%s ",argv[i]);
printf("\n");

mycat

1.实现功能

mycat.c的功能与系统cat程序相同,mycat将指定的文件内容输出到屏幕,可以处理无参数和多参数的情况,要求使用系统调用open/read/write/close实现。

2.实现思路

mycat在输出文件内容时主要通过函数调用read/write实现,read函数返回值为rd,如果读取失败,返回-1,如果读取成功,返回实际读取的字节个数;返回0则代表读取到了文件末尾,可以用if语句作为是否读完的条件判断,再将读到的rd个字节用write写出到标准输出 STDOUT_FILENO

mycat在没有参数时,也就是argc==1,只有一个程序名,那程序会将用户在屏幕输入的内容原封不动打印出来,此时read的文件描述符参数就是STDIN_FILENO,表示从屏幕输入。由于输入的内容大小不确定,因此需要开辟一个不是很大的缓冲区来接收字符,通过循环来不断读,这样可以避免输入过大或过小造成的缓冲区溢出或浪费:

if(argc == 1)
{
	while(1)
	{
		int rd;
		char buf[10];
		if(rd = read(STDIN_FILENO,buf,MAX) > 0)
            write(STDOUT_FILENO,buf,rd);
		else
			break;
	}
}

mycat有多个参数时,会将这些文件依次打开并输出到标准输出,因此当 argc>1 时,对 argv 中的文件逐个打开,调用read 读取打开的文件,参数为fd,然后用 write 函数将当前文件的字符写到标准输出中去。同样,由于输入的内容大小不确定,因此需要开辟一个不是很大的缓冲区来接收字符,通过循环来不断读,这样可以避免输入过大或过小造成的缓冲区溢出或浪费:

/* show multiple files */
for(int i = 1;i < argc;i++){
	int fd = open(argv[i],O_RDONLY);
	if(fd < 0)
		panic("open file fail!");
	while(1)
	{
		char buf[10];
		memset(buf,0,MAX);
		int rd;
		if((rd = read(fd,buf,MAX)) > 0)
			/* write to screen */
			write(STDOUT_FILENO,buf,rd);
		else
			break;
	}
	close(fd);
}

mycp

1.实现功能

mycp.c的功能与系统cp程序相同, 将源文件复制到目标文件,要求使用系统调用open/read/write/close实现

2.实现思路

对于mycp,和之前两个程序不同,mpcp需要有可执行程序名argv[0],源文件argv[1],目的文件argv[2],当它的参数小于3个的时候一定是参数错误,因此要先判断 if(argc < 3)

if(argc != 3)
	panic("Parameter error!");

对于目的文件,可能不存在,这时应该创建一个新的文件,因此open函数用 O_CREAT,mode 打开,赋予权限为0777,若两次执行程序都移动到同一文件中,需要覆盖操作,因此open函数还需 O_TRUNC 参数。当文件都打开/创建成功时,调用 read 函数对源文件逐个读取,调用 write 函数写入目标文件中。这里write的文件描述符不再是标准输出STDOUT,而是目标文件的fd2:

int fd1 = open(argv[1], O_RDONLY);
mode_t mode=0777;
int fd2 = open(argv[2], O_RDWR | O_TRUNC | O_CREAT,mode);
if(fd1 < 0 || fd2 < 0)
	panic("Open file 1 failed!");
char buf[MAX];
memset(buf,0,MAX);
int rd;
while(1)
{
	rd = read(fd1,buf,MAX);
	if(rd > 0)
		write(fd2,buf,rd);
	else
		break;
}
close(fd1);
close(fd2);

多进程

mysys

1.实现功能

mysys的功能与系统函数system相同,要求用进程管理相关系统调用自己实现一遍,使用fork/exec/wait系统调用。

3.实现思路

mysys的实现主要基于以下三个函数:execvp、strtok、fork

程序main函数读入读入一个字符串 command,传给mysys模块,在mysys模块中,先检查命令长度是否为0,如果没有要执行的命令,则直接返回退出;定义一个myargc表示这个命令的参数个数,定义一个myargv表示参数列表,调用change模块进行字符串处理,返回值为myargc参数个数:

int t=strlen(command);
/* if no command, continue to exec the next one */
if(t == 0)
	return;
int myargc;
char *myargv[MAXARG];
myargc = change(command,myargc,myargv);
myargv[myargc]=NULL;

change模块:strtok不可以直接对字符串常量进行分割,要用strncpy得到一个char型数组buf, 对这个buf数组进行处理即可。用空格作为分隔符,首次调用时,第一个参数指向要分解的字符串,之后再次调用要把第一个参数设成NULL,将分割后的子字符串存入 myargv字符数组中,每次myargc++:

int change(char *command,int myargc,char *myargv[])
{
        char buf[MAX];
        memset(buf,0,MAX);
        strncpy(buf,command,strlen(command));
        char *delim=" ";
        myargc=0;
        myargv[myargc]=strtok(buf,delim);
        while(myargv[++myargc]=strtok(NULL,delim));
        return myargc;
}

分割字符串成功后就开始创建一个子进程,调用子进程执行模块chile,让子进程执行execvp,父进程等待子进程结束再退出:

pid_t pid;
pid = fork();
if(pid == 0)
	child(myargc,myargv);
else
	wait(NULL);

子进程模块调用execvp,第一个参数为可执行程序名,存在了myargv[0]中,之后的参数列表通过myargv传入,装入可执行程序,执行命令:

void child(int myargc,char *myargv[])
{
        int status = execvp(myargv[0],myargv);
        if(status < 0)
                Perror("Arguments fault!");
        exit(1);
}

sh3

1.实现功能

实现管道和文件重定向。

2.实现构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RfaqS1mh-1593603294451)(C:\Users\lenovo\Desktop\2.PNG)]

3.实现思路

main函数中调用mysys模块:

int main()
{
        while (1) mysys();
        return 0;
}

mysys模块实现的是内置命令的处理,其他的命令交给parse_command模块。

首先通过fgets读取字符串,并去掉最后一个换行符变为\0:

char command[ARG_MAX + 1], argv[ARG_MAX + 1];
memset(command, 0, ARG_MAX + 1);
printf("> ");
fgets(command, ARG_MAX, stdin);
command[strlen(command) - 1] = '\0';

先把第一个空格之前的子串分离出来分类讨论

如果是内置命令,就不用fork一个子进程,而是直接执行,exit直接调用exit(0)退出,cd调用chdir(path)切换路径;

如果不是内置命令,就交给parse_command来分析命令:

char *p;
p = command;
/* first command */
char temp[10];
memset(temp,0,10);
int i=0,j=0;
while((*p!=' ')&& (*p))
{
	temp[i++]=command[j++];
	++p;
}
if(strcmp(temp,"exit") == 0)
	exit(0);
else if(strcmp(temp,"cd") == 0)
{
	++p;
	int state = chdir(p);
	char buf[ARG_MAX];
	memset(buf,0,ARG_MAX);
	printf("now in : %s\n",getcwd(buf,ARG_MAX));
}
else
{
	p=command;	
    parse_commands(p);
}

parse_command模块中,用 | 作为分隔符,解析成子命令,并将子命令的个数记录在 cmd_count中,子命令名记录在cmds[]中,以便之后的执行。用一个for循环遍历这个command字符串。

cat output.txt为例来说明这个过程:

在最开始,把第一个原始命令加入cmds,并把cmd_count+1。当遍历到command[i] == '|' 时,让指针p指向 ‘|’ 前一个字符,去掉多余的空格之后,用\0来截断,分离出第一个 ‘|’左边的部分,这时command命令就变成了cat

cmds[cmd_count++] = command;
int len = strlen(command);
for (i = 0; i < len; ++i)
{
	if (command[i] == '|')
	{
    /* deal left and remove space */
    p = command + i - 1;
	while (*p && (*p == ' '))
	{
        *p = '\0';
		--p;
	}
    ......

再将指针p定位到 ‘|’ 右边第一个字符的位置,去掉多于的空格后,p就指向了 sort | uniq | cat >output.txt这一字符串,写进cmds,并把cmd_count+1:

......
/* deal right and remove space */
p = command + i + 1;
while (*p && (*p == ' '))
{
	*p = '\0';
	++p;
}
/* cmd add one */
cmds[cmd_count++] = p;
......

继续执行循环,当再次遇到 ‘|’ 时,同样将p指向’|'之前的一个字符,由于cmds[cmd_count++]是指向p的,去掉空格并用\0截断之后,cmd[1]就会自动变成sort,p再次指向unique| cat >output.txt,并把unique| cat >output.txt写进cmds,cmd_count+1…执行完这个循环后,cmd_count=4,

cmd[0]=cat

cmd[1]=sort

cmd[2]=uniq

cmd[3]= cat >output.txt

当然,这只是针对这一个特例的情况。要想实现所有类型的命令,就要先判断cmd_count的值,根据不同的count交给split_file模块进行不同的处理。

先提前对于split_file模块进行说明,split_file模块的功能是解析重定向命令

如存在文件重定向,把它们分离出来记录到char *file_input, char *file_output变量中并打开,并且使用文件描述符;否则使用管道描述符。

函数原型为:void split_file(char *command, int pipe_read, int pipe_write)

command是上一步parse_command的分析结果,pipe_read和pipe_write是读描述符和写描述符,可以有也可以没有,没有某一个描述符时用-1表示。

先定义一些变量:

int fd_read = -1, fd_write = -1;
int i, len = strlen(command);
int flagout = 0, flagin = 0;
char *file_input, *file_output;

fd_read和fd_write或是一个打开的文件的描述符,或是一个管道的读端或写端,或两者都不是(-1);flagin/flagout用来标识有无’<’、’>’;file_input/file_output存储文件名。

命令从头开始遍历,遇到’>‘时,说明需要重定向输出到指定的文件,用一个指针p指向’>'后的第一个字符,去掉多于空格后向后减,将command截断,用一个指针s去掉多余空格后指向输出文件,赋给file_output,并去掉多于的换行符等。

例如对cmd[3]即cat >output.txt处理完后,command变成cat,file_output变成output.txt,最后将flagout置位1,表明该命令中有需要重定向输出的文件。

for (i = 0; i < len; ++i)
{''
	/* find dis file */
    if (command[i] == '>')
    {
		p = command + i - 1;
        while (*p == ' ')
			*(p--) = '\0';
        command[i] = '\0';
	    char *s = command +i+1;
	    while (*s == ' ')
			++s;
    	file_output = s;
    	for (int j = 0; j < strlen(s); ++j)
			if (s[j] == ' ' || s[j] == '\n')
				s[j] = '\0';
        flagout = 1;
}
    ......

遇到 ‘<’ 的情况类似:例如对cat

	......
/* find src file */
else if (command[i] == '<')
{
	char *s = command + i + 1;
	while (*s == ' ')
		++s;
	file_input = s;
	for (int j = 0; j < strlen(s); ++j)
		if (s[j] == ' ' || s[j] == '\n')
			s[j] = '\0';
	p = command + i - 1;
	while (*p == ' ')
		*(p--) = '\0';
	flagin = 1;
}

之后对fd_read/fd_write进行赋值,如果flagin/flagout有效,表明这条命令中存在文件重定向,就打开它并作为该文件的描述符;

如果flagin/flagout无效,但是pipe_read/pipe_write有效,说明这是一条管道命令,那么把这个管道描述符赋给fd_read/fd_write;

如果二者皆无,说明只是一条普通命令,当做exec_simple处理,分割好输入输出文件之后,就交给exec_pipe模块。

再回到parse_command模块中,该模块剩下的部分就是根据不同的cmd_count进行不同情况的分割文件,如果cmd_count == 1,说明不是一条管道命令,就向split_file传参数(-1,-1)表示没有管道:

/* not a pipe command */
if (cmd_count == 1)
{
	split_file(cmds[0], -1,-1);
    return;
}

如果包含管道命令就创建一个管道,并返回读端和写段,从cmd[0]开始遍历,由于执行第一个子命令时,只有后面有管道,所以用不到read,将对应参数置位-1即可,即

split_file(cmds[i], -1, fd_pipe[1]);

执行最后一个子命令时只有前面有管道,所以用不到write,将对应参数位置置位-1,即

split_file(cmds[i], fd_pipe[0], -1);

注意,对于非头尾的命令,可能前面有管道,后面也有管道,某一个子命令对应的子进程会用了两个管道,那么要在进入下一个子进程之前,抛弃老管道写端,建立新管道写端,即

split_file(cmds[i], fd_tmp, fd_pipe[1]);

将写描述符与上一条命令产生的的读描述符传入重定向解析函数:

/* pipe command */
pipe(fd_pipe);
for (i = 0; i < cmd_count; ++i)
{
	if (i == 0)
    /* input to pipe,close fd[0] */
    	split_file(cmds[i], -1, fd_pipe[1]);
    else if (i != cmd_count - 1)
    {
    	fd_tmp = fd_pipe[0];
	    status = pipe(fd_pipe);
        if (fd_tmp == -1)
        {
        	printf("can't open pipe!\n");
            return;
        }
        split_file(cmds[i], fd_tmp, fd_pipe[1]);
     }
	else
       	/* output from pipe,close fd[1] */
       split_file(cmds[i], fd_pipe[0], -1);
}

至此,parse_command对于’|'的处理结束,调用split_file分类处理完并拿到重定向输入输出文件描述符后,就会转向exec_pipe模块,这一模块实现创建子进程执行命令。首先创建子进程,父进程什么都不做,关闭不必要的读写端,等待子进程结束,为下一个子进程做准备:

pid_t pid;
pid = fork();
if (pid != 0)
{
	if (fd_read != -1)
		close(fd_read);
    if (fd_write != -1)
        close(fd_write);
     wait(NULL);
}

每一个子命令对应一个子进程,fd_read != -1,说明需要重定向,或将标准输出重定向到输入文件,或将标准输出重定向到管道的读端;

fd_write != -1,说明需要重定向,或将标准输出重定向到输出文件,或将标准输出重定向到管道的写端;

例如当命令为cat /etc/passwd wc | -l时,会将标准输入重定向到管道的入口,将cat /etc/passwd写进管道,然后执行wc | -l 对管道内容操作。

重定向处理完成后,就可以进行execvp装入可执行程序了:

else
{
	if (fd_read != -1)
    {
    	close(STDIN_FILENO);
        status = dup2(fd_read, STDIN_FILENO);
        close(fd_read);
        if (status < 0)
        	printf("%s dup stdin failed!\n", argv[0]);
    }
    if (fd_write != -1)
     {
     	close(STDOUT_FILENO);
        status = dup2(fd_write, STDOUT_FILENO);
        close(fd_write);
        if (status < 0)
        	printf("%s dup stdout failed!\n", argv[0]);
     }
    argc=change(command, argc, argv);
    argv[argc] = NULL;
    if (execvp(argv[0], argv) == -1)
    	printf("%s exec failed!\n", argv[0]);
    exit(0);
}

多线程

pi1

1.实现功能

莱布尼兹级数公式: 1 - 1/3 + 1/5 - 1/7 + 1/9 - … = PI/4,主线程创建1个辅助线程,主线程计算级数的前半部分,辅助线程计算级数的后半部分,主线程等待辅助线程运行结束后,将前半部分和后半部分相加。

2.实现思路

主线程创建一个子线程,让它从compute处开始执行,计算级数前半部分

pthread_t tid;
int state = pthread_create(&tid,NULL,&compute,NULL);
if(state != 0)
{ 
	printf("create error\n");
	return 0;
}

compute计算部分无参数,迭代次数越大,结果越精确,这里accy=1e8,前半段计算到1e4,注意类型转化:

void * compute(void * arg)
{
	int N=accy/2;
	for(int i = 1;i < N;i = i + 2)
	{
		double tmp = (double)1/i;
		if(((i+1)/2)%2 == 0)
			tmp = -tmp;
		worker_output += tmp;
	}
}

主线程几乎做一样的步骤,只不过是从1e4计算到accy:

int N = accy/2;
for(int i = N;i <= accy;i = i+2)
{
	double tmp = (double)1/i;
    if(((i+1)/2)%2 == 0)
        tmp = -tmp;
    master_output += tmp;
}

为了防止主线程提前退出,因此要等待子线程完成,再将前半部分和后半部分的值合并,输出:

pthread_join(tid, NULL);
sum = (worker_output+master_output);
printf("Leibniz series = %.15lf\n",sum);
printf("PI = %.15lf\n",sum*4);

pi2

1.实现功能

与上一题类似,但本题更加通用化,能适应N个核心,主线程创建N个辅助线程,每个辅助线程计算一部分任务,并将结果返回。主线程等待N个辅助线程运行结束,将所有辅助线程的结果累加

本题要求 1: 使用线程参数,消除程序中的代码重复

本题要求 2: 不能使用全局变量存储线程返回值

2.实现思路

定义NR_TOTAL表明全部计算量,NR_CPU表示内核数目,NR_CHILD表明每个核的计算量:

#define NR_TOTAL 100000000
#define NR_CPU 8
#define NR_CHILD (NR_TOTAL/NR_CPU)

定义线程参数,记录每个核计算部分的开始和起始位置以及计算结果:

struct param{
	int start;
	int end;
};

struct result{
	double res=0.0;
};

主线程创建NR_CPU个线程,并为每一个线程分配任务,把总任务分成等量的NR_CPU份,启动每一个线程,并把线程参数传给compute:

pthread_t tid[NR_CPU];
struct param params[NR_CPU];
for( int i = 0;i < NR_CPU;i++)
{
	struct param *param;
	param = &params[i];
    param->start = i * NR_CHILD;
    param->end = (i + 1) * NR_CHILD;
    int state = pthread_create(&tid[i], NULL, compute, param);
	if(state != 0)
	{
		printf("create error!\n");
		return 0;
	}
}

compute计算部分一定要将接收到的参数进行强制类型转换,计算的结果要保存在result结构体中,保存之前要为它开辟空间,否则没有空间存储结果,最后将result结果返回:

void * compute (void * arg)
{
	struct param *param;
	struct result *result;
    double sum = 0.0;
    param = (struct param *)arg;
    for (int i = param->start; i < param->end; i++)
    {
		if(i&1==1)
		{
			double tmp=(double)1/i;
			if(((i+1)/2)%2 == 0)
				tmp = -tmp;
			sum +=tmp;
		}
	}
    result = malloc(sizeof(struct result));
    result->res = sum;
    return result;
}

主线程不做任何计算,只负责分配任务,等所有子线程结束后,调用 pthread_join汇总结果,并释放result占用的空间,打印输出:

double sum = 0.0;
for (int i = 0; i < NR_CPU; i++) 
{
        struct result *result;
        pthread_join(tid[i], (void **)&result);
        sum += result->res;
        free(result);
}
printf("Leibniz series = %.15lf\n",sum);
printf("PI = %.15lf\n", sum*4);

sort

1.实现功能

主线程创建两个辅助线程,辅助线程1使用选择排序算法对数组的前半部分排序,辅助线程2使用选择排序算法对数组的后半部分排序,主线程等待辅助线程运行结束后,使用归并排序算法归并子线程的计算结果

本题要求 : 使用线程参数,消除程序中的代码重复

2.实现思路

定义NR_TOTAL表明全部计算量,NR_CPU表示内核数目,NR_CHILD表明每个核的计算量,依据题目要求,NR_CPU的数量为2,创建两个子线程,数组元素个数为20:

#define NR_TOTAL 20
#define NR_CPU 2
#define NR_CHILD (NR_TOTAL/NR_CPU)

用rand函数产生随机数组:

/* Randomly generated array */
for(int i = 0;i<NR_TOTAL;i++)
{
	array[i]=(rand() % N);
	printf("%d ",array[i]);
}

主线程为这两个线程分配任务,启动每一个线程,并把线程参数传给compute:

for (i = 0; i < NR_CPU; i++) 
{
	struct param *param;
    param = &params[i];
    param->array = array;
    param->start = i * NR_CHILD; 
    param->end = (i + 1) * NR_CHILD;
	/* child thread do select sort */
    pthread_create(&workers[i], NULL, compute, param);
}

这两个子线程的计算部分都调用了选择排序函数,只不过第一个线程计算数组前半段,第二个线程计算数组后半段,根据param的start和end值来标识:

void *compute(void *arg)
{
    struct param *param;
    int i;
    param = (struct param *)arg;
	select_sort(array,param->start,param->end);
    printf("\n");
}

void select_sort(int A[],int start,int end)
{
	for(int i = start; i < end; i++)
	{
		int k = i;
		for(int j = i; j < end; j++)
			if(A[j] < A[k]) 
				k=j;
		int temp = A[i]; 
		A[i] = A[k];
		A[k] = temp; 
	}
}

主线程要等待两个子线程排序完毕:

for (i = 0; i < NR_CPU; i++) 
        pthread_join(workers[i], NULL);

主线程再归并数组的前半部分和后半部分,直接在主线程里调用自定义的Merge函数即可将两个选择排序的结
果归并排序完成:

/* parent thread do merge sort */
merge_sort(array,0,NR_TOTAL-1);
printf("After sort : \n");
for (int i=0;i<NR_TOTAL;i++)
	printf("%d ",array[i]);

pc1

1.实现功能

系统中有3个线程:生产者、计算者、消费者,系统中有2个容量为4的缓冲区:buffer1、buffer2,生产者生产’a’、‘b’、‘c’、‘d’、‘e’、‘f’、‘g’、'h’八个字符,放入到buffer1,计算者从buffer1取出字符,将小写字符转换为大写字符,放入到buffer2,消费者从buffer2取出字符,将其打印到屏幕上。

3.实现思路

使用长度为4的两个数组表示共享缓冲区,生产者生产八个字符,放入到buffer1,计算者从 buffer1 取出字符,将小写字符转换为大写字符,放入到 buffer2,消费者从 buffer2 取出字符,将其打印到屏幕上;变量out1,out2分别为两个共享缓冲区的读指针,变量in1,in2分别为两个共享缓冲区的写指针:

#define CAPACITY 4
char buffer1[CAPACITY];
char buffer2[CAPACITY];

int in1=0,in2=0;
int out1=0,out2=0;

由于有两个共享缓冲区,所以要用flag来标识是1号还是2号,in指针和out指针相同时,缓冲区为空;n指针和out指针相邻时,缓冲区为满,用求模保证数组不越界:

int buffer_is_empty(int flag)
{
    	if(flag == 1)
        	return in1 == out1;
    	if(flag == 2)
        	return in2 == out2;
}

int buffer_is_full(int flag)
{
    	if(flag == 1)
        	return (in1 + 1) % CAPACITY == out1;
    	if(flag == 2)
        	return (in2 + 1) % CAPACITY == out2;
} 

对缓冲区的生产/消费动作要根据flag判断是对哪个缓冲区操作,get_item获取out指针指向的元素同时,移动out指针指向下一项;put_item将元素放置在in指针指向的位置同时,移动in指针指向下一项:

char get_item(int flag)
{
    	char item;
    	if(flag == 1){
        	item = buffer1[out1];
        	out1 = (out1 + 1) % CAPACITY;
    	}
    	if(flag == 2){
        	item = buffer2[out2];
        	out2 = (out2 + 1) % CAPACITY;
    	}
    	return item;
}

void put_item(char item, int flag)
{
    	if(flag == 1){
        	buffer1[in1] = item;
        	in1 = (in1 + 1) % CAPACITY;
    	}
    	if(flag == 2){
        	buffer2[in2] = item;
        	in2 = (in2 + 1) % CAPACITY;
    	}
}

定义互斥信号量mutex用于进程间互斥,定义条件变量pthread_cond_t用于进程间同步:

pthread_mutex_t mutex1,mutex2;
pthread_cond_t wait_empty_buffer1,wait_empty_buffer2;
pthread_cond_t wait_full_buffer1,wait_full_buffer2;

生产者的任务是生产八个字符,放入到buffer1,为了实现互斥,在对缓冲区操作前必须加锁,为了避免缓冲区1满的情况,要先试探是否还有空间,再选择等待还是继续执行。生产结束后,释放一个满缓冲区,并解锁以唤醒计算者:

void *produce(void *arg)
{
    	char item;
    	for(int i = 0;i < ITEM_COUNT;i++){
        	pthread_mutex_lock(&mutex1);
			/* producer use the first buffer */
        	while(buffer_is_full(1))
            	pthread_cond_wait(&wait_empty_buffer1, &mutex1);
        	item = 'a' + i;
        	put_item(item,1);
        	printf("produce item:%c\n",item);
        	pthread_cond_signal(&wait_full_buffer1);
        	pthread_mutex_unlock(&mutex1);
    	}
    	return NULL;
}

计算者从buffer1取出字符,将小写字符转换为大写字符,放入到buffer2,先作为 buffer1 的消费者,给 buffer1 加锁并取数,将小写字母变成大写字母;最后再作为buffer2 的生产者,给 buffer2 加锁并存数,完成对两个缓冲区的存取:

void *compute(void *arg)
{
    	char item;
    	for(int i = 0;i < ITEM_COUNT;i++)
        {
        	pthread_mutex_lock(&mutex1);
			/* computer get item from buf1 */
       	 	while(buffer_is_empty(1))
          	  pthread_cond_wait(&wait_full_buffer1, &mutex1);
        	item = get_item(1);
        	pthread_cond_signal(&wait_empty_buffer1);
        	pthread_mutex_unlock(&mutex1);
			/* change to capital */
        	item -= 32;
			pthread_mutex_lock(&mutex2);
			/* and put in to buf2 */
        	while(buffer_is_full(2))
            	pthread_cond_wait(&wait_empty_buffer2, &mutex2);
        	put_item(item,2);
        	printf("    compute put item:%c\n", item);
        	pthread_cond_signal(&wait_full_buffer2);
        	pthread_mutex_unlock(&mutex2);
    	}
   	 	return NULL;
}

消费者作为 buffer2 的消费者,给 buffer2 加锁并取数字,取完后要释放一个空缓冲区并解锁唤醒计算者:

void *consume(void *arg)
{
    	char item;
    	for(int i = 0;i < ITEM_COUNT;i++){
        	pthread_mutex_lock(&mutex2);
			/* consumer get item ,use the second buffer */
        	while(buffer_is_empty(2))
            		pthread_cond_wait(&wait_full_buffer2, &mutex2);
        	item = get_item(2);
        	printf("            comsume item:%c\n", item);

        	pthread_cond_signal(&wait_empty_buffer2);
        	pthread_mutex_unlock(&mutex2);
    	}
    	return NULL;
}

在主线程中创建三个线程,分别用于承担生产者,计算者与消费者:

pthread_t tids[3];
pthread_create(&tids[0],NULL,produce,NULL);
pthread_create(&tids[1],NULL,compute,NULL);
pthread_create(&tids[2],NULL,consume,NULL);

定义两个锁用于线程间互斥:

pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);

定义四个条件变量用于线程间同步:

pthread_cond_init(&wait_empty_buffer1, NULL); 
pthread_cond_init(&wait_full_buffer1, NULL);
pthread_cond_init(&wait_empty_buffer2, NULL);
pthread_cond_init(&wait_full_buffer2, NULL);

再将三个进程都调用 pthread_join()函数等待线程结束:

for(int i = 0;i < 3;i++)
        pthread_join(tids[i],NULL);

pc2

1.实现功能

功能和前面的实验相同,使用信号量解决

2.实现思路

实现的时候利用信号量,使用条件变量实现信号量sema_t,value记录了信号量的值

typedef struct{
    int value;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
}sema_t;

如果信号量的值小于等于0,则等待条件变量,将信号量的值减一:

void sema_wait(sema_t *sema)
{
    pthread_mutex_lock(&sema->mutex);
    while(sema->value <= 0)
        pthread_cond_wait(&sema->cond, &sema->mutex);
    sema->value--;
    pthread_mutex_unlock(&sema->mutex);
}

对共享缓冲区的操作结束后,将信号量的值加一,唤醒等待条件变量的线程:

void sema_signal(sema_t *sema)
{
    pthread_mutex_lock(&sema->mutex);
    ++sema->value;
    pthread_cond_signal(&sema->cond);
    pthread_mutex_unlock(&sema->mutex);
}

定义两个信号量 mutex_sema1,mutex_sema2,分别对(生产者-计算者)与(计算者-消费者)进行线程间互斥:

sema_t mutex_sema1,mutex_sema2;

定义了四个信号量 对共享变量 buffer1,buffer2 进行线程间同步。

sema_t empty_buffer_sema1,empty_buffer_sema2;
sema_t full_buffer_sema1,full_buffer_sema2;

在生产者、计算者、消费者的函数中,先等待互斥信号量(上锁),再获取同步信号量,对 buffer 中的数据进行操作后,释放互斥信号量及解锁。要注意,需要先获取同步信号量再对互斥信号量进行上锁,不然可能造饥饿的现象:

void *consume(void *arg){
    char item;
    for(int i = 0;i < ITEM_COUNT;i++)
    {
		/* consumer get item ,use the second buffer */
       	sema_wait(&full_buffer_sema2);
		sema_wait(&mutex_sema2);
        item = get_item(2);
        printf("            comsume item:%c\n", item);
		sema_signal(&mutex_sema2);
        sema_signal(&empty_buffer_sema2);
    }
    return NULL;
}

void *produce(void *arg){
    char item;
    for(int i = 0;i < ITEM_COUNT;i++)
    {
		/* producer use the first buffer */
        sema_wait(&empty_buffer_sema1);
        sema_wait(&mutex_sema1);
        
        item = 'a' + i;
        put_item(item,1);
        printf("produce item:%c\n",item);
        	
        sema_signal(&mutex_sema1);
        sema_signal(&full_buffer_sema1);
    }
    return NULL;
}

void *compute(void *arg){
    char item;
    for(int i = 0;i < ITEM_COUNT;i++)
    {
		/* computer get item from buf1 */
        sema_wait(&full_buffer_sema1);
        sema_wait(&mutex_sema1);
        
        item = get_item(1);
        sema_signal(&mutex_sema1);
        sema_signal(&empty_buffer_sema1);
		/* change to capital */
        item -= 32;

        sema_wait(&empty_buffer_sema2);
        sema_wait(&mutex_sema2);
        /* and put in to buf2 */
        put_item(item,2);
        printf("    compute put item:%c\n", item);
       
        sema_signal(&mutex_sema2);
        sema_signal(&full_buffer_sema2);
    }
    return NULL;
}

main 函数中开启三个线程分别对应生产者、计算者、消费者,再对两个互斥信号量以及四个同步信号量进行初始化,调用 pthread_join 函数等待三个进程的结束即可:

/* create three thread */
pthread_t tids[3];

sema_init(&mutex_sema1, 1);
sema_init(&mutex_sema2, 1);
sema_init(&empty_buffer_sema1,CAPACITY - 1);
sema_init(&full_buffer_sema1,0);
sema_init(&empty_buffer_sema2,CAPACITY - 1);
sema_init(&full_buffer_sema1,0);

pthread_create(&tids[0],NULL,produce,NULL);
pthread_create(&tids[1],NULL,compute,NULL);
pthread_create(&tids[2],NULL,consume,NULL);
for(int i = 0;i < 3;i++)
	pthread_join(tids[i],NULL);

ll_buffer_sema1);
sema_wait(&mutex_sema1);

    item = get_item(1);
    sema_signal(&mutex_sema1);
    sema_signal(&empty_buffer_sema1);
	/* change to capital */
    item -= 32;

    sema_wait(&empty_buffer_sema2);
    sema_wait(&mutex_sema2);
    /* and put in to buf2 */
    put_item(item,2);
    printf("    compute put item:%c\n", item);
   
    sema_signal(&mutex_sema2);
    sema_signal(&full_buffer_sema2);
}
return NULL;

}


main 函数中开启三个线程分别对应生产者、计算者、消费者,再对两个互斥信号量以及四个同步信号量进行初始化,调用 pthread_join 函数等待三个进程的结束即可:

```c
/* create three thread */
pthread_t tids[3];

sema_init(&mutex_sema1, 1);
sema_init(&mutex_sema2, 1);
sema_init(&empty_buffer_sema1,CAPACITY - 1);
sema_init(&full_buffer_sema1,0);
sema_init(&empty_buffer_sema2,CAPACITY - 1);
sema_init(&full_buffer_sema1,0);

pthread_create(&tids[0],NULL,produce,NULL);
pthread_create(&tids[1],NULL,compute,NULL);
pthread_create(&tids[2],NULL,consume,NULL);
for(int i = 0;i < 3;i++)
	pthread_join(tids[i],NULL);

需源码可滴滴

你可能感兴趣的:(OS-Experiment for NUAA)