Head First C (2) 进程与系统调用

这篇文章中的代码linux和win都可用。

什么是内核?

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

系统调用

C程序无论做什么事都要靠操作系统。如果它想与硬件打交道,就要进行系统调用。系统调用是操作系统内核中的函数, C标准库中大部分代码都依赖于它们。每当调用printf()在命令行显示字符串时, C程序都会在幕后向操作系统发出系统调用,把字符串发送到屏幕。

system()函数
下面来看一个系统调用的例子,我们将从一个名副其实的系统调用——system()开始。system()接收一个字符串参数,并把它当成命令执行:

system("dir D:"); //打印D盘内容。
system("gedit"); //在Linux中启动编辑器。
system("say 'End of line'"); //在Mac上朗读文本

system()函数是在代码中运行其他程序的捷径,特别是在建立快速原型时,与其写很多C代码,不如调用外部程序。不过,system()函数虽然用起来方便,但很多时候需要更规范的方法。你需要用命令行参数甚至是环境变量调用指定程序。

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

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

/*第一个参数告诉exec()函数将运行什么程序
  对execl()或execle()来说,它是程序的完整路径名
  对execlp()来讲就是命令的名字, execlp()会根据它去查找程序*/
//需要在最后一个命令行参数后加上NULL,告诉函数没有其他参数了
execl("/home/flynn/clu", "/home/flynn/clu", "paranoids", "contract", NULL)
execlp("clu", "clu", "paranoids", "contract", NULL)

//execLE = 参数列表(List) + 环境变量(Environment)
//env_vars是一个字符串数组,里面放了环境变量
execle("/home/flynn/clu", "/home/flynn/clu", "paranoids", "contract", NULL, env_vars)

为什么有那么多的exec()函数? 人们想以不同的方式创建进程,于是创建了不同版本的exec()来提高灵活性。

getenv()函数
每个进程都有一组环境变量。你可以在命令行中输入set或env查看它们的值,它们一般会告诉进程一些有用的信息,比如用户主目录的位置 ,或去哪里找命令。 C程序可以用getenv()系统调用读取环境变量。

errno变量
由于系统调用依赖于程序以外的东西,所以它们一旦出错,就没办法控制。为了解决这个问题,系统调用总是以相同方式出错。errno变量是定义在errno.h中的全局变量,和它定义在
一起的还有很多标准错误码,如:

EPERM=1 不允许操作
ENOENT=2 没有该文件或目录
ESRCH=3 没有该进程

这样你就可以拿errno和这些值比较,也可以用string.h中的strerror()的函数查询标准错误消息:

puts(strerror(errno));  //strerror()将错误码转换为一条消息

当系统找不到你想运行的程序时就会把errno变量设置为ENOENT,以上代码就会显示这条消息:没有该文件或目录

练习1

你可以在不同的机器上用不同命令查看网络配置。 在Linux和Mac上, 你可以用一个叫/sbin/ifconfig的程序; 而在Windows上可以用ipconfig的命令, 它的路径保存在命令路径中。下面这个程序试图运行/sbin/ifconfig程序, 如果失败就运行ipconfig命令。 你不用传递任何参数给这两条命令, 仔细考虑将使用什么类型的exec()命令?

#include <stdio.h>
#include <unistd.h> //为了使用exec()函数,你需要它
#include <errno.h> //为了使用errno变量,你需要它。
#include <string.h> //有了它,就能用strerror()函数显示错误消息了
int main()
{
	//使用execl(),因为你有程序文件的路径
	//如果execl()返回-1,就表明它执行失败,我们应该去找ipconfig
	if (execl("/sbin/ifconfig”,“/sbin/ifconfig",NULL) == -1) 
	{
		//我们可以用execlp()根据PATH查找ipconfig命令
		if (execlp("ipconfig","ipconfig",NULL)==-1) 
		{
			fprintf(stderr, "Cannot run ipconfig: %s",strerror(errno));
			return 1;
		}
	}
	return 0;
}
练习2

注:此练习代码只可在linux系统下执行。
RSS源是网站发布新闻的常用方式。 RSS源其实就是一个XML文件,里面有新闻的摘要和链接。当然,你完全有能力写一个直接从网页读取RSS文件的C程序,但这涉及一些你没有接触过的编程概念。为什么不找一个程序帮忙处理RSS文件呢?RSS Gossip是一个Python小脚本,它可以根据某个关键字在RSS源中查找新闻。你必须先安装Python才能运行这个脚本。一旦有了Python和rssgossip.py,就可以在终端里像这样搜索新闻:

export RSS_FEED=http://www.cnn.com/rss/celebs.xml  //创建一个放RSS源地址的环境变量
python rssgossip.py 'pajama death' //用搜索关键字运行rssgossip脚本

编辑希望程序一次搜索多个RSS源, 为此你可以为不同的RSS源多次运行rssgossip。newshound程序代码。

int main(int argc, char *argv[])
{
	char *feeds[] = {"http://www.cnn.com/rss/celebs.xml",
				     "http://www.rollingstone.com/rock.xml",
				 	 "http://eonline.com/gossip.xml"};
	int times = 3;
	char *phrase = argv[1]; //把搜索关键字当做参数传递
	int i;
	for (i = 0; i < times; i++)  //遍历RSS源
	{
		char var[255];
		sprintf(var, "RSS_FEED=%s", feeds[i]);
		char *vars[] = {var, NULL};  //环境变量数组
		if ( execle("/usr/bin/python", "/usr/bin/python", 
		     "./rssgossip.py", phrase, NULL, vars) == -1) 
		{
			fprintf(stderr, "Can't run script: %s\n", strerror(errno));
			return 1;
		}
	}
	return 0;
}

运行程序:

./newshound 'pajama death'

运行结果:只显示了列表中第一条RSS源的新闻。exec()函数通过运行新程序来替换当前程序,那原来的程序去哪儿了?它终止了,而且是立刻终止。所以,程序会在第一次运行exec()之后终止。无法通过循环来一次搜索多个RSS源。

用fork()克隆进程 ——windows不支持
你可以用一个叫fork()的系统调用来解决这个问题。fork()会克隆当前进程。新建副本将从同一行开始运行相同程序,变量和变量中的值完全一样,只有进程标识符(PID)和原进程不同。原进程叫父进程,而新建副本叫子进程。

用fork()+exec()运行子进程
诀窍是在子进程中调用exec()函数,这样原来的父进程就能继续运行了。
①复制进程。第一步用fork()系统调用复制当前进程。进程需要以某种方式区分自己是父进程还是子进程,为此fork()函数向子进程返回0,向父进程返回非零值。

②如果是子进程,就调用exec()。这一刻,你有两个完全相同的进程在运行,它们使用相同的代码,但子进程(从fork()接收到0的那个)现在需要调用exec()运行程序替换自己。

现在你有两个独立的进程:子进程在运行rssgossip.py脚本,而原来的父进程可以继续做其他事,完全不受干扰。

你可以像这样调用fork():

pid_t pid = fork();

fork()会返回一个整型值: 为子进程返回0, 为父进程返回一个正数。 父进程将接收到子进程的进程标识符。什么是pid_t? 不同操作系统用不同的整数类型保存进程ID, 有的用short, 有的用int, 操作系统使用哪种类型, pid_t就设为哪个。

#include <stdio.h>
#include <unistd.h> //为了使用exec()函数,你需要它
#include <errno.h> //为了使用errno变量,你需要它。
#include <string.h> //有了它,就能用strerror()函数显示错误消息了
int main(int argc, char *argv[])
{
	char *feeds[] = {"http://www.cnn.com/rss/celebs.xml",
				     "http://www.rollingstone.com/rock.xml",
				 	 "http://eonline.com/gossip.xml"};
	int times = 3;
	char *phrase = argv[1]; //把搜索关键字当做参数传递
	int i;
	for (i = 0; i < times; i++)  //遍历RSS源
	{
		char var[255];
		sprintf(var, "RSS_FEED=%s", feeds[i]);
		char *vars[] = {var, NULL};  //环境变量数组
		pid_t pid = fork(); //首先,调用fork()克隆进程
		if (pid == -1) //如果fork()返回-1,就说明在克隆进程时出了问题
		{
			fprintf(stderr, "Can't fork process: %s\n", strerror(errno));
			return 1;
		}
		if (!pid) //相当于if(pid==0),如果fork()返回0,说明代码运行在子进程中
		{
			//如果你执行到这里,说明你是子进程,应该调用exec()运行脚本
			if ( execle("/usr/bin/python", "/usr/bin/python", "./rssgossip.py", phrase, NULL, vars) == -1) 
			{
				fprintf(stderr, "Can't run script: %s\n", strerror(errno));
				return 1;
			}
		}
	}
	return 0;
}

这些进程将同时运行。噢!实验完成。但是由于现在RSS源很少被人用到,所以我也没有试验书中的代码,感兴趣的小伙伴可以尝试一下!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值