进程与系统调用

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

#include <stdlib.h>
int main(){
        system("gedit");
        return 0;
}

上面的函数可以在linux平台上打开gedit程序。我们再来看看一个时间函数的系统调用:
patrol.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

char* now(){
        time_t t;
        time(&t);
        return asctime(localtime(&t));
}

int main(){
        char comment[80];
        char cmd[120];
        fgets(comment,80,stdin);
        sprintf(cmd,"echo '%s %s'>>reports.log",comment,now()); //程序会创建reports.log文件
        system(cmd);
        return 0;
}

编译运行一下:

~/Desktop/Mc$ gcc patrol.c -o patrol
~/Desktop/Mc$ ./patrol
This is a new day!
~/Desktop/Mc$ tail -f reports.log
This is a new day!
 Sun Feb 17 11:58:02 2019

1.用fgets读取非结构化文本,它的参数comment表示需要把读取到的文本保存在comment数组中,80指定了数组长度,stdin表示从标准输入(即键盘)读取数据。
2.sprintf函数会把格式化 "echo ‘%s %s’>>reports.log"存到cmd数组中,格式化里是comment先出现,时间戳后出现。

注意:虽然程序调用了系统内核函数,但是它的代码不会出现在你的程序里,而是在系统中。在类Unix操作系统中,系统调用的函数在操作系统内核中,而有的平台则是在动态库中。

此外,内核利用设备驱动与连接到计算机上的设备交互。系统调用是程序用来与内核对话的函数。

我们再介绍一下exec()函数调用:
exec()函数替换当前进程,使用此函数时,要包含头文件unistd.h。

进程是在内存中运行的程序。在linux终端ps -ef可以看到系统中运行的进程。操作系统用一个数字来标识进程,它叫进程标识符。
如我们在一终端运行程序:

~/Desktop/Mc$ ./patrol
|

然后在另一终端查看:

~$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
wong      3521  3095  0 12:35 pts/0    00:00:00 ./patrol
...

exec()函数通过运行其他程序来替换当前进程,新程序启动后的PID和老程序一样。举个例子:
test3.c

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main(){
        char c[80];
        fgets(c,89,stdin);
        if(strlen(c)>1){
                execl("/usr/bin/gedit","/usr/bin/gedit",NULL);
        }
}

编译运行:

~/Desktop/Mc$ gcc test3.c -o test3
~/Desktop/Mc$ ./test3
|

另起一终端查看PID号:

~$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
wong      3699  3095  0 12:56 pts/0    00:00:00 ./test3

上面是正在运行的test3程序,它的PID是3699,它正在等待我们输入字符串,输完后,就会判断字符长度是否大于1,是的话就会执行execl函数。
输入两个字符12:

~/Desktop/Mc$ ./test3
12
|

再次查看进程列表

 ~$ ps -ef
UID        PID     PPID  C  STIME  TTY         TIME  CMD
wong      3699  3095   0  12:56     pts/0    00:00:00  /usr/bin/gedit

新进程已成功替换了PID为3699的老进程了。这有点像两个程序在玩接力赛,老程序把进程交接给了新进程。

顺便一提:exec()函数有很多个版本,分为

  1. 列表函数:execl() execlp() execle()
  2. 数组函数:execv() execvp() execve()

列表函数以参数列表形式接收参数,依次为:
程序:第一个参数是要运行的程序
命令行参数:开头同第一个参数一样都是要运行的程序,其后就是其他参数
NULL:在命令行参数后面要加上NULL
环境变量(如果有的话),在NULL后,跟上环境变量,如果有的话。
我们用一个新例子来说明:
testA.c

 #include <unistd.h>
 #include <stdio.h>
 #include <errno.h>
 #include <string.h>

    
    int main(){
            char *my_env[] = {"APPLE_ENV=this is a apple tree",NULL};
            execle("testB","testB","hello world","Good !!!",NULL,my_env);
            puts("what happened!!!");
 			puts(strerror(errno));

            return 0;
    }

上面的程序向testB程序传递了两个参数分是"hello world"和"Good !!!"。命令行参数后面必须跟NULL,最后跟上环境变量数组my_env。
两条puts代码会在execle执行失败后执行,如果execle执行成功就不会执行。
errno变量是定义在errno.h中的全局变量。可以和string.h中的strerror()函数一起使用,用于查询标准错误消息。

注意:环境变量数组最后一项必须是NULL,否则程序不知道读取到哪才可以停止。

testB.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc,char *argv[]){
        printf("Argument 1:%s\n",argv[1]);
        printf("Argument 2:%s\n",argv[2]);
        printf("Environment Arg:%s\n",getenv("APPLE_ENV"));
        return 0;
}

C程序可以用getenv()系统调用读取环境变量,要引用stdlib.h头文件。

编译运行:

~/Desktop/Mc$ gcc testA.c -o testA
~/Desktop/Mc$ gcc testB.c -o testB
~/Desktop/Mc$ ./testA
Argument 1:hello world
Argument 2:Good !!!
Environment Arg:this is a apple tree

教大家一个方法记住这些函数:
我们会发现每个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

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
int main(){
        pid_t pid = fork();

        if(pid == -1){
                fprintf(stderr,"Can't fork process: %s\n",strerror(errno));
                return 1;
        }

        if(!pid){
                char *my_env[] = {"APPLE=this is an apple",NULL};
                if(execle("two","two","HELOO","23",NULL,my_env) == -1){
                        fprintf(stderr,"Can't run script: %s\n",strerror(errno));
                        return 1;
                }
        }
        char c[80];
        fgets(c,80,stdin);
}

two.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int args,char *argv[]){
        printf("First Arg:%s\n",argv[1]);
        printf("second Arg:%s\n",argv[2]);
        printf("Env Arg: %s\n",getenv("APPLE"));
        char c[80];
        fgets(c,80,stdin);
}

编译运行:

~/Desktop/Mc$ gcc one.c -o one
~/Desktop/Mc$ gcc two.c -o two
~/Desktop/Mc$ ./one
First Arg:HELOO
second Arg:23
Env Arg: this is an apple

查看进程信息:
~$ 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变量设置为错误码。

猜你喜欢

转载自blog.csdn.net/weixin_40763897/article/details/87525510
今日推荐