进程
文章目录
1.进程概念
1.1什么是程序,什么是进程,有什么区别?
程序是静态的概念,gcc xxx.c -o pro
磁盘中生成的文件,叫做程序。
进程是程序的一次运行活动,通俗点的意思就是程序跑起来了,系统中就多了一个进程。
下面的这些程序,我们不打开它时,他就呆呆的待在桌面上,此时只是一个程序,如果双击运行起来,就开始执行,后台也会多了一个东西,它的名字就叫进程
1.2 如何查看系统中有那些进程
ps指令
实际工作中,配合grep来查找程序中是否存在某一个进程。(他会帮你过滤掉一些信息)
ps -aux|grep xxx;
top指令查看
类似windows的任务管理器
1.3什么是进程标识符
每一个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证
pid = 0 ;称为交换进程(swapper) 作用—进程的调度
pid = 1 ;init 进程 作用—系统的初始化
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = getpid();
printf("my pid is %d\n",pid);//并不是固定的,每次运行都会变,这点也要理解
while(1);
return 0;
}
1.4什么是父进程?什么是子进程?
进程 A 创建了进程 B
那么 A 叫做父进程,B 叫做子进程,父子进程是相对的概念,理解为人类中的父子关系
1.5C语言存储空间是如何分配的
2.创建进程函数fork
pid_t fork(void);
fork 函数会新生成一个进程,调用 fork 函数的进程为父进程,新生成的进程为子进程。
在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。
fork 函数调用成功,返回两次,fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程pid;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值
通俗理解就是:fork是复制进程的函数,程序一开始就会产生一个进程,当这个进程(代码)执行到fork()时,fork就会复制一份原来的进程即就是创建一个新进程,我们称子进程,而原来的进程我们称为父进程,此时父子进程是共存的,他们一起向下执行代码。
注意:就是调用fork函数之后,一定是两个进程同时执行fork函数之后的代码,而之前的代码以及由父进程执行完毕。
2.1根据代码研究fork函数
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
pid_t pid2;
pid = getpid();
printf("before fork: pid = %d \n",pid);
fork();
pid2 = getpid();
printf("after fork: pid = %d \n",pid2);
if(pid == pid2){
printf("this id father print , pid = %d \n",getpid());
}
else{
printf("this id child print , pid = %d \n",getpid());
}
return 0;
}
通过这个我们发现:
在调用fork函数之前,只有一个进程在跑,而调用了fork函数之后,原先的进程变为了父进程,紧接着下面代码的运行,而衍生出来的子进程,从调用它的代码后面接着运行,单独走一路与父进程分开(从本质上讲子进程是由父进程复制出来的产物,只是父进程调用了getpid得到了自己的进程ID号,子进程重复运行调用getpid得到属于子进程的ID号,由于进程ID号是唯一的,所以父进程与子进程的ID号并不相同,由此我们便通过if语句,使得两个进程走向不同的方向)。
2.2根据代码研究fork函数返回值
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
pid_t pid2;
pid_t retpid;//做一个变量来承接fork函数返回值
pid = getpid();
printf("before fork: pid = %d \n",pid);
retpid = fork();
pid2 = getpid();//接收
printf("after fork: pid = %d \n",pid2);
if(pid == pid2){
printf("this id father print , retpid = %d ,pid = %d\n",retpid,getpid());
}
else{
printf("this id child print ,retpid = %d , pid = %d \n",retpid,getpid());
}
return 0;
}
**通过代码结果验证了开头说的结论:**在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。
fork函数相当于拷贝了两次,一次给父进程,一次给子进程
注:fork创建了子进程,这时的子进程与父进程共享内存空间,只有当子进程的值发生改变时,与父进程不一样了,才会给子进程数据分配新空间。
3.进程实际运用场景
有一台服务器,每次客户端来介入,就需要服务器来处理这个客户端,谁来处理?fork函数创建的子进程,每一次客户端介入我就创建一个子进程来处理数据。
示例代码:
//我们用1来模拟客户端接入,进而处理一些我们想要的数据
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
int data = 10;
while(1){
printf("please input your data\n");
scanf("%d",&data);
if(data == 1)
{
pid = fork();
if(pid>0){
}else if(pid == 0){
while(1){//不断的跟客户端去进行对接服务
printf("do net request,pid=%d\n",getpid());
sleep(3);
}
}
}else{
printf("do nothing!\n");
}
}
return 0;
}
4.vfork函数创建进程
-
关键区别一:
vfork直接使用父进程的存储空间,不拷贝。
-
关键区别二:
vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行
vfork | fork |
---|---|
子进程与父进程共享数据段. | 子进程拷贝父进程的数据段,代码段 |
保证子进程先运行 | 父子进程的执行次序不确定 |
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid > 0){
while(1){
printf("this is father print: pid = %d \n",getpid());
printf("cnt = %d\n",cnt);
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print: pid = %d \n",getpid());
cnt++;
if(cnt == 3){
exit(0);
//break; 这个属于异常退出了
}
sleep(1);
}
}
return 0;
}
运行结果:
- 由vfork创造出来的子进程还会导致父进程挂起,除非子进程exit或者execve才会唤起父进程
- 由vfok创建出来的子进程共享了父进程的所有内存,包括栈地址,直至子进程使用execve启动新的应用程序为止
从运行结果可以看到vfork创建出的子进程(线程)共享了父进程的cnt变量,二者的cnt指向了同一个内存,所以子进程修改了cnt变量,父进程的 cnt变量同样受到了影响。
冷知识:为什么会有vfork?
因为以前的fork当它创建一个子进程时,将会创建一个新的地址空间,并且拷贝父进程的资源,而往往在子进程中会执行exec调用,这样,前面的拷贝工作就是白费力气了,这种情况下,聪明的人就想出了vfork,它产生的子进程刚开始暂时与父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中运行,所以子进程不能进行写操作,
并且在儿子“霸占”着老子的房子时候,要委屈老子一下了,让他在外面歇着(阻塞),一旦儿子执行了exec或者exit后,相当于儿子买了自己的房子了,这时候就相当于分家了。此时vfork保证子进程先运行,在她调用exec或exit之后父进程才可能被调度运行。
因此vfork设计用以子进程创建后立即执行execve系统调用加载新程序的情形。在子进程退出或开始新程序之前,内核保证了父进程处于阻塞状态
用vfork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序,当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行,因为调用exec并不创建新进程,所以前后的进程id 并未改变,exec只是用另一个新程序替换了当前进程的正文,数据,堆和栈段。
5.进程退出
正常退出:
- Main函数调用return
- 进程调用exit(),标准c库
- 进程调用 _ exit()或者_ Exit(),属于系统调用
补充:1.进程最后一个线程返回 2.最后一个线程调用pthread_exit
异常退出:
- 调用abort
- 当进程收到某些信号时,如ctrl+C
- 最后一个线程对取消(cancellation)请求做出响应
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
对上述任意一种终止情形、我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit、 _ exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination sratus)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态
6.父进程等待子进程退出
还是上面的代码,子进程退出之后,父进程并没有收集子进程exit(0)返回来的数据。
僵尸进程
1.子进程退出状态不被收集,变成僵死进程(僵尸进程zombie)
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid > 0){
while(1){
printf("this is father print: pid = %d \n",getpid());
printf("cnt = %d\n",cnt);
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print: pid = %d \n",getpid());
cnt++;
if(cnt == 3){
exit(0);
}
sleep(1);
}
}
return 0;
}
wait函数
2.子进程退出状态被父进程收集,调用wait
我们为什么要用wait函数?
我们应当知道的是,在用fork创建子进程后,父子进程的执行的先后顺序是不定的,这时,我们可以用wait函数,wait()会暂停当前进程的执行,直到有信号到来或者子进程结束。总的来说,wait()的作用就是阻塞父进程,等待子进程。
-
我们是在父进程中使用wait(),可以不让父进程先于其产生的子进程结束,因为如果父进程结束了,而子进程还没有结束,那这个进程就会变成一个“孤儿进程”,(后面会讲)他会被init进程收养(进程号为1),并由init进程对它们完成状态收集工作。
-
当然如果子进程结束了,但是父进程没有调用wait或者waitpid(),那么子进程的资源就无法得到收集释放,这时候的子进程被称为“僵尸进程”,会占用系通资源,是非常不好的,危害很大。
总结:wait用于等待子进程结束,回收并释放子进程资源
wait函数
函数原型:pid_t wait(int *status)
头文件://不用加进代码里,已经被下面其他头文件包含了
#include <sys/types.h>
#include <sys/wait.h>
函数参数:
1.status作为一个整形值,用来保存子进程退出时的状态;//exit(3)
2.如果status为NULL,表示忽略子进程退出时的状态;
3.如果status不为空,wait函数会将子进程退出的状态存入status中,
另外,子进程退出时的状态可以通过linux中的特定的宏(macro)来进一步测定退出状态。
返回值:
成功,返回子进程的进程号;
失败,返回-1
在等待的过程中:
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回
wait函数status参数为空
//wait函数status参数为空
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = vfork();
if(pid > 0){
wait(NULL);//唯一改动的地方,在子进程运行完之前,父进程会一直在这里等待子进程
while(1){
printf("this is father print: pid = %d \n",getpid());
printf("cnt = %d\n",cnt);
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print: pid = %d \n",getpid());
cnt++;
if(cnt == 3){
exit(0);
}
sleep(1);
}
}
return 0;
}
运行结果:
wait函数status参数非空
当wait函数status参数非空,并且我们要查看返回的是哪一个子进程的终止状态的状态码的时候,我们就需要下面这个表来检查wait和waitpid所返回的终止状态的宏,来解析状态码。
宏 | 说明 |
---|---|
WIFEXITED(status) | 若为正常终止子进程返回的状态,则为真。对于这种情况可执行WEXITSTATUS(status),取子进程传送给exit、_exit或_Exit参数的低8位 |
WIFSIGNALED(status) | 若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况,可执行WTERMSIG(status),取使子进程终止的信号编号。另外,有些实现定义宏WCOREDUMP(status),若已产生终止进程的core文件,则它返回真 |
WIFSTOPPED(status) | 若为当前暂停子进程的返回状态,则为真。对于这种情况,可执行WSTOPSIG(status),取使子进程暂停的信号编号 |
WIFCONTINUED(status) | 若在作业控制暂停后已经继续的子进程返回了状态,则为真。(POSIX.1的XSI扩展;仅用于waitpid。) |
下面代码调用的是第一个宏 WEXITSTATUS(status) , 正常终止子进程返回的状态,为真的情况
//wait函数status参数非空
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;//提前定义出来
pid = vfork();
if(pid > 0){
wait(&status);//函数参数是int型指针
printf("child quit,child status = %d\n",WEXITSTATUS(status));//会打印出3,与exit有关
while(1){
printf("this is father print: pid = %d \n",getpid());
printf("cnt = %d\n",cnt);
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print: pid = %d \n",getpid());
cnt++;
if(cnt == 3){
exit(3);//值可以为0 1 2 3......
}
sleep(1);
}
}
return 0;
}
运行结果:
waitpid函数
补充:waitpid函数
**函数原型:**pid_t waitpid(pid_t pid ,int *status , int options)
1.参数pid:
输入的pid | 作用 |
---|---|
pid > 0 | 等待其进程ID与pid相等的子进程。 |
pid = 0 | 等待其组ID等于调用进程组ID的任一子进程。 |
pid = -1 | 等待任一子进程。就这一方面而言,waitpid与wait等效。 |
pid < -1 | 等待其组ID等于pid绝对值的任一子进程。 |
2.参数options:
输入的options | 作用 |
---|---|
WNOHANG | 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0 |
WUNTRACED | 若某实现支持作业控制,而由pid指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态。WIFSTOPPED宏确定返回值是否对应于一个暂停子进程 |
WCONTINUED | 若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态(POSIX.1的XSI扩展) |
//这里我们用第一个options参数
//wait 使调用者阻塞,waitpid第一个选项,可以使调用者不阻塞。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0)
//wait(&status);
waitpid(pid,&status,WNOHANG);
printf("child quit,child status = %d\n",WEXITSTATUS(status));
while(1){
printf("cnt = %d\n",cnt);
printf("this is father print: pid = %d \n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is child print: pid = %d \n",getpid());
cnt++;
if(cnt == 3){
exit(3);
}
sleep(1);
}
}
return 0;
}
运行结果:
我们看到父子进程都在同时跑,但子进程已经沦为僵尸进程了
孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时的子进程叫做孤儿进程
Linux避免系统存在过多的孤儿进程,init进程(也就是干爹)(系统的一个初始化进程,它的pid号为1)收留孤儿进程,变成孤儿进程的父进程。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0){
printf("this is father print: pid = %d \n",getpid());//执行完,父进程死掉
}
else if(pid == 0){
while(1){
//getppid(),获取父进程的pid,了解一下
printf("this is child print: pid = %d,my father pid = %d \n",getpid(),getppid());
cnt++;
if(cnt == 5){
exit(3);
}
sleep(1);
}
}
return 0;
}
运行结果:
7.exec族函数
date 获取系统时间 whereis ls 查找ls指令在那个路径下
1.exec族函数函数的作用:
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
exec族函数调用完不会往下走
2.功能:
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
3.函数族:
exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe
尾巴带e的两个函数不常用,入门不推荐,了解即可
4.函数介绍:
//1.头文件:
#include <unistd.h>
extern char **environ;
//2.函数原型:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
//3.参数说明:
path:可执行文件的路径名字(就是路径)
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
//4.返回值:
exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l | 使用参数列表 |
---|---|
p | 使用文件名,并从PATH环境进行寻找可执行文件 |
v | 应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。 |
e | 多了envp[]数组,使用新的环境变量代替调用进程的环境变量 |
一、带l的一类exac函数(l表示list),包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。
execl函数:
//文件execl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
int main(void)
{
printf("before execl\n");
if(execl("./echoarg","echoarg","abc",NULL) == -1)
{
printf("execl failed!\n");
}
printf("after execl\n");
return 0;
}
//文件echoarg.c
//编译该文件,gcc echoarg.c -o echoarg 生成一个名为echoarg的可执行文件
#include <stdio.h>
int main(int argc,char *argv[])
{
int i = 0;
for(i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}
return 0;
}
运行结果:
实验说明:
我们先用gcc编译echoarg.c,生成可执行文件echoarg并放在当前路径bin目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。
加餐:
在上述运行结果的第三步中,如果找不到这个程序,会返回-1,但是我们并不知道错误的原因是什么?如何知道?
//文件execl2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("before execl\n");
if(execl("./echoarg","echoarg","abc",NULL) == -1)
{
printf("execl failed!\n");
perror("why");//用这个perror函数解析出来,这里不细讲了
}
printf("after execl\n");
return 0;
}
运行结果:
因为我们并没有在./echoarg这个路径中生成echoarg执行程序文件,所以报错了
再加一餐:
Linux指令—ls,也可以说是一个可执行程序(打印出来当前文件下的文件)如何通过代码来调用ls?
首先根据execl函数参数可知,第一个参数为路径,我们就需要知道ls在那个根目录下,如何查找?whereis ls
//文件execl3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("before execl\n");
// ./bin中的点是当前路径下,所以不能加点
if(execl("/bin/ls","ls",NULL,NULL) == -1)//第三个参数暂时先为空,我们看一下运行结果
{
printf("execl failed!\n");
perror("why");//用这个perror函数解析出来,这里不细讲了
}
printf("after execl\n");
return 0;
}
运行结果:
拓展:
指令ls -l 显示长列表文件
-l如何输入?用execl函数的第三个参数
//文件execl4.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("before execl\n");
// ./bin中的点是当前路径下,所以不能加点
if(execl("/bin/ls","ls","-l",NULL) == -1)//第三个参数暂时先为空,我们看一下运行结果
{
printf("execl failed!\n");
perror("why");//用这个perror函数解析出来,这里不细讲了
}
printf("after execl\n");
return 0;
}
运行结果:与ls -l同效
指令:date显示系统当前时间
execlp函数:
在第一个函数传参过程中,不需要再加绝对路径了,这个函数自己会在**PATH环境变量**中寻找可执行文件
#include<stdio.h>
#include<unistd.h>
int main(void)
{
printf("before execlp\n");
if(execlp("ps","ps",NULL,NULL)==-1)//唯一的变化就是不需要路径了
{
printf("execl failed\n");
perror("why");
}
printf("afther execlp\n");
return 0;
}
execvp函数:
这个相当于啥呢,就是在execlp函数的基础上,把二三四参数搞到了一个字符型指针数组里面了
#include<stdio.h>
#include<unistd.h>
int main(void)
{
printf("before execlvp\n")
char *argv[]={"ps",NULL,NULL};
if(execvp("ps",argv)==-1)
{
printf("execlvp failed\n");
perror("why");
}
printf("afther execlvp\n");
return 0;
}
8.linux 下修改环境变量配置绝对路径
当前环境变量PATH查看:echo $PATH (系统可以找到这个路径底下的可执行程序)
我们在编译程序时往往需要 ./a.out 来运行这个程序,“ ./ ”表明是在当前路径下,而你有木有好奇为什么像 ls cd cp 这些Linux指令,同样是可执行程序,为什么他们就不是 “ ./ls ”?原因就是这些指令全部在PATH环境变量里?
那么只要我们能把程序路径也搞到环境变量里面去,以后就再也不用“ ./a.out ”了,说搞就搞
第一步:
Linux命令:pwd 查看当前路径是多少
第二步:
Linux命令:export PATH=$PATH:所要加的路径
9.exec族函数配合fork函数使用
实现功能:当父进程检测到输入为1的时候,创建子进程把配置文件的字段值修改掉。
文件:
//文件名:config.txt
SPEED=5
LENG=1
SCORE=90
LEVEL=95
1.修改配置文件的程序:
详细介绍请看Linux系统编程中的文件编程
//文件名 a.c 编译后执行文件 a
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
void findFun(char *str1,char *str2)
{
char *p=strstr(str1,str2);
if(p==NULL)
{
printf("no found\n");
exit(-1);
}
p=p+strlen(str2);//指针偏移到需要改动的地方
*p='5';//注意是字符5
}
int main(int argc,char **argv)
{
int fdSrc;
int size;
char *readBuf=NULL;
if(argc!=2)
{
printf("the params no ok\n");
exit(-1);
}
fdSrc=open(argv[1],O_RDWR);
size=lseek(fdSrc,0,SEEK_END);//计算出配置文件的大小
lseek(fdSrc,0,SEEK_SET);//把光标移到头,为下面读操作做铺垫
readBuf=(char*)malloc(sizeof(char)*size+1);
read(fdSrc,readBuf,size);
findFun(readBuf,"LENG=");
lseek(fdSrc,0,SEEK_SET); //移到头,把原先信息覆盖掉
/******************方法二:****************
close(fdSrc);
open("./file1",O_RDWR|O_TRUNC);
*****************************************/
write(fdSrc,readBuf,strlen(readBuf));
close(fdSrc);
return 0;
}
2.fork函数配合exec调用a.c程序:
//b.c
//我们用1来模拟客户端接入,进而处理一些我们想要的数据
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
int data = 10;
while(1){
printf("please input your data\n");
scanf("%d",&data);
if(data == 1)
{
pid = fork();
if(pid>0){
wait(NULL);
}else if(pid == 0){
//if(execl("./a.c","a","config.txt",NULL) == -1)不要加后缀名.c
if(execl("./a","a","config.txt",NULL) == -1)
{
printf("execl failed!\n");
}else{
printf("execl sucessful!\n");//注意这个点,execl调用成功并不会执行到这里
}
}
}else{
printf("do nothing!\n");
}
}
return 0;
}
运行结果:
我们看到execl成功被调用后并不会再向后运行(小本本记重点,后面要与system函数做区别)
10.system函数
//1.头文件
#include<stdlib.h>
//2.函数原型
int system(const char * string);
函数说明
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行
完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
//3.返回值
1.成功,则返回进程的状态值;
2.当sh不能执行时,返回127;
3.失败返回-1;
如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值。
如果 system()调用成功则最后会返回执行shell命令后的返回值,但是此返回值也有可能为system()调用/bin/sh失败
所返回的127,因此最好能再检查errno 来确认执行成功。
附加说明:
在编写具有SUID/SGID权限的程序时请勿使用system(),system()会继承环境变量,通过环境变量可能会造成系统安全的问题。
当system接受的命令为NULL时直接返回,否则fork出一个子进程,因为fork在两个进程:父进程和子进程中都返回,这里要检查返回的 pid,fork在子进程中返回0,在父进程中返回子进程的pid,父进程使用waitpid等待子进程结束,子进程则是调用execl来启动一个程序代替自己,execl(“/bin/sh”, “sh”, “-c”, cmdstring,(char*)0)是调用shell,这个shell的路径是/bin/sh,后面的字符串都是参数,然后子进程就变成了一个 shell进程,这个shell的参数是cmdstring,就是system接受的参数。在windows中的shell是command,想必大家很熟悉shell接受命令之后做的事了。
如果上面的你没有看懂,那我再解释下fork的原理:当一个进程A调用fork时,系统内核创建一个新的进程B,并将A的内存映像复制到B的进程空间中,因为A和B是一样的,那么他们怎么知道自己是父进程还是子进程呢,看fork的返回值就知道,上面也说了fork在子进程中返回0,在父进程中返回子进程的pid。(上述篇章皆有讲解)
system函数源码:
//说白了,sysytem函数就是将execl函数给封装了起来
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL){
return (1);
}
if((pid = fork())<0){
status = -1;
}
else if(pid == 0){
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);//(char *)0 就是NULL空指针的意思
-exit(127); //子进程正常执行则不会执行此语句
}
else{
while(waitpid(pid, &status, 0) < 0){
if(errno != EINTER){
status = -1;
break;
}
}
}
return status;
}
这两个指令其实是等效的,只不过习惯于把sh -c ls省略了
下面简单做一个程序demo:
//把系统时间打印出来
#include <stdio.h>
#include <unistd.h>
int main(int argc ,char **argv){
if(system("date") == -1){
printf("execl filed!\n");
perror("becasue");
}
printf("system successful\n");
return 0;
}
运行结果:我们发现system在调用完之后,还会继续再向下运行,这就是与execl函数相区别的地方(一般我们更倾向于用system)
system( ) 函数的参数书写的规律是,可执行文件怎么执行,就怎么写:比如 system(“./a.out aa bb”);
11.popen函数
提到system函数,就不得不提到popen函数,根据system函数的源代码:system函数的执行需要通过调用fork()函数创建一个子进程,子进程通过execl函数调用shell对传参的可执行文件进行实现。这也意味着system函数实现需要依赖execl函数实现自身功能。因此system函数的结果将直接显示在终端上,这样原本运行的结果就无法保存在文件中用于实现信息交互等功能。
//1.头文件
#include <stdio.h>
//2.函数原型
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
参数说明:
commmand:是一个指向以 NULL 结束的 shell 命令字符串的指针。
这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。
type:只能是读和写的一种,如果是 “r” 则文件指针连接到command的标准输出,
则返回的文件指针是可读的;如果是 “w” 则文件指针连接到command的标准输入,则返回的文件指针是可写的。
stream:popen返回的文件指针。
//3.返回值
如果调用fork()或pipe()失败,或者不能分配内存将返回NULL,否则返回标准I/O流。
popen()没有为内存分配失败设置errno值。如果调用fork()或pipe()时出现错误,errno被设为相应的错误类型。
如果type参数不合法,errno将返回EINVAL。
popen函数比system在实际应用中的好处:可以获得运行的输出结果。
**作用:**创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据。
**原理:**创建一个管道,fork一个子进程,关闭未使用的管道端(读端或者写端),执行一个shell运行命令,然后等待命令终止。
示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//FILE *popen(const char *command, const char *type);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
int main(void)
{
FILE *fp;
char ret[1024]={0};
fp = popen("ps","r");
int nread = fread(ret,1,1024,fp);
printf("read ret %d byte,ret = %s \n",nread,ret);
return 0;
}
运行结果:
如果没有上述打印的内容,实验结果将毫无现象。因此可以通过popen函数读入信息,通过网络的方式发送从而实现多机通信。
pclose函数则用于关闭标准I/O流。如果使用popen函数在 “w” 模式下,则可以继续往标准I/O流中写入内容,直到调用pclose关闭它。