linux系统编程之进程

        本篇文章我们来聊一聊linux编程中进程相关的内容,在我们正式开始之前来看看几个进程相关的问题:

1、什么是程序,什么是进程

答:程序是静态的概念,比如我们在编译.c的文件时-gcc xxx.c -o pro,其中pro就是程序,pro文件是在磁盘中生成;进程是程序的一次运行活动,每运行一次程序系统中就多了一个进程。

2、如何查看系统有哪些程序

答:ps -aux 查看所有进程;ps -aux|grep xxx 查看与xxx相关的进程;top 类似任务管理器。

3、什么是进程标识符

答:每一个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证。其中pid=0的进程称为交换进程,作用:进程调度;pid=1的进程称为init进程,作用:系统初始化。(调用getpid函数获取自身的进程标识符,getppid函数获取父进程的进程标识符)

4、什么是父进程,什么是子进程

答:如果进程A创建了B进程,那么A就是父进程,B就是子进程。

5、C程序的存储空间是如何分配

答:典型的存储器由正文段、初始化数据段、非初始化数据段(bss段)、栈和堆组成,一般来说,代码中main函数里面的一下语句都是属于正文里的,一切初始化了的变量属于初始化数据段,未初始化的变量属于非初始化数据段(bss段)。

一、调用fork函数创建一个进程

函数原型:pid_t fork(void),包含头文件 #include <unistd.h>;

调用成功,即成功创建了一个子进程,返回两次:

返回值为0:代表当前进程是子进程;

返回值为非负数:代表当前进程为父进程;

调用失败,返回-1;

既然说到了进程那当然少不了进程标识符,上文有提到过,我们可以通过getpid函数来获取当前进程的pid:

函数原型:pid_t getpid(void),包含头文件#include <sys/types.h>、#include <unistd.h>;

调用后返回当前进程的pid;

对于初学linux进程的小伙伴来说上段代码的运行结果看起来应该比较难理解,我们可以看到fork函数后面的代码执行了两遍,这是因为fork函数创建了一个子进程,子进程又把fork函数后面的代码执行了一遍,而且从上面代码可以得知父进程的pid为44313,子进程的pid为44314。 

我们把if...else那块代码修改一下,用fork函数的返回值来判断:

这段代码就验证了fork返回非负数代码该进程为父进程,否则为子进程,同时也说明了fork函数创建子进程时,把fork函数前面的变量也拷贝了一份,即代码中的data在父进程中修改了,但是在子进程中仍不变。 

        那么fork函数在实际开发过程中到底有什么用呢?我们来看看下面这段代码:

        这段代码中我们主函数一直在检测用户键盘输入的指令,当用户输入1时即表示需要做某事,这时我们就可以通过创建一个子进程来完成该事,同时还可以在检测用户输入。

        既然讲到了fork函数,那必然少不了vfork函数:

函数原型:pid_t vfork(void),包含头文件#include <sys/types.h>、#include <unistd.h>;

大部分和fork函数一样,主要要注意以下两点不同:

1.vfork直接使用父进程存储空间,不拷贝,fork会将父进程进行全拷贝;

2.vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行;

从结果上我们可以看到确实是子进程先执行,当子进程退出后父进程才开始执行。 

二、等待子进程退出 

        我们先弄清楚为什么要等待子进程退出,就比如我们上段代码中父进程就没用等待其子进程退出,直观上来看父进程是等待了子进程了的,毕竟确实是子进程先执行,但是父进程并没有收集子进程的退出状态,如果子进程的退出状态不被收集,其就会变成僵死进程(僵尸进程)。 

        等待子进程需要用到wait函数或者waitpid函数:

函数原型:pid_t wait(int *status);pid_t waitpid(pid_t pid, int *status, int options);

其中status是一个整型数指针,非空时表示子进程退出状态放在它所指向的地址中;

包含头文件:#include <sys/types.h>、#include <sys/wait.h>

两者区别之一:wait函数会阻塞父进程,waitpid函数有个选项可以选择不阻塞;

一般使用WEXITSTATUS(status)来解析status;

这里我们使用的wait函数,接下来我们来看看waitpid函数:

这里我们调用waitpid函数,并且选择不阻塞选项即WNOHANG,可以发现父进程和子进程同时进行,这里要注意的是此时子进程仍然是僵尸进程。至于为什么cnt=0,就是我们前面所讲的fork函数会拷贝整个代码,并且子进程和父进程不关联。 

        这里再补充一个概念:孤儿进程——父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫孤儿进程。linux避免系统中存在过多的孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。

可以看到父进程打印了一次自己的pid后就结束了,子进程在父进程还在时它的父进程pid就是真正父进程的pid,父进程结束后子进程打印的父进程pid就是init进程的pid了,即为1。 

三、exec族函数

        exec族函数的作用:我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另一个程序,当进程调用exec函数时,该进程被完全替换为新进程,因为调用exec函数并不会创建新进程,所以前后进程的ID并没有改变。

函数原型:

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[]);

包含头文件#include <unistd.h>;

调用成功后不会返回,调用失败时,会设置errno并返回-1,注意:如果有设置errno我们在程序中就可以通过调用perror来打印出错信息;

这里还有一点要注意:每个exec族函数最后一个参数必须时NULL;

        我们先来创建一个.c文件,其作用就是将其参数打印出来:        

注意这里我们将可执行程序命名为了encoarg,后面会用到。 

        接下来我们通过调用exec族函数在另一个程序中执行该程序(encoarg):

可以发现程序运行的结果是一样的。

        再举个例子,我们查看系统时间的指令是date,我们怎么通过在程序中实现呢:

可以看到程序运行的结果并不是我们所想看到的,并且通过perror得知失败的原因是该目录下没有该文件。所以我们要先找到date指令的根目录,通过whereis date指令: 

可以发现其在bin目录下,所以我们在代码上做修改:

把其目录修改后执行的结果就是我们想看到的。

        上面我们用到的是exec族函数中的execl函数,接下来我们将一下execlp函数,它的功能比execl要强大一点:

我们看到在用execlp函数时date指令不需要其根目录,最后运行结果也是正确的,这是因为execlp函数会从PATH环境变量中寻找该可执行文件。 

        接下来我们来讲讲execvp函数:

可以到在execvp函数中用了一个字符型的指针数组来存放指令,最后把argc传进去,结果依然是对的,那么对应我们要讲的最后一个函数execv其实也没啥好讲的了,对比execvp函数,就是需要手动添加它的根目录。​​​​​​​ 

        对于上文讲的PATH环境变量,这里做一些小补充:在某一个目录下有一个可执行文件,如果我们到其他目录下,该可执行文件还能不能执行呢?一般来说是不能的,但是如果我们通过修改其环境变量就可以实现。首先我们可以通过echo $PATH指令来查看当前环境变量,再通过export PATH=$PATH:xxx指令来修改环境变量

可以看到不管在哪个路径中原路径下的可执行文件到其他路径下都可以执行。

        接下来我们让fork函数配合exec族函数来使用,也是正常开发中需要使用到的。fork函数我们用之前的代码主函数一直检测用户输入,检测到用户要求时创建子进程来执行exec族函数,exec族函数用来执行修改文件内容的程序,这个程序在我上一篇博文—linux系统编程之文件编程中讲过。

这样我们就通过在该程序中执行另一个程序了。

四、system函数

        system函数本质上就是execl函数,大家如果看过其源码就知道system函数就是将execl函数进行了包装。

函数原型:int system(const char *command);

包含头文件#include <stdlib.h>;

成功则返回进程的状态值,当sh不能执行时返回127,失败则返回-1

把前面的代码用上述代码替换,运行后的结果是一样的。

五、popen函数 

        popen函数和前面所讲的system函数等作用一样,它的好处就是可以获取运行的输出结果。就比如我们之前执行的指令会显示在命令终端上,但是如果我们用popen就可以获取这些结果。

函数原型:FILE *popen(const char *command, const char *type);

包含头文件#include <stdio.h>

其中type可以选"r"或"w";(一般选"r");

以上就是我对于linux中进程相关概念的理解,希望对大家有所帮助,共勉! 

 

       

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

STRIVE1151

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值