转C保平安
- C primer Plus
- Computer System, A Programmer’s Perspective
- Advanced Programming in the Unix Environment
PHP
JS/CSS
- Vue.js
- ES 6
- Bootstrap 4
Golang
Java
- Netty
- Effective Java
DB
- Redis
- High Performance MySQL
- PostgreSQL
Paper
- 小论文 0.5/1
- 大论文 0.0/1
关注C、Go、C++、JavaScript和Rust
Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.
包括创建进程,执行程序和进程终止,还有进程属性的各种ID,以及它们如何受到进程控制原语的影响,最后还会介绍解释器文件,进程会计机制等。
每个进程都有一个非负整型表示的唯一进程ID,进程ID标识符总是唯一的。虽然是唯一的,但是进程ID是可以复用的,当一个进程终止后,其进程ID就成为复用的候选者。大多数Unix系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程的ID。所以我们可以认为一段时间内,一个进程ID唯一代表那一个进程。
系统中有一些专用进程,比如ID为0的进程通常是调度进程,常常称为交换进程(swapper),该进程是内核的一部分(调度进程是内核的核心),它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID为1的通常是 init 进程,在自举(bootstrap)过程中由内核调用,此进程负责在自举内核后启动一个UNIX系统。init进程绝不会终止,它是一个普通的用户进程(而不是内核进程),但它以超级用户特权运行。(在某些Unix实现中,进程ID为2的是页守护进程(page daemon),它负责支持虚拟存储器系统的分页操作。
今天看完了红楼梦,看到了《惊噩耗黛玉魂归》这一集,黛玉归去的时候,眼泪止不住地流出来,实在忍不住,把衣服的帽子戴上了,趴在桌子上了抽泣了好一会。怕被室友瞧见,笑我一个大老爷们看个电视剧还哭上了,但是想到黛玉这样美好的人儿,就这样殁了,怎么也控制不住。
这几日北京天气极差,又逢到周末,于是在宿舍从早到晚看了两天红楼梦,前些天看了十多集,昨天看了十五集,今天把剩下的都看完了。87版的红楼是极经典的作品,但是不像三国、西游那样经常在电视台播出,所以一直也没怎么看过。
这次是偶然想起来看的,本是想看个开头,因为之前看了红楼书的前几章吧,突然想起甄士隐,贾雨村这两个人的名字,想看看电视了解一下。由于闲散了好一段时间了,也没什么急需着要做的事情,就中午的时候偶尔看上一会,竟根本停不下来了。
C 的函数指针前面已经简单介绍过,常用的函数指针及其变量声明方式如下:
typedef int func(int, int); |
上面的 fp 和 fpt 都是一个指向函数func的指针了,这就有一个问题,它们都是指向函数的指针,那么 *fp 就是本身,在它后面加上参数列表就是一次函数调用了。其形式如下:
int a = 1; |
这种调用看起来太麻烦了,实际上函数经常可以通过指针调用,实际的函数名也总是隐式地转换为指针(因为这种隐式的转换,所以函数指针的取地址与间接引用经常被忽略)。所以可以直接通过指针调用:
a = fp(a, b); |
这样看起来清晰多了。ANSI C 使用后一种解释,即显示地间接引用(*)不是必需的,虽然仍然允许使用这种方法
下面是一个简单示例:
typedef int Pfunc(int); |
参考:
主演:金·凯瑞,凯特·温斯莱特,这是温斯莱特的电影里,我最喜欢的一部。
影片一开头,穿着橙色运动衫,染着“蓝色废墟”发色的温斯莱特绝对让人着迷。
本来不太喜欢染发的人,而另一个让我觉得很美的染发案例就是《谍影重重》女主的淡淡的酒红头发。
真是,光是看温斯莱特那些变幻莫测,但总是很惊艳的发色就值得去看这部片子了,更不要说编剧巨大无比的脑洞。
作为一部爱情电影,所表现的爱情观也是极正。
这是一部比较需要细看的电影,上周看的,推荐去看。
虽然 goto 被认为是语言设计中的“毒瘤”,人人都避而远之,但是在真正的C开发者心中,goto不应该是让人害怕的东西。反而有一些比goto更加放肆的跳转函数!
在C中,goto语句只能在函数内部进行跳转,而不能跨越函数,要执行这种跳转功能需要 setjmp
和 longjmp
配合使用,这两个函数对于处理发生在很深层嵌套函数调用中的出错情况是非常有用的,我们把它们成为非局部跳转,表示它们在栈上跳过多个调用栈帧,返回到当前函数调用路径上的某一个函数。
这两个函数的声明如下:
#include <setjmp.h> |
这两个函数让人感到比较奇怪的是,它的参数env看起来是一个结构体变量,而不是一个结构体指针,实际上是因为 jmp_buf 并不是结构体定义,而是某种形式的数组定义,本身就是一个指针,所以不需要使用取地址操作。
我们在希望返回的地方调用 setjmp
,该调用会将用于恢复栈帧状态的信息保存到 env 变量中,因为 env 变量在 longjmp函数调用中要用到,而longjmp常在另一个函数调用,所以常需要将env设置为全局变量。
调用 setjmp 后可以在后续调用的函数中使用 longjmp 函数,它使用 setjmp 时所使用的 env 作为参数,第二个参数是一个非0值,则作为 setjmp 处恢复栈帧后的函数返回值,第二个参数可以用来作为多处调用 longjmp 返回到一个 setjmp时判断具体是那个 longjmp 的跳转。
#include <setjmp.h> |
我们要考虑一下跳转后自动变量和寄存器变量(其他类型不需要考虑)的值的状态。当longjmp返回到main函数时,这些变量的值是否能恢复到以前调用setjmp是的值,或者这些变量的值保持为调用函数时的值?很遗憾,这个问题的答案是看情况,所有的标准称这些值是不确定的,虽然大多数的实现并不回滚这些自动变量和寄存器变量的值,但是在编译器使用优化策略时,其值可能会被回滚。
如果有一个自动变量,又要确保不回滚其值,可以定义其为具有 volatile
属性,声明为全局变量或者静态变量的值在执行 longjmp 时保持不变(总是能保持最近所呈现的值)。
下面是一个示例,表示了不同类型的变量在 longjmp 跳转后值是否回滚:
#include <setjmp.h> |
不使用优化的编译结果如下:
wuxu@centos7 % gcc -o jmpvar jmpvariable.c |
开启编译器优化的结果:
wuxu@centos7 % gcc -O2 -o jmpvarO2 jmpvariable.c |
可以看出,在启用编译器优化后,自动变量和寄存器变量都回滚了,而全局的,volatile修饰的变量咩有回滚值。
对于自动变量,一个基本的规则是声明自动变量的函数已经返回后,不能再引用这些变量。下面是一个例子,使用一个自动变量作为文件流的缓冲区了:
#include <stdio.h> |
open_file
函数中,使用自动变量 databuf 的空间作为文件流的缓冲区,当函数返回时,databuf在栈上的的空间将由下一个被调用函数的栈帧使用,这个时候就产生了冲突(在实际测试(gcc 4.8.3, centos 7)中,并没有出现问题,文件流能正常读取)。不过按照理论这是容易出现问题,为了防止出现问题,应该使用全局存储空间静态地(static, extern)或者动态地(malloc等)为数组 databuf 分配空间。
基本完
似乎今年北京的雾霾比往年更加严重,更加频繁了。这几天又是严重的雾霾,这个月已经好几次这样连续的严重雾霾了。偶尔有阵大风吹来能好上那么几天,然后又开始慢慢积累,又是严重雾霾。
前几年都没感觉有这么严重,今年真是看着那雾霾都不敢出门了,幸好的是9月中下实习离职之后就只需要呆在学校看书,写论文,并不太需要出门,但是就是从宿舍走到实验室的这一段路,甚至坐在实验室,呆在宿舍,都感觉呼吸的空气很“浑浊”。这几天晚上睡觉都觉得呼吸的空气很难受,受不了,早上起床感觉鼻子和嗓子都很难受。
这样的北京,真是一天都不想在这里多呆了。
9月开始就是来北京的第6个年头了,时间过得真快,北京这个地方有很多好,也有很多不好,总的来说还是不喜欢。第一是政治味道太浓,毕竟是共和国的首都,是政治的中心。这几年变得非常讨厌政治,讨厌体制,在北京总是能体会到很多政治的东西,最能感觉到体制的强大力量,让人害怕。现在的政治发展态势也是越来越不看好,虽然不太可能发生什么“不好的”事情,但是,政治味太浓的地方总归是个是非之地。
第二是北京的吸血式发展,这个很多人讨论过,我也觉得北京的发展确实是畸形的,依靠自身强大的政治力量,吸取了周边省市的优秀资源却没有带动周边的发展,反而让周边的人只能向北京靠拢,我所认识的河南、河北、山东、山西的,就没有一个说要去别的地方的,他们基本没有选择,只能留在北京,为北京的发展做贡献。畸形,讨厌发展不良的东西,虽然它也能展现出很华丽,很光辉的一面,但是想想就知道这光辉灿烂背后是什么在支撑着。
第三则是这该死的天气。本来来北京这么久,已经适应北方的干燥和大风,对冬天的暖气已是非常依赖了,但是一到冬天就雾霾,太难受了。北国风光,千里冰封,万里雪飘,这是我所期待的北方的冬天,但是来了这么久,没见过什么大的雪,下过的雪还没有在老家湖南见过的大,大概北京的冬天是下不下来大雪的了吧,城市效应太强大了。
环境治理这个问题看起来不是一天两天能解决的了,最近对于雾霾一些专家预测需要20-30年才能治理好,而根据最近在法国的气候谈判,中国的二氧化碳排放争取在2030年达到顶峰,所以可以预计,这未来的十年到十五年,雾霾基本是不可能急速好转的了。虽然我们国家的集中力量办大事经常能办成一些让人吃惊的“大事”,但是在雾霾这方面,似乎没有什么能立竿见影的好办法,毕竟罗马不是一天建成的,雾霾的原因也不是一天形成的,感觉要整个北京周边省市的经济结构大调整,产业大升级,北京和周边省市的经济、政治地位慢慢平等,才能慢慢缓解这个问题,但是这在短期内可能吗?
另外一个就是北京变态的户口政策,我是非常反感户籍制度的,也许留在北京的人,为了户口奋斗,为了买房奋斗了,在北京留下来了,成为了一个“地道的北京人”,人各有所求,这不是我所求的。北京的户口政策,包括现在新出的居住证政策都是强行从工资卡里抢钱罢了,很多人为了户口工资少几千,工作还不如意,只是为了户口委身而已;但没有户口的话,看病啊,教育啊各种卡壳。
之前和学姐吃饭的时候,她就和我们计算了一下她和她男朋友要在北京买房从现在开始计划着省吃俭用要多少年才能首付,才能过上“一般”的生活,然后就感慨自己最美好的年华都要献给北京的房子了。我相信这么多人选择在北京“奋斗”,争相在北京买房,说明这条路是有好处的,是有很大吸引力的;但是对我来说,有北京的户口,在北京有一套房子,这根本不是我所追求的。人生有那么多的选择,为什么要为了这两个东西付出几近半生的光阴与精力。
既然雾霾治不好,那就只能逃跑了。估计很多人都有逃离北京的计划了,作为南方人,不想留北京,其他的选择还是很多的。不过说在北京呆了这么久,认识的同学,老师都是在这边,人们说的“资源”都在北京,很多人因此而不愿离开(当然这只是原因之一),但这不会成为让我留下来的理由。
不过北京有很多好处,互联网整体发展更加好,各个领域都做得比较好,外企在中国的部门也大部分在北京,其他的比如音乐会啊,歌剧啊,演唱会啊,文化氛围啊的倒没什么,毕竟我是个粗鄙的人,来了这么多年也没去过几次。
逃离北京,逃得远远的。
程序执行时,main函数是如何被调用的,命令行参数是如何传递给程序的,典型的存储空间布局是什么样式,如何分配其他存储空间,进程如何使用环境变量,进程的终止等等这些都是进程控制的基础知识。
我们知道C程序总是从main函数开始执行,main函数的原型如下:
int main(int argc, char *argv[]); |
其中int是main函数的类型,虽然旧的编译器使用void定义,或者不声明main的类型也可以编译,但是那是不好的做法,根据 ISO C和POSIX.1 的定义都应该将main显式声明为 int 类型的。argc是命令行参数的数目,argv是指向参数的各个指针构成的数组。与众面向对象语言不同,C需要显式的 argc 传递参数的个数,因为仅凭 argv 不能确定其大小。
当内核执行C程序时,在调用 main 前先调用了一个特殊的启动例程,可执行程序文件将此例程指定为程序的起始地址–这是有连接编辑器设置的,而连接编辑器则由C编译器调用,启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。
进程有多种退出运行的方式,最常用的是从main函数返回,或者main函数执行到结束。所有的进程终止的方式总结如下,其中前5种正常终止,后三种是异常中止:
退出函数 exit 也是很常用的, _exit 和 _Exit 则不太常用,它们之间的区别是 exit 会先执行一些清理操作,比如对所有打开的文件调用 fclose 函数,刷新输出缓冲等,然后在如内核,_exit 和 _Exit 则是立即进入内核的。还有一个区别是它们包含在不同的头文件中,exit 和 _Exit 包含在 <stdlib.h> 中, _exit 包含在 <unistd.h> 中(因为前两者是ISO C说明的,而后者是POSIX.1说明的)。
这些终止函数都是用一个整型的状态码作为参数,称为终止状态(exit status)。C99 规定没有显示调用return而main执行到最后一个语句时返回,那么进程的终止状态是0,在之前的标准这种情况是为定义,所以返回值可能是随机的。我们的应该以C99为标准。
按照 ISO C的规定,一个进程可以登记最多32个(具体实现可能更多)由exit自动调用的函数,这些函数称为终止处理程序,调用 atexit
函数来登记这些函数。
#include <stdlib.h> |
还记得之前介绍的函数指针吗,atexit函数的参数的类型就是一个函数指针(函数地址),其返回值和参数都是 void 。注意:exit调用这些登记了的函数的顺序与它们登记的顺序相反,同一函数若登记多次,则会被调用多次。
一个C程序的启动和终止流程:
可以看出内核使程序执行的唯一方法是调用一个exec函数。
命令行参数其实我们之前已经使用过了,基本了解了,对于Java和Go中的命令行参数方式也有所了解:Java通过一个String数组获取命令行参数,而Go则通过设置 flag 可以非常方便地获取特定的参数。
C的命令行参数保存字啊 main函数的第二个参数,char **argv
或者 (char *argv[]
)中,通过第一个参数 argc 获得参数的个数。如果要想Go那样获取特定形式的参数则需要自己对 argv 数组进行一些处理。
在ISO C 和 POSIX.1 中,都要求 argv[argc] 是一个空指针(这由C启动例程保证),所以对argv的遍历也可以不借助 argc 的值。
for (int i=0; iargv[i] != NULL; i++) { |
每个程序都自动接受(获得)一张环境表,环境表也是一个字符指针数组(字符串数组),全局变量environ
包含了该指针数组的地址。定义为: extern char **environ;
。要想在代码中使用这个数组,需要前面的声明,否则 environ 是一个非定义的符号。
按照惯例,环境由 name=value
这样形式的字符串组成,大多数预定义名完全由大写字母组成,但是不保证全部是这样。
ISO C定义了一些函数对环境变量进行读写相关的操作:
#include <stdlib.h> |
一些常用的环境变量名和它们在各个系统中的实现:
这一段在深入理解计算机系统中已经详细介绍了,这里看有需要记的再记。
ISO C说明了3个用于存储空间动态分配的函数:
malloc
分配指定字节数的存储区,存储区的初始值不确定calloc
为指定数量,指定长度的对象分配存储空间,该空间中,每一位都初始化为0realloc
增加或减少以前分配区的长度,当增加长度时,可能将以前分配的内容移到另一个足够大的区域以便在尾端增加存储区,新增的存储区的初始值不确定它们的函数声明如下:
#include <stdlib.h> |
关于它们返回值的赋值有一个要注意的地方,参见这里。要注意 realloc函数的第二个参数是存储区的新长度,而不是新旧存储区的长度之差。
这些存储区分配函数通常用 sbrk
系统调用实现,该系统调用扩充(或缩小)进程的堆,虽然 sbrk 可以扩充或者缩小进程的存储空间,但是大多数 malloc 和 free的实现都不减少进程的存储空间,释放的空间可供以后再分配,但是将它们保持在 malloc 池中,而不是返还给内核。
大多数动态分配函数的实现实际分配的空间比所请求的要大一些,额外的空间用于记录管理信息,比如分配块的长度,指向下一个分配块的指针等。如果在超多分配去尾端或者在已分配区开始位置之前进行写操作,会修改另一块的管理记录信息,这导致的错误是灾难性的,可恶的是这中错误很难发现。在动态分配的缓冲区的前或后进行写操作,破坏的可能不仅仅是该区的管理记录信息,这些区域可能用于其它动态分配的对象,这些对象因此可能被破坏,而且很难追查到原因。
另一个导致致命错误的是:释放一个已经释放的块,或者free的参数的指针不是由上面的函数分配的对象。对一个对象调用 free 后,这个指针的值实际上没有改变,它仍然在作用域内,如果该指针指向的地址被重新分配了,对它再进行free就会导致预料之外的行为。
如果一个分配的区域没有调用free则会导致进程占用的存储空间越来越大,导致泄漏(即时是在调用函数中分配的空间,没有free的话在函数调用结束也不会自动释放)。
后面的新记一篇
Unix系统的正常运作需要使用大量与系统有关的数据文件,例如口令文件 /etc/passwd 和组文件 /etc/group 就经常被多个程序使用,由于历史原因,这些文件都是ASCII文本文件,并且使用标准IO库读这些文件…但是,对于较大的系统,顺序扫描指令文件很花费时间,我们需要能够以非ASCII文本格式存储这些文件,但仍然向使用其他文件格式的应用程序提供接口。
口令文件就是 /etc/passwd 文件,里面记录了系统的用户的信息,包括用户名,密码(可能单独存放在另一个文件),备注,家目录,默认shell等信息,对Linux比较熟悉的话,对这个文件应该也不陌生。不同的系统的passwd 文件实现不同,Linux实现了7个字段,而FreeBSD 和 MacOS X则实现了全部10个字段(至于各个字段是什么…其实并不重要)。
与口令文件操作相关的函数和结构声明包含在 <pwd.h> 中,其中定义的结构体 struct passwd
用来保存一个用户对应的信息。
#include <pwd.h> |
上面两个函数分别通过uid和username获取口令文件中的信息,它们都返回一个指向passwd结构的指针,该结构在执行函数时填写了信息,一般返回的passwd结构是函数内部的静态变量,只要调用任意相关函数,其内容就会被重写。