1.1 CPU
1.1.1 程序 cpu.c
1.使用vim编辑器创建cpu.c文件并进行编写
vim cpu.c
#include <stdio.h> #include <stdlib.h> #include "common.h" int main(int argc, char *argv[]) { if(argc != 2) { fprintf(stderr,"usage: cpu <string>\n"); exit(1); } char *str = argv[1]; while(1) { printf("%s\n",str); Spin(1); } return 0; }
程序分析
在该程序中只有一个main方法,该方法有两个形参argc和*argv[].
进入函数首先进行判断,如果参数个数不是2就通过fprintf函数打印一个提示信息“usage: cpu <string>”并调用函数exit(1)退出程序。
然后创建一个指针变量str用来接收用户提供的字符串。
随后进入一个while循环条件为1即无限循环:
在循环体里面首先通过printf函数打印str里面的值,然后执行Spin,函数这里的Spin是在头文件“common.h”里的函数。这头文件里用了时间相关的函数我不是很了解就用ChatGPT粗略解释了一下。“common.h”文件里有两个函数GetTime()获取当前时间,Spin(int howlong)通过持续获取系统时间并比较时间差,来实现一个简单的忙等待。他会让程序等待howlong秒,在这段时间内持续占用CPU资源进行无意义的空循环。
该程序在正确参数下会进行无限打印命令行输入的字符串直到手动强制结束,否则打印提示信息并结束程序。
1.1.2 编译
在命令行键入以下指令进行编译
gcc -o cpu cpu.c -Wall
1.1.3 执行
-
./cpu A
结果如图所示,与预期结果一样 2.在命令行输入下面命令后按下回车
./cpu A & ./cpu B & ./cpu C & ./cpu D &
终止后结果如图。
1.2 内存
1.2.1 程序
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include "common.h" int main(int argc, char *argv[]) { if(argc != 2) { fprintf(stderr,"usage: mem <value>\n"); exit(1); } int *p; p = malloc(sizeof(int)); assert(p != NULL); printf("(%d) addr pointed to by p: %p\n",(int)getpid(),p); *p = atoi(argv[1]); while(1) { Spin(1); *p = *p+1; printf("(%d) value of p: %d\n",getpid(),*p); } }
程序分析
在该程序中只有一个main方法,该方法有两个形参argc和*argv[].
进入函数首先进行判断,如果参数个数不是2就通过fprintf函数打印一个提示信息“usage: cpu <string>”并调用函数exit(1)退出程序。
然后创建一个指针变量p,用 malloc()函数分配一个int类型大小的内存给p,用assert(p != NULL)用来却报内存分配成功,如果失败会退出程序。(刻意查了一下assert是C标准库中的一个宏它的功能是在程序中进行调试,它的作用是检查给定的表达式是否为真。如果表达式为假(即结果为 0),assert()会打印一条错误信息并终止程序。虽然第一眼不知道assert(p != NULL)这个函数是干嘛的但是从上一个代码语句能看出分配内存后要检验内存是否分配成功。)然后通过printf打印一个信息,“(%d) addr pointed to by p: %p”第一%d匹配的是(int)getpid(),这是获取当前进程的pid,%p是打印指针p指向的内存地址。*p = atoi(argv[1])将命令行参数转换为整数,并存储在指针 p 指向的内存位置。即 p存储的是传入的值。随后进入死循环首先调用"common.h"中的Spin()函数然后对*p进行自增,然后打印进程id,以及*p的值。
该程序在参数正确的情况下会先打印一行进程pid和p的地址随后一直打印进程id和*p的值直到手动强制结束。
1.2.2 编译
在命令行输入编译指令
gcc -o mem mem.c -Wall
1.2.3 执行
./mem 0
执行结果:
./mem 0 & ./mem 0 &
执行结果:
1.3 多线程
1.3.1 程序 threads.c
#include <stdio.h> #include <stdlib.h> #include "common.h" #include "common_threads.h" volatile int counter = 0; int loops; void *worker(void *arg) { int i; for(i = 0; i < loops; i++) { counter++; } return NULL; } int main(int argc,char *argv[]) { if(argc != 2) { fprintf(stderr,"usage: threads <loops>\n"); exit(1); } loops = atoi(argv[1]); pthread_t p1,p2; printf("Initial value : %d\n",counter); Pthread_create(&p1, NULL, worker,NULL); Pthread_create(&p2, NULL, worker,NULL); Pthread_join(p1,NULL); Pthread_join(p2,NULL); printf("Final value :%d\n",counter); return 0; }
程序分析
程序在写了两个全局变量 counter 和 loops ,counter 使用了volatile关键字,volatile 关键字在 C 和 C++ 中的作用是告诉编译器,不要对声明为 volatile 的变量进行优化。具体来说,它确保每次访问 volatile变量时,程序都会从内存中重新读取该变量的值,而不是使用寄存器或缓存的值。然后是一个指针函数worker,里面是一个循环体,循环次数由 loops决定,对counter进行递增,这里的counter++不是单一的自增因为使用了 volatile关键字来修饰所以他有多个步骤1.读取 counter 的值。2.递增 counter 的值。3.将递增后的值写回到内存。然后是main函数,与上面两个程序一样首先检查参数个数,参数不正确就打印提示信息并退出,参数正确才进入下一步,loops = atoi(argv[1])将接收到的参数转化为整数。然后定义两个pthread_t类型变量p1,p2。pthread_t是 POSIX 线程库(POSIX Threads,简称为 Pthreads)中的一个数据类型,用于表示线程对象。在使用 Pthreads 库进行多线程编程时,pthread_t 用来标识和管理一个线程。然后打印当前counter的值。然后使用Pthread_create()创建两个线程,两个线程分别执行worker()函数所以每个线程都会对counter进行递增操作。Pthread_create()函数在 common_threads.h 中被封装,用于简化线程创建和错误处理。然后是使用函数Pthread_join()函数,这个函数是将pthread_join()封装在common_threads.h头文件中 Pthread_join(p1, NULL);Pthread_join(p2, NULL);Pthread_join() 等待 p1 和 p2 线程执行完毕。主线程在调用 pthread_join() 时会阻塞,直到目标线程完成其执行。最后打印线程执行完毕后counter的值并正常结束程序。
1.3.2 编译
在命令行输入下面命令进行编译
gcc -o threads threads.c -Wall -pthread
1.3.3 执行
1. ./threads 1000 2. ./threads 10000 3. ./threads 100000
运行结果
在这里老师给的三个测试目前没有什么问题,然后我自己输入了一个更大的数却出现了以下情况
这个情况通过查询然后了解到多个线程同时对 counter 进行递增操作,且 counter++ 不是原子操作,程序很容易出现竞态条件。两个线程同时读取 counter 的值,分别递增后,再写回内存时,可能会覆盖彼此的修改,导致最终的 counter 值不准确。 例如如果 counter = 5,两个线程几乎同时执行 counter++: 线程1 读取 counter = 5,线程2 也读取 counter = 5。 线程1 将 counter 递增为 6,线程2 也将 counter 递增为 6。 线程1 将 6 写回内存,线程2 也将 6 写回内存。 尽管有两个线程执行了递增操作,counter 只增加了 1,而不是 2。这就导致最后counter最后的结果不准确。 为解决这个问题我在网上找到了用同步机制互斥锁通过在多个线程修改 counter 时使用互斥锁,可以确保同一时间只有一个线程能够访问和修改 counter。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include "common.h" #include "common_threads.h" volatile int counter = 0; int loops; pthread_mutex_t lock; void *worker(void *arg) { int i; for(i = 0; i < loops; i++) { pthread_mutex_lock(&lock); // 加锁 counter++; pthread_mutex_unlock(&lock); // 解锁 } return NULL; } int main(int argc, char *argv[]) { if(argc != 2) { fprintf(stderr, "usage: threads <loops>\n"); exit(1); } loops = atoi(argv[1]); pthread_mutex_init(&lock, NULL); pthread_t p1, p2; printf("Initial value : %d\n", counter); pthread_create(&p1, NULL, worker, NULL); pthread_create(&p2, NULL, worker, NULL); pthread_join(p1, NULL); pthread_join(p2, NULL); pthread_mutex_destroy(&lock); printf("Final value : %d\n", counter); return 0; }
添加互斥锁后上面的问题得以解决
1.4 文件
1.4.1 程序 io.c
#include <stdio.h> #include <unistd.h> #include <assert.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <string.h> int main(int atgc,char *argv[]) { int fd = open("/tmp/file",O_WRONLY | O_CREAT | O_TRUNC,S_IRUSR | S_IWUSR); assert(fd >= 0); char buffer[20]; sprintf(buffer,"hello world\n"); int rc = write(fd,buffer,strlen(buffer)); assert(rc == (strlen(buffer))); fsync(fd); close(fd); return 0; }
程序分析
int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)打开或创建一个文件,文件路径为/tmp/file。O_WRONLY表示只写模式,O_CREAT表示如果文件不存在则创建,O_TRUNC表示如果文件存在则截断为0长度。文件权限设置为用户可读写。assert(fd >= 0)断言文件描述符fd是非负值,如果open调用失败,程序将终止。然后创建一个字符数组buffer[20],sprintf(buffer, "hello world\n")使用sprintf函数将字符串"hello world\n"格式化到buffer中。int rc = write(fd, buffer, strlen(buffer))调用write函数将buffer中的内容写入文件描述符fd指向的文件。strlen(buffer)用于获取字符串的长度。assert(rc == (strlen(buffer)));:断言write函数返回的字节数等于buffer的长度,确保所有内容都被写入。fsync(fd);:调用fsync函数同步文件描述符fd指向的文件,确保所有修改都写入磁盘。close(fd)关闭文件描述符fd。最后程序正常退出。
1.4.2 编译
gcc -o io io.c -Wall
1.4.3 执行
./io
程序运行结束后使用命令查看类容
结果正常。