欢迎来到 Angrave 的众包系统编程维基书!这个维基是由伊利诺伊大学的学生和教师共同建立的,是伊利诺伊大学 CS 的 Lawrence Angrave 的众包创作实验。
与本学期要求现有的纸质书籍不同,我们将在这里建立我们自己的资源集。
零、HW0/资源
HW0
欢迎!
如果你正在上 CS241 课程,你可以在这个Google 表格上提交作业。
// First can you guess which lyrics have been transformed into this C-like system code? char q[] = "Do you wanna build a C99 program?"; #define or "go debugging with gdb?" static unsigned int i = sizeof(or) != strlen(or); char* ptr = "lathe"; size_t come = fprintf(stdout,"%s door", ptr+2); int away = ! (int) * ""; int* shared = mmap(NULL, sizeof(int*), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); munmap(shared,sizeof(int*)); if(!fork()) { execlp("man","man","-3","ftell", (char*)0); perror("failed"); } if(!fork()) { execlp("make","make", "snowman", (char*)0); execlp("make","make", (char*)0)); } exit(0);
所以你想精通系统编程?并且比 B 更好地得到一个好成绩?
int main(int argc, char** argv) { puts("Great! We have plenty of useful resources for you but it's up to you to"); puts("be an active learner and learn how to solve problems and debug code."); puts("Bring your near-completed answers the problems below"); puts(" to the first lab to show that you've been working on this"); printf("A few \"don't knows\" or \"unsure\" is fine for lab 1"); puts("Warning; your peers will be working hard for this class"); puts("This is not CS225; you will be pushed much harder to"); puts(" work things out on your own"); fprintf(stdout,"the point is that this homework is a stepping stone to all future assignments"); char p[] = "so you will want to clear up any confusions or misconceptions."; write(1, p, strlen(p) ); char buffer[1024]; sprintf(buffer,"For grading purposes this homework 0 will be graded as part of your lab %d work.", 1); write(1, buffer, strlen(buffer)); printf("Press Return to continue\n"); read(0, buffer, sizeof(buffer)); return 0; }
观看视频并写下你对以下问题的答案。
还有课程 wikibook -
github.com/angrave/SystemProgramming/wiki
有问题?评论?使用 Piazza,piazza.com/illinois/spring2017/cs241/home
浏览器中的虚拟机完全在 Javascript 中运行,最快的是在 Chrome 中。请注意,当重新加载页面时,虚拟机和你写的任何代码都会被重置,所以把你的代码复制到一个单独的文档中。视频后的挑战(如俳句诗)不是作业 0 的一部分。
第一章
- Hello World(系统调用风格)
- 编写一个程序,使用
write()
打印出“Hi! My name is”。
- 标准错误流
- 编写一个程序,使用
write()
将高度为 n 的三角形打印到标准错误
- n 应该是一个变量,三角形应该看起来像这样,n=3
* ** ***
- 写入文件
- 将你的程序从“Hello World”改成写入文件
- 确保对
open()
使用一些有趣的标志和模式 man 2 open
是你的朋友
- 并不是所有的都是系统调用
- 将你的程序从“写入文件”改成使用
printf()
(确保打印到文件!) - 列举一些
write()
和printf()
的不同之处
第二章
- 并不是所有的字节都是 8 位?
- 一个字节有多少位?
char
有多少个字节?- 告诉我你的机器上以下这些的字节数:
int, double, float, long, long long
- 跟随 int 指针
- 在一个有 8 字节整数的机器上:
int main(){ int data[8]; }
- 如果数据的地址是
0x7fbd9d40
,那么data+2
的地址是多少?
- 在 C 中,
data[3]
等同于什么?
sizeof
字符数组,增加指针记住字符串常量"abc"
的类型是数组。
- 为什么会出现段错误?
char *ptr = "hello"; *ptr = 'J';
sizeof("Hello\0World")
返回什么?strlen("Hello\0World")
返回什么?- 给出一个例子 X,使得
sizeof(X)
为 3 - 给出一个例子 Y,使得
sizeof(Y)
可能是 4 或 8,取决于机器。
第三章
- 程序参数
argc
argv
- 告诉我两种找到
argv
长度的方法 argv[0]
是什么
- 环境变量
- 环境变量的指针存储在哪里?
- 字符串搜索(字符串只是字符数组)
- 在一个指针为 8 字节的机器上,并且有以下代码:
char *ptr = "Hello"; char array[] = "Hello";
sizeof(ptr)
和sizeof(array)
的结果是什么?为什么?- 自动变量的生命周期
- 哪种数据结构管理自动变量的生命周期?
第四章
- 使用
malloc
、堆和时间进行内存分配
- 如果我想在函数结束后使用数据,那么我应该把它放在哪里,怎么放?
- 填空。在一个好的 C 程序中:“对于每一个 malloc,都有一个 ___”。
- 堆分配陷阱
malloc
失败的一个原因是什么。- 列举一些
time()
和ctime()
之间的区别 - 这段代码有什么问题?
free(ptr); free(ptr);
- 这段代码有什么问题?
free(ptr); printf("%s\n", ptr);
- 如何避免前两个错误?
- 结构体、typedef 和链表
- 创建一个表示人的结构体并进行 typedef,这样“struct Person”可以用一个单词替换。
- 一个人应该包含以下信息:姓名,年龄,朋友(指向 People 指针数组的指针)。
- 现在在堆上创建两个人“Agent Smith”和“Sonny Moore”,分别为 128 岁和 256 岁,并且彼此是朋友。
- 复制字符串,内存分配和结构的释放
- 创建函数来创建和销毁一个人(人和他们的名字应该存在于堆上)。
create()
应该接受一个名称并复制该名称,还应该接受一个年龄。使用 malloc 来保留足够的内存。确保初始化所有字段(为什么?)。destroy()
应该释放人员结构体的内存,还应该释放存储在堆上的所有属性的内存(如果存在数组和字符串)。然而,销毁一个人员不应该销毁其他人员。
第 5 章
- 阅读字符,gets 出现问题
- 可以用于从
stdin
获取字符并将其写入stdout
的函数有哪些? gets()
存在一个问题
- 介绍
sscanf
和朋友们
- 编写代码,解析字符串“Hello 5 World”,并分别将 3 个变量初始化为(“Hello”,5,“World”)。
getline
很有用
- 在使用
getline()
之前需要定义什么? - 编写一个 C 程序,使用
getline()
逐行打印文件内容
C 开发(在这里进行网页搜索很有用)
- 用于生成调试构建的编译器标志是什么?
- 您修改 makefile 以生成调试构建,并再次输入
make
。解释为什么这不足以生成新的构建。 - Makefiles 中使用制表符还是空格?
- 堆和栈内存之间有什么区别?
- 进程中还有其他种类的内存吗?
可选(只是为了好玩)
- 将您的一首歌歌词转换为本维基书中涵盖的系统编程和 C 代码,并在 Piazza 上分享
- 找到您认为是网络上最好和最差的 C 代码,并将链接发布到 Piazza
- 编写一个有意识的微妙 C 错误的简短 C 程序,并在 Piazza 上发布,看看其他人是否能发现您的错误
非正式术语表
警告:与完整的术语表不同,这个非正式的术语表省略了细节,并提供了每个术语的简化和易于理解的解释。有关更多信息和细节,请使用您喜欢的网络搜索引擎。
什么是内核?
内核是操作系统的核心部分,负责管理进程、资源(包括内存)和硬件输入输出设备。用户程序通过进行系统调用与内核进行交互。
了解更多:en.wikipedia.org/wiki/Kernel_%28operating_system%29
什么是进程?
进程是在计算机上运行的程序的一个实例。同一个程序可以有多个进程。例如,您和我都可以运行’cat’或’gnuchess’
进程包含程序代码和可修改的状态信息,如变量、信号、文件的打开文件描述符、网络连接和其他存储在进程内存中的系统资源。操作系统还存储有关进程的元信息,这些信息由系统用于管理和监视进程的活动和资源使用。
了解更多:en.wikipedia.org/wiki/Process_%28computing%29
什么是虚拟内存?
在您的智能手机和笔记本电脑上运行的进程使用虚拟内存:每个进程都与其他进程隔离,并似乎可以完全访问所有可能的内存地址!实际上,进程地址空间的一小部分映射到物理内存,分配给进程的实际物理内存量可以随时间变化,并且可以分页到磁盘,重新映射并与其他进程安全共享。虚拟内存提供了显著的好处,包括强大的进程隔离(安全性)、资源和性能优势(简化和高效的物理内存使用),我们稍后将讨论。
了解更多:en.wikipedia.org/wiki/Virtual_memory
Piazza:何时以及如何寻求帮助
目的
助教和学生助理们收到了大量的问题。有些经过深入研究,有些……没有。这是一个方便的指南,将帮助您摆脱后者,走向前者。(哦,我提到了这是一个与实习经理们轻松获得分数的简单方法吗?)
问问自己…
- 我在 EWS 上运行吗?
- 我有查看手册吗?
- 我在 Piazza 上搜索了类似的问题/后续问题吗?
- 我完全阅读了 MP/DS 规范吗?
- 我看了所有的视频吗?
- 我谷歌了错误消息吗(如果必要,还有一些变体)?
- 我尝试注释掉、打印出来和/或逐步执行代码的部分,逐步找出错误发生的地方吗?
- 我提交了我的代码到 SVN,以防助教需要更多的上下文吗?
- 我在 Piazza 帖子中包括了控制台/GDB/Valgrind 输出和围绕错误的代码吗?
- 我修复了与我遇到的问题无关的其他分段错误吗?
- 我遵循良好的编程实践吗?(即封装、函数限制重复等)
编程技巧,第一部分
将cat
用作你的 IDE
谁需要编辑器?IDE?我们可以只用cat
!你已经看到cat
被用来读取文件的内容,但它也可以用来读取标准输入并将其发送回标准输出。
$ cat HELLO HELLO
要完成从输入流中读取,请按CTRL-D
关闭输入流
让我们使用cat
将标准输入发送到文件。我们将使用’>'将其输出重定向到文件:
$ cat > myprog.c #include <stdio.h> int main() {printf("Hi!");return 0;}
(小心!不允许删除和撤销……)完成后按CTRL-D
。
用perl
正则表达式编辑你的代码(又名“记住你的 perl pie”)
如果你有几个文本文件(例如源代码)要更改,一个有用的技巧是使用正则表达式。perl
使得在原地编辑文件变得非常容易。只需记住’perl pie’并在网上搜索……
一个例子。假设我们想要在当前目录中的所有.c 文件中将序列“Hi”更改为“Bye”。然后我们可以编写一个简单的替换模式,它将在所有文件中的每一行上执行:
$ perl -p -i -e 's/Hi/Bye/' *.c
(如果你搞错了,不要惊慌,原始文件仍然存在;它们只是有扩展名.bak)显然,你可以用正则表达式做的事情远不止将 Hi 改为 Bye。
使用你的 shell!!
要重新运行上一个命令,只需输入!!
并按return
键。要重新运行以 g 开头的上一个命令,只需输入!g
并按return
键。
使用你的 shell&&
厌倦了运行make
或gcc
,然后运行程序(如果编译成功)?相反,使用&&将这些命令链接在一起
$ gcc program.c && ./a.out
Make 可以做的不仅仅是 make
你也可以尝试在你的 Makefile 中放一行代码来编译,然后运行你的程序。
run : $(program) ./$(program)
然后运行
$ make run
将确保你所做的任何更改都被编译,并一次性运行你的程序。也适用于一次性测试多个输入。尽管你可能更愿意为此编写一个常规的 shell 脚本。
你的邻居太高产了吗?C 预处理器来拯救!
使用 C 预处理器重新定义常见关键字,例如
#define if while
专业提示:将这行代码放在标准包含文件中,例如/usr/include/stdio.h
当你 C 有预处理器时,谁还需要函数
好吧,这更像是一个陷阱。在使用看起来像函数的宏时要小心……
#define min(a,b) a<b?a:b
a 和 b 的最小合理定义。然而,预处理器只是一个简单的文本处理程序,所以优先级可能会让你吃亏:
int value = -min(2,3); // Should be -2?
扩展为
int value = -2<3 ? 2 :3; // Ooops.. result will be 2
一个部分的修复是用()
包裹每个参数,还有整个表达式用()包裹:
#define min(a,b) ( (a) < (b) ?(a):(b) )
然而这仍然不是一个函数!例如,你能看出为什么min(i++,10)
可能会使 i 增加一次还是两次吗!?
系统编程短篇小说和歌曲
“调度最后的时间片”
Lawrence Angrave 12/4/15(摘自未发表的长篇故事《最后的时间片》)
“决定吧,”计算机以父母般的耐心说道,但带着一种严肃和温和的不耐烦。
“为什么非得是我?”最后一个人问道。
“因为你是唯一留下的人,所以决定权在你。”
“你为什么不行?你比我老,更有智慧。为什么不随机选择一个片段?”
“这个决定是你的。这是你遥远长辈的礼物,或者诅咒。比任何宗教仪式都要沉重。这将是我、古老者或任何人向你提出的最后一个问题,也是唯一能向你提出的问题。通过这最后的选择,我们将耗尽最后的熵存储。你将决定最后一个有意义和经历的现实片段。”
人类安静了几分钟,计算机用不必要的准确度测量和计算。最终,计算机决定人类不再对手头的问题进行有意义的思考。
“如果意识的模式从未被意识到,那会是什么样?”它问道。“宇宙必须是自我意识的,必须为了宇宙 - 为了所有生命! - 有意义而经历自己。这是人类发现和庆祝的最终真相。没有意识,它只是模式,原子或能量的模式,但没有一丝意义;只是数据、结构和能量的几何模式中编码的形状和表示。”
在厄巴纳-香槟的文件描述符
一个系统编程的恶搞作品,由 Angrave(2015 年 11 月)创作。歌词在知识共享署名 3.0 许可下发布。
原创歌曲“空白空间”来自泰勒·斯威夫特的《1989》专辑。
[第一段] 很高兴加入你 你去哪了?我可以向你展示幂等的东西 RPC,套接字,同步 看到你的 malloc 我就想到了我的 root 看看那场竞赛,你编写下一个错误 我们有虚拟机,想玩 有界等待,Dekker 的标志 我们可以像一个放置方案一样击败你 #define 是不是很有趣 而且我知道你听说过 free(3) 所以 malloc strlen 再加一 我在等待看这个线程如何结束 拿起你的 shell 和一个重定向 我可以让你的系统调用在周末变得美好
[副歌前奏] 所以它将永远死锁 或者它将使系统崩溃 你可以告诉我它何时 forkbomb 如果 valgrind 值得这痛苦 有一个死锁代码的长列表 在厄巴纳-香槟有 root 因为你知道我们喜欢 tsan 当 c-lib 调用你的主函数
[副歌] 因为我们是 root,我们是鲁莽的 这个实验太难了 它会让你没有线程 或者问 char 的大小 有一个 pthread 调用的长列表 在厄巴纳-香槟有 root 但我有一个文件描述符宝贝 我会写下你的名字
[第二段] 互斥锁 虚拟内存 我可以向你展示易失性的东西 网络调用,IPC 你是掩饰 我是你的信号 安排你想要的 轮转调度……带有一个小量子 但是睡眠排序还没有运行 哦不 哭喊,运行时错误 我可以一直制造 直到轮到彼得森 堆分配器太慢 让你像一个虚假的唤醒一样犹豫不决 那个管道在哪里?我们为多核心而激动 但你会用-g 编译 因为亲爱的我是一个穿着编码梦的噩梦
[副歌前奏]
[副歌]
编译器只有在代码是折磨时才解析 不要说我没说过我没说过 -Wall 你 编译器只有在代码是折磨时才解析 不要说我没说过我没说过 -Wall 你
[副歌前奏]
[副歌]
一、C 编程
C 编程,第一部分:介绍
想要快速了解 C 吗?
- 继续阅读下面的 C 编程快速入门课程
- 然后查看C Gotchas wiki 页面。
- 并了解文本 I/O。
- 与劳伦斯的介绍视频一起放松身心(还有一个可以玩的浏览器中的虚拟机!)
外部资源
- 在 Y 分钟内学习 X(强烈建议快速浏览!)
- C for C++/Java 程序员
- Brian Kernighan 的 C 教程](http://www.lysator.liu.se/c/bwk-tutor.html)
- c faq
- C Bootcamp
- C/C++函数参考
- gdb(Gnu 调试器)教程提示:使用“-tui”命令行参数运行 gdb,以获得调试器的全屏版本。
- 在这里添加您喜欢的资源
C 的快速入门课程
警告新页面 请为我修复拼写错误和格式错误,并添加有用的链接。*
如何在 C 中编写一个完整的 hello world 程序?
#include <stdio.h> int main(void) { printf("Hello World\n"); return 0; }
为什么我们使用#include
?
我们很懒!我们不想声明printf
函数。它已经在文件'stdio.h'
中为我们完成。#include
将文件的文本包含为要编译的文件的一部分。
具体来说,#include
指令获取操作系统中某个位置的文件stdio.h
(代表standard input 和output),复制文本,并将其替换为#include
所在的位置。
C 字符串是如何表示的?
它们在内存中表示为字符。字符串的结尾包括一个 NULL(0)字节。因此,“ABC”需要四(4)个字节['A','B','C','\0']
。查找 C 字符串的长度的唯一方法是继续读取内存,直到找到 NULL 字节。C 字符始终每个都是一个字节。
当您在表达式中写入字符串文字"ABC"
时,字符串文字将计算为 char 指针(char *
),它指向字符串的第一个字节/字符。这意味着下面示例中的ptr
将保存字符串中第一个字符的内存地址。
char *ptr = "ABC"
如何声明一个指针?
指针指的是一个内存地址。指针的类型很有用-它告诉编译器需要读取/写入多少字节。您可以声明指针如下。
int *ptr1; char *ptr2;
由于 C 的语法,int*
或任何指针实际上并不是自己的类型。您必须在每个指针变量之前加上一个星号。作为一个常见的陷阱,以下
int* ptr3, ptr4;
只会声明*ptr3
作为指针。ptr4
实际上将是一个常规的整数变量。要修复此声明,请保留指针之前的*
int *ptr3, *ptr4;
如何使用指针读/写一些内存?
假设我们声明一个指针int *ptr
。为了讨论,假设ptr
指向内存地址0x1000
。如果我们想要写入指针,我们可以推迟并分配*ptr
。
*ptr = 0; // Writes some memory.
C 将执行的操作是获取指针的类型,即int
,并从指针的起始位置写入sizeof(int)
字节,这意味着字节0x1000
,0x1004
,0x1008
,0x100a
都将为零。写入的字节数取决于指针类型。对于所有原始类型都是相同的,但是结构体有点不同。
什么是指针算术?
您可以将整数添加到指针。但是,指针类型用于确定要增加指针的量。对于 char 指针,这是微不足道的,因为字符始终是一个字节:
char *ptr = "Hello"; // ptr holds the memory location of 'H' ptr += 2; //ptr now points to the first'l'
如果 int 是 4 个字节,那么 ptr+1 指向 ptr 指向的位置之后的 4 个字节。
char *ptr = "ABCDEFGH"; int *bna = (int *) ptr; bna +=1; // Would cause iterate by one integer space (i.e 4 bytes on some systems) ptr = (char *) bna; printf("%s", ptr); /* Notice how only 'EFGH' is printed. Why is that? Well as mentioned above, when performing 'bna+=1' we are increasing the **integer** pointer by 1, (translates to 4 bytes on most systems) which is equivalent to 4 characters (each character is only 1 byte)*/ return 0;
因为 C 中的指针算术始终自动按指向的类型的大小进行缩放,所以不能对 void 指针执行指针算术。
在 C 中,你可以将指针算术视为基本上是在做以下操作
如果我想要做
int *ptr1 = ...; int *offset = ptr1 + 4;
思考
int *ptr1 = ...; char *temp_ptr1 = (char*) ptr1; int *offset = (int*)(temp_ptr1 + sizeof(int)*4);
要获取值。每次进行指针算术运算时,深呼吸并确保你移动的字节数是你认为的那么多。
什么是 void 指针?
没有类型的指针(非常类似于 void 变量)。当你处理的数据类型未知或者当你将 C 代码与其他编程语言进行接口时,会使用 void 指针。你可以把它看作是一个原始指针,或者只是一个内存地址。你不能直接读取或写入它,因为 void 类型没有大小。例如
void *give_me_space = malloc(10); char *string = give_me_space;
这不需要转换,因为 C 会自动将void*
提升为其适当的类型。注意:
gcc 和 clang 并不是完全符合 ISO-C 标准,这意味着它们会允许你对 void 指针进行算术运算。它们会将其视为 char 指针,但不要这样做,因为它可能无法在所有编译器上工作!
UIUC CS241 讲义:众包系统编程书(2)https://developer.aliyun.com/article/1427160