C程序无论做什么事都要靠操作系统。例如它想与硬件打交道,就要进行系统调用。系统调用是调用操作系统内核中的函数,C标准库中大部分代码都依赖于它们。例如调用printf()函数在命令行显示出字符串时,C程序都会在背后向操作系统发出系统调用,把字符串发送屏幕显示。
例如system函数:
上面的函数可以在linux平台上打开gedit程序。我们再来看看一个时间函数的系统调用:
patrol.c
编译运行一下:
1.用fgets读取非结构化文本,它的参数comment表示需要把读取到的文本保存在comment数组中,80指定了数组长度,stdin表示从标准输入(即键盘)读取数据。
2.sprintf函数会把格式化 "echo ‘%s %s’>>reports.log"存到cmd数组中,格式化里是comment先出现,时间戳后出现。
注意:虽然程序调用了系统内核函数,但是它的代码不会出现在你的程序里,而是在系统中。在类Unix操作系统中,系统调用的函数在操作系统内核中,而有的平台则是在动态库中。
此外,内核利用设备驱动与连接到计算机上的设备交互。系统调用是程序用来与内核对话的函数。
我们再介绍一下exec()函数调用:
exec()函数替换当前进程,使用此函数时,要包含头文件unistd.h。
进程是在内存中运行的程序。在linux终端ps -ef可以看到系统中运行的进程。操作系统用一个数字来标识进程,它叫进程标识符。
如我们在一终端运行程序:
然后在另一终端查看:
exec()函数通过运行其他程序来替换当前进程,新程序启动后的PID和老程序一样。举个例子:
test3.c
编译运行:
另起一终端查看PID号:
上面是正在运行的test3程序,它的PID是3699,它正在等待我们输入字符串,输完后,就会判断字符长度是否大于1,是的话就会执行execl函数。
输入两个字符12:
再次查看进程列表
新进程已成功替换了PID为3699的老进程了。这有点像两个程序在玩接力赛,老程序把进程交接给了新进程。
顺便一提:exec()函数有很多个版本,分为
- 列表函数:execl() execlp() execle()
- 数组函数:execv() execvp() execve()
列表函数以参数列表形式接收参数,依次为:
程序:第一个参数是要运行的程序
命令行参数:开头同第一个参数一样都是要运行的程序,其后就是其他参数
NULL:在命令行参数后面要加上NULL
环境变量(如果有的话),在NULL后,跟上环境变量,如果有的话。
我们用一个新例子来说明:
testA.c
上面的程序向testB程序传递了两个参数分是"hello world"和"Good !!!"。命令行参数后面必须跟NULL,最后跟上环境变量数组my_env。
两条puts代码会在execle执行失败后执行,如果execle执行成功就不会执行。
errno变量是定义在errno.h中的全局变量。可以和string.h中的strerror()函数一起使用,用于查询标准错误消息。
注意:环境变量数组最后一项必须是NULL,否则程序不知道读取到哪才可以停止。
testB.c
C程序可以用getenv()系统调用读取环境变量,要引用stdlib.h头文件。
编译运行:
教大家一个方法记住这些函数:
我们会发现每个exec()函数后面都会跟一到两个字符,但只能是l、v、p、e中的一个。
l:表示参数列表
v:参数数组/向量
p:根据环境变量PATH查找
e:环境变量
如:
execle = exec + l + e =参数列表+环境变量
其他函数依次类推,自行百度学习吧。
exec()函数通过运行新程序来替换当前程序,那原来的程序去哪儿了?它终止了,而且是立刻终止的。如果想在启动另一个进程的同时让原进程继续运行下去,该怎么做?用fork()克隆进程。
fork()系统调用会克隆当前进程,新建副本将从同一行开始运行相同程序,变量和变量中的值完全一样,只有进程标识符PID和原进程不同。原进程叫父进程,而新建的副本叫子进程。
但是这样一来两个进程除了PID不同之外,都一模一样,不是我们想要的,我们想要的是原进程继续运行,同时启动另一个进程,那么解决的办法就是在子进程中调用exec()函数,这样原来的父进程就能继续运行,新进程也运行起来了。
下面是具体的做法:
用fork()+exec()运行子进程
1.复制进程
第一步用fork()系统调用复制当前进程。进程需要以某种方式区分自己是父进程还是子进程,为此fork()函数向子进程返回0,向父进程返回非0值。
2.如果是子进程,就调用exec()
此时,你有两个完全相同的进程在运行,它们使用相同的代码,但子进程现在需要调用exec()运行程序替换自己。那么就会有两个独立的进程,子进程在运行另一个程序,而父进程则继续运行不受干扰。
我们举个例子吧:
one.c
two.c
编译运行:
查看进程信息:
~$ ps -ef
UID PID PPID C STIME TTY TIME CMD
wong 4790 3095 0 17:16 pts/0 00:00:00 ./one
wong 4791 4790 0 17:16 pts/0 00:00:00 two HELOO 23
可以看到父进程和子进程都在运行,注意它们的PID是不一样的哦。
fork()进程会复制当前进程,过程会有点慢,但是大多数系统都对此进行了优化,如操作系统并不真正复制父进程的数据,而是让父子进程共享数据。当子进程修改了存储器中的数据,操作系统会发现,然后就会为它复制一份。这个技术就是“写时复制”。
总结一下:
系统调用是内核中的函数;
exec()函数比system()函数提供了更多控制权;
exec()函数替换当前进程;
fork()函数复制当前进程;
系统调用失败时通常返回-1;
系统调用失败以后会把errno变量设置为错误码。