HeadFirstC笔记_9 进程与系统调用:打破疆界

操作系统热线电话
C程序无论做什么事都要靠操作系统。如果它想与硬件 打交道,就要进行系统调用。系统调用是操作系统内核 中的函数,C标准库中大部分代码都依赖于它们

system()
sytem()是一个系统函数,它接收一个字符串参数,并把它当成命令执行:
   
    
    
  1. system("dir D:"); // windows上 打印D盘内容。
  2. system("gedit"); // linux上 启动编辑器。
  3. system("say 'End of line'"); // Mac上朗读文本。
system() 函数是在代码中运行其他程序的捷径,特别 是在建立快速原型时,与其写很多C代码,不如调用外 部程序。

下面代码将一段带有时间戳的文本写到日志文件的底部。整个程序都可以用C 语言来写,但程序员用了system()调用,因为它可以更快速地处理文件。
    
     
     
  1. // chapter9.c
  2. #include <_mingw.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <time.h>
  6. #include <string.h>
  7. /*descript:replace str,返回一个替换以后的字符串,用完之后要free()*/
  8. char* replacestr(char *strbuf, char *sstr, char *dstr) {
  9. char *p, *p1;
  10. int len;
  11. if ((strbuf == NULL) || (sstr == NULL) || (dstr == NULL))
  12. return NULL;
  13. p = strstr(strbuf, sstr); //返回字符串第一次出现的地址,否则返回NULL
  14. if (p == NULL) /*not found*/
  15. return NULL;
  16. len = strlen(strbuf) + strlen(dstr) - strlen(sstr);
  17. p1 = malloc(len);
  18. // bzero(p1, len); // 这个函数有个string.h里没有定义,不过他也调的是memset(a,0,b);函数
  19. memset(p1, 0, len);
  20. strncpy(p1, strbuf, p - strbuf);
  21. strcat(p1, dstr);
  22. p += strlen(sstr);
  23. strcat(p1, p);
  24. return p1;
  25. }
  26. /*获取当前时间*/
  27. char* now() {
  28. time_t t;
  29. time(&t);
  30. return asctime(localtime(&t)); // "Tue May 21 13:46:22 1991\n",注意这里最后也会带个回车符
  31. }
  32. /* 主控程序, 用来登记警卫的巡逻记录。 */
  33. int main(void) {
  34. char comment[80];
  35. char cmd[120];
  36. // 从键盘读取文本保存在comment字符数组中
  37. fgets(comment, 80, stdin); // 在命令行 输入字符串后按回车结束输入,但是此函数会将回车符也存进去,导致得到的comment字符串不符合要求
  38. // puts(comment);
  39. // puts(now());
  40. // 把第一个参数后面的一系列字符串写到字符数组cmd中
  41. sprintf(cmd, "echo '%s %s' >> reports.log", comment, now());
  42. //comment,now() 由于这两个字符串都带了\n结尾,所以后面的system(cmd)运行错误
  43. // 所以这里将字符串中的 \n 全部替换成空字符串,后面就正常了,有两个故调两次
  44. char* dstr = replacestr(cmd, "\n", "");
  45. dstr = replacestr(dstr, "\n", "");
  46. puts(dstr);
  47. // echo 'fdsfdsafdsf Mon Nov 28 11:44:25 2016' >> reports.log
  48. // system("echo 'fdsfdsafdsf Mon Nov 28 11:44:25 2016' >> reports.log");
  49. system(dstr);
  50. // 别忘了释放掉malloc()生成的p1,即dstr
  51. free(dstr);
  52. return 0;
  53. }
运行结果:

生成了 reports.log,并追加到末尾了。

 程序工作了。它从命令行读取注释,然后调用 echo 命令 把注释追加到文件底部。 整个程序都可以用C语言来写,但你用 system() 简化了 程序,可谓事半功倍。

问: 我在进行系统调用时会调用外部代码,像库一样,是吗?
答: 差不多,但具体细节要看操作系统。在一些操 作系统中,系统调用的代码位于操作系统内核。而对其 他操作系统而言,系统调用可能保存在动态库中。

system() 函数的弊端
先来看看如何入侵程序。 代码通过拼接命令字符串的方式工作,像这样:
       
        
        
  1. echo ' <comment> <timestamp> ' >> reports.log
但如果有人输入了这样的命令怎么办?
        
         
         
  1. echo ' && ls / && echo ' <timestamp> ' >> reports.log (linux中)
通过在文本中注入命令行代码,就能随心所欲地让 程序运行任何命令:

这个问题很严重!只要用户能运行 guard_log ,就 能轻易地运行其他程序。

岂止是安全问题
也可以删除文件或启动病毒。但你不应该只关注安全问题。
1.注释文本中出现了撇号怎么办?----->这会破坏 echo 命令中的引号。
2.PATH变量让system()函数调错了程序怎么办?
3.需要先设置一批专门的环境变量,程序才能工作,怎么办?
system() 函数用起来方便,但很多时候需要更规范的方法。你 需要用命令行参数甚至是环境变量调用 指定 程序。

什么是内核?
在大部分计算机上,系统调用就是操作系统内核中的函数。什么是内核?虽然你从来没在屏幕上看到过 它,但内核其实一直都在那里控制计算机。内核是计算机中最重要的程序,它主管三样东西:
进程
只有当内核把程序加载到存储器时程序才能运行。内核创建进程,并确保它们得到了所需资源。内核同 时也会留意那些变得贪得无厌或者已经崩溃的进程。
存储器
计算机所能提供的存储器资源是有限的,因此内核必须小心翼翼地分配每个进程所能使用的存储器大 小。内核还能把部分存储器交换到磁盘从而增加虚拟存储器空间。
硬件
内核利用设备驱动与连接到计算机上的设备交互。你的程序在不了解键盘、屏幕和图形处理器的情况下 就能使用它们,因为内核会代表你与它们交涉。
系统调用是程序用来与内核对话的函数。

exec()给你更多控制权
当调用 system() 函数时,操作系统必须解释命令字符串, 然后决定运行哪些程序和怎样运行。问题就出在“操作系 统需要解释字符串”上,你已经看到这有多么容易出错。 要想解决这个问题就必须消除歧义,明确地告诉操作系 统你想运行哪个程序,这就是 exec() 函数的用处。

exec()函数替换当前进程
进程是存储器中运行的程序。如果在Windows中输入 taskmgr ,或在Linux或Mac上面输入 ps –ef ,就可以看 到系统中运行的进程。操作系统用一个数字来标识进程, 它叫进程标识符(process identifier,简称PID)。
exec() 函数通过运行其他程序来替换当前进程。你可以 告诉 exec() 函数要使用哪些命令行参数和环境变量。新 程序启动后PID和老程序一样,就像两个程序接力跑,你 的程序把进程交接给了新程序。

exec()函数有很多
exec() 函数的版本众多,但可以分为两组:列表函数和数 组函数。exec()函数在头文件  unistd.h中
列表函数:execl()、execlp()、execle(),特点是都有一个l,代表list(列表)
列表函数以参数列表的形式接收命令行参数:
1.程序。
第一个参数告诉 exec() 函数将运行什么程序。对 execl() 或 execle() 来说,它是程序的完整路径名;对 execlp() 来讲就 是命令的名字, execlp() 会根据它去查找程序。
2. 命令行参数。
你需要依次列出想使用的命令行参数。别忘了,第一个命令行参 数必须是程序名,也就是说列表版 exec() 的前两个参数是相同 字符串。
3.
NULL。
没错,需要在最后一个命令行参数后加上 NULL ,告诉函数没有 其他参数了。
4.环境变量(如果有的话)。
如果调用了以 ...e() 结尾的 exec() 函数,还可以传递环境变量数组, 像“ POWER=4 ”、“ SPEED=17 ”、“ PORT=OPEN ”……那样的字符 串数组。
    
     
     
  1. // execl = 参数列表(List) // 最后用NULL来结束列表
  2. execl("/home/flynn/clu", "/home/flynn/clu", "paranoids", "contract", NULL);
  3. // execlp = 参数列表(List) + 在PATH中查找程序。
  4. execlp("clu", "clu", "paranoids", "contract", NULL);
  5. // execle= 参数列表(List) + 环境变量(Environment) // env_vars是一个字符串数组,里面放了环境变量。
  6. execle("/home/flynn/clu", "/home/flynn/clu", "paranoids", "contract", NULL, env_vars);

数组函数:execv()、execvp()、execve(), 特点是都有一个v,代表vector(数组向量)
如果已经把命令行参数保存在了数组中,就会发现这两个版 本用起来更容易:
    
     
     
  1. // execv=参数数组或参数向量(Vector)
  2. execv("/home/flynn/clu", my_args); // 参数需要保存在字符串数组my_args中
  3. // execvp = 参数数组/向量(Vector) + 在PATH中查找。
  4. execvp("clu", my_args);
  5. //上面两个函数的唯一区别就是 execvp 会用 PATH 变量查找程序。
教你如何记住exec()函数
可以通过构造名称的方法来找到你需要的 exec() 函数。每个 exec() 函数名之后可以跟一到两个字符,但只能是 l 、 v 、 p 和 e 中的一个。它们分别代表你想使用的功能。对 execle() 函数来讲:

 
传递环境变量
每个进程都有一组环境变量。你可以在命 令行中输入 set 或 env 查看它们的值,它 们一般会告诉进程一些有用的信息,比 如用户主目录的位置,或去哪里找命 令。C程序可以用 getenv() 系统调用读 取环境变量,下面的 diner_info.c 程序 演示了 getenv() 的使用方法。
    
     
     
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main(int argc, char *argv[])
  4. {
  5. printf("Diners: %s\n", argv[1]);
  6. printf("Juice: %s\n", getenv("JUICE"));
  7. return 0;
  8. }
如果你想用命令行参数和环境变量运行 程序,可以这样做:
     
      
      
  1. // 用字符串指针数组的形式创建一组环境变量。
  2. char *my_env[] = { "JUICE=peach and apple", NULL}; // 数组最后一项必须是NULL
  3. execle("diner_info", "diner_info", "4", NULL, my_env);
execle() 函数将设置命令行参数和环境变量,然后用 diner_info 替换当前进程.

出错了怎么办?
如果在调用程序时发生错误,当前进程会继续运行。这点 很有用,因为就算第二个进程启动失败,还是能够从错误 中恢复过来,并向用户报告错误信。

大多数系统调用以相同方式出错
由于系统调用依赖于程序以外的东西,所以它们一旦出 错,就没办法控制。为了解决这个问题,系统调用总是 以相同方式出错。
就拿 execle() 调用来说,判断 exec() 有没有出错很容 易:如果 exec() 调用成功,当前程序就会停止运行。一 旦程序运行了 exec() 以后的代码,就说明出了问题。
但仅仅告诉用户系统调用失败与否是不够的,通常你想 知道系统调用为什么失败,因此几乎所有系统调用都遵
失败黄金法则”:
1.尽可能收拾残局。
2.把errno变量设为错 误码。
3.返回 -1。

errno 变量
errno 变量是定义在errno.h中的全局变量,和它定义在 一起的还有很多标准错误码,如:
EPERM=1  不允许操作
ENOENT=2  没有该文件或目录
ESRCH=3  没有该进程

这样你就可以拿 errno 和这些值比较,也可以用string.h中 的 strerror() 的函数查询标准错误消息,当系统找不到你想运行的程序时就会把 errno 变量设置 为 ENOENT ,以上代码就会显示这条消息:没有该文件或目录。

代码示例:my_exec_programc.c
    
     
     
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <errno.h>
  4. int main(int argc, char *argv[]) {
  5. //可以用字符串指针数组的形式创建一组环境变量。环境变量的格式是“变量名=值”。数组最后一项必须是NULL。
  6. char *my_env[] = { "JUICE=peach and apple", NULL };
  7. execle("diner_info", "diner_info", "4", NULL, my_env);
  8. //如果 exec() 调用成功,当前程序就会停止运行。一旦程序运行了 exec() 以后的代码,就说明出了问题。
  9. puts(" 哥们, diner_info程序肯定发生了什么问题 ");
  10. // errno = 1; // 手动改错误码
  11. //当系统找不到你想运行的程序时就会把 errno 变量设置为 ENOENT
  12. //strerror()将错误码转换为一条消息。
  13. puts(strerror(errno));
  14. return -1;
  15. }
运行结果:

调用系统命令查看网络配置
你可以在不同的机器上用不同命令查看网络配置。在Linux和Mac上,你可以用一个 叫/sbin/ifconfig的程序;而在Windows上可以用ipconfig的命令,它的路径 保存在命令路径中。 下面这个程序试图运行/sbin/ifconfig程序,如果失败就运行ipconfig命令。你 不用传递任何参数给这两条命令,仔细考虑需要使用什么类型的exec()命令?
      
       
       
  1. #include <stdio.h>
  2. #include <unistd.h> // 为了使用exec()函数,你需要它。
  3. #include <errno.h> // 为了使用errno变量,你需要它。
  4. #include <string.h> // 有了它,就能用strerror()函数显示错误消息了。
  5. int main() {
  6. // 先尝试运行/sbin/ifconfig程序,返回-1说明执行失败
  7. if (execl("/sbin/ifconfig", "/sbin/ifconfig", NULL) == -1) {
  8. // 如果打开失败,就尝试运行 ipconfig
  9. if (execlp("ipconfigf", "ipconfigf", NULL) == -1) {
  10. fprintf(stderr, "Cannot run ipconfig: %s", strerror(errno)); //strerror()函数将显示任何可能出现的错误。
  11. return 1;
  12. }
  13. }
  14. return 0;
  15. }

问: 调用了exec()函数以后还能做其他事吗?
答: 不能,只要让exec()函数执行成功,就会修改 进程。它会运行新程序替代你的程序。也就是说,只要 exec()函数一运行,你的程序就会停止运行

代码示例:
一个新的订单生成程序offee.c
      
       
       
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main(int argc, char *argv[]) {
  4. char *w = getenv("EXTRA");
  5. if (!w)
  6. w = getenv("FOOD");
  7. if (!w)
  8. w = argv[argc - 1];
  9. char *c = getenv("EXTRA");
  10. if (!c)
  11. c = argv[argc - 1];
  12. printf("%s with %s\n", c, w);
  13. return 0;
  14. }
为了检验程序,这里又创建了一个测试程序test_coffee.c
       
        
        
  1. #include <string.h>
  2. #include <stdio.h>
  3. #include <errno.h>
  4. #include <unistd.h>
  5. int main(int argc, char *argv[]) {
  6. // 打印结果:donuts with coffee
  7. // char *my_env[] = { "FOOD=coffee", NULL };
  8. // if (execle("./coffee", "./coffee", "donuts", NULL, my_env) == -1) {
  9. // fprintf(stderr, "Can't run process 0: %s\n", strerror(errno));
  10. // return 1;
  11. // }
  12. // 打印结果:cream with donuts
  13. // char *my_env[] = { "FOOD=donuts", NULL };
  14. // if (execle("./coffee", "./coffee", "cream", NULL, my_env) == -1) {
  15. // fprintf(stderr, "Can't run process 0: %s\n", strerror(errno));
  16. // return 1;
  17. // }
  18. // 打印结果:./coffee with ./coffee
  19. // if (execl("./coffee", "./coffee", NULL) == -1) {
  20. // fprintf(stderr, "Can't run process 0: %s\n", strerror(errno));
  21. // return 1;
  22. // }
  23. // 打印结果:coffee with donuts
  24. char *my_env[] = { "FOOD=donuts", NULL };
  25. if (execle("./coffee", "./coffee", NULL, my_env) == -1) {
  26. fprintf(stderr, "Can't run process 0: %s\n", strerror(errno));
  27. return 1;
  28. }
  29. return 0;
  30. }

用RSS读新闻
RSS源是网站发布新闻的常用方式。RSS源其实 就是一个XML文件,里面有新闻的摘要和链接。 当然,你完全有能力写一个直接从网页读取RSS
文件的C程序,但这涉及一些你没有接触过的编程 概念。你可以先找一个程序帮忙处理RSS文件。
RSS Gossip脚本下载地址:https://github.com/dogriffiths/rssgossip
如果你没有安装过Python,可以从这里下载: http://www.python.org/
下面是 rssgossip.py文件:
         
          
          
  1. import urllib
  2. import os
  3. import re
  4. import sys
  5. import string
  6. import unicodedata
  7. import getopt
  8. from xml.dom import minidom
  9. def usage():
  10. print("Usage:\npython rssgossip.py [-uh] <search-regexp>")
  11. try:
  12. opts, args = getopt.getopt(sys.argv[1:], "uh", ["urls", "help"])
  13. except getopt.GetoptError, err:
  14. print str(err)
  15. usage()
  16. sys.exit(2)
  17. include_urls = False
  18. for o, a in opts:
  19. if o == "-u":
  20. include_urls = True
  21. elif o in ("-h", "--help"):
  22. usage()
  23. sys.exit()
  24. else:
  25. assert False, "unhandled option"
  26. searcher = re.compile(args[0], re.IGNORECASE)
  27. for url in string.split(os.environ['RSS_FEED']):
  28. feed = urllib.urlopen(url)
  29. try:
  30. dom = minidom.parse(feed)
  31. forecasts = []
  32. for node in dom.getElementsByTagName('title'):
  33. txt = node.firstChild.wholeText
  34. if searcher.search(txt):
  35. txt = unicodedata.normalize('NFKD', txt).encode('ascii', 'ignore')
  36. print(txt)
  37. if include_urls:
  38. p = node.parentNode
  39. link = p.getElementsByTagName('link')[0].firstChild.wholeText
  40. print("\t%s" % link)
  41. except:
  42. sys.exit(1)
下面的程序能一次搜索多个RSS源,为此你可以为不同的RSS源多次运行 rssgossip. py
newshound.c文件:
          
           
           
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <errno.h>
  4. #include <unistd.h>
  5. int main(int argc, char *argv[]) {
  6. char *feeds[] = { "http://syndication.eonline.com/syndication/feeds/rssfeeds/topstories.xml",
  7. "http://rss.sina.com.cn/news/society/wonder15.xml",
  8. "http://rss.sina.com.cn/news/society/misc15.xml" };
  9. int times = 3;
  10. char* phrase = argv[1]; // 我们把搜索关键字当做参数传递
  11. int i;
  12. for (i = 0; i < times; i++) {
  13. char var[255];
  14. sprintf(var, "RSS_FEED=%s", feeds[i]); // 把环境变量写到var变量中
  15. char *vars[] = { var, NULL }; // 环境变量数组。
  16. if (execle("C:/Python27/python","C:/Python27/python","rssgossip.py", phrase, NULL, vars) == -1) {
  17. fprintf(stderr, "Can't run script: %s\n", strerror(errno));
  18. return 1;
  19. // }
  20. }
  21. return 0;
  22. }
运行结果:
 
程序其实有问题!
newshound 程序虽然运行了rssgossip.py脚本, 但它并没有为所有RSS源都运行脚本。它实际 上只显示了列表中第一条RSS源的新闻,而与 搜索关键字匹配的其他新闻都不见了踪影。

原因:exec() 是程序中最后一行代码
exec() 函数通过运行新程序来替换当前程序,
那原来的程序就马上终止了,
这就是为什么程序只为第一条RSS源
运行了rssgossip.py脚本。程序在第一次调用
execle() 以后 newshound 程序就终止了。

解决方案:用fork()克隆进程
fork() 会克隆当前进程。新建副本将从同一行
开始运行相同程序,变量和变量中的值完全一
样,只有进程标识符(PID)和原进程不同。
原进程叫父进程,而新建副本叫子进程。
(注意:与Unix和Mac不 同,Windows天 生不支持fork()。 如果想在Windows 中使用fork(),必须先要安装 Cygwin。)

用fork()+exec()运行子进程
诀窍是在子进程中调用 exec() 函数,这样原来的父进程 就能继续运行了。我们一步一步来看。

1. 复制进程
第一步用 fork() 系统调用复制当前进程。
进程需要以某种方式区分自己是父进程还是子进程,为
此 fork() 函数向 子进程返回0,向父进程返回非零值。
2. 如果是子进程,就调用exec()
此时你有两个完全相同的进程在运行,它们使用相
同的代码,但子进程(从 fork()接 收到0的那个)现在
需要调用 exec() 运行程序替换自己,
而原来的父进程可以继续做其他事,完全不受干扰。

下面就来修改newshound程序
     
      
      
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <errno.h>
  4. #include <unistd.h>
  5. int main(int argc, char *argv[]) {
  6. char *feeds[] = { "http://syndication.eonline.com/syndication/feeds/rssfeeds/topstories.xml",
  7. "http://syndication.eonline.com/syndication/feeds/rssfeeds/topstories.xml",
  8. "http://syndication.eonline.com/syndication/feeds/rssfeeds/topstories.xml"};
  9. int times = 3;
  10. char* phrase = argv[1];
  11. int i;
  12. for (i = 0; i < times; i++) {
  13. char var[255];
  14. sprintf(var, "RSS_FEED=%s", feeds[i]);
  15. char *vars[] = { var, NULL };
  16. // fork()会返回一个整型值:为子进程返回0,为父进程返回一个正数。父进程将接收到子进程的进程标识符。
  17. pid_t pid = fork();// 调用fork()克隆进程
  18. //什么是pid_t?不同操作系统用不同的整数类型保存进程ID,有的用short,有的用int,操作系统使用哪种类型,pid_t就设为哪个。
  19. // 与Unix和Mac不同, Windows天生不支持fork()。如果想在Windows中使用fork(),必须先要安装 Cygwin。
  20. if (pid == -1){ //如果fork()返回-1,就说明在克隆进程时出了问题。
  21. fprintf(stderr, "Can't fork process: %s\n", strerror(errno));
  22. return 1;
  23. }
  24. if (!pid) { // 相当于if(pid==0),进来说明代码运行在子进程中
  25. // 子进程(linux下)
  26. if (execle("/usr/bin/python","/usr/bin/python","rssgossip.py", phrase, NULL, vars) == -1) {
  27. fprintf(stderr, "Can't run script: %s\n", strerror(errno));
  28. return 1;
  29. }
  30. }
  31. }
  32. return 0;
  33. }
运行结果:

 通过克隆自己,然后在独立进程中运行Python脚 本, newshound 成功地为每个RSS源运行了独立的 进程,而且最妙的是这些进程将同时运行。这要比逐条读取新闻源快多了。通过学习用 fork() 和 exec() 创建并运行独立进程,不但能更好地利用现有软 件,而且还能提高程序的性能。

问: 克隆进程岂不是很慢?我的意思是在用exec()替换子进程前我们还要等fork()复制完整个进程。
答: 为了让fork进程变快,操 作系统使用了很多技巧。比如操作系 统不会真的复制父进程的数据,而是 让父子进程共享数据

问: 这样一来,如果子进程修改了存储器中的数据,岂不是会把事情搞砸?
答: 不会,如果操作系统发现 子进程要修改存储器,就会为它复制 一份。这就叫“写时复制”(copy-
on-write)。

问: pid_t就是int吗?
答: 这取决于平台,你唯一知 道的就是它是整型。

问: 我在int里保存fork()调用的结果,程序还是能运行。
答: 最好还是用pid_t来保存进 程ID,否则当把代码拿到其他机器上 编译时可能会出错。

问 :如 果 我 想 让 代 码 能 在Windows上运行,但它又不支持fork(),有其他替代品吗?
答: 嗯,有一个名叫Create Process()的函数,它是一个加强版 的system()。如果你想了解更多信 息,可以到http://msdn.microsoft.com 搜索CreateProcess。

猜你喜欢

转载自blog.csdn.net/woshiwangbiao/article/details/53743260