一、进程关键概念(简单了解)
1、什么是程序,什么是进程,有什么区别?
程序是静态的概念,
gcc xxx.c –o pro
;磁盘中生成pro文件,叫做程序。
进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程。
2、如何查看系统中有哪些进程?
a.使用
ps
指令查看
实际工作中,配合grep来查找程序中是否存在某一个进程,如:ps -aux|grep pro
b.使用top
指令查看
类似windows任务管理器
3、什么是进程标识符?
每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证
pid=0:称为交换进程(swapper)
作用—进程调度
pid=1:init进程
作用—系统初始化
编程调用
getpid();
函数获取自身的进程标识符;getppid();
获取父进程的进程标识符
4、什么叫父进程,什么叫子进程?
进程A创建了进程B
那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系
5、C程序的存储空间是如何分配?
补充说明强调:
栈:由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。
堆:存放动态分配的数据,一般由程序员动态分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
共享库的内存映射区域:这是 Linux动态链接器和其他共享库代码的映射区域。
二、fork创建子进程
1、函数fork
函数原型:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回值:
若成功:子进程返回0,父进程返回子进程ID(非负数)
若失败:返回-1
即,返回0,代表当前进程是子进程;返回非负数,代表当前进程是父进程;返回-1,代表调用失败。
示例1
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
pid_t pid2;
pid_t retpid;//fork返回值
pid = getpid();
printf("before fork: pid = %d\n",pid);
//pid_t fork(void);
retpid = fork();
pid2 = getpid();
printf("after fork: pid = %d\n",pid2);
if(pid == pid2){
printf("this is partent process,retpid = %d,partent pid = %d\n",retpid,getpid());//retpid打印子进程的ID,父进程返回子进程的ID
}else{
printf("this is child process,retpid = %d,child pid = %d\n",retpid,getpid());
}//子进程返回0
return 0;
}
结果:
fork的两种用法(创建一个子进程的目的):
示例2:fork实战
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int data;
while(1){
printf("please input a 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("wait,do nothing!\n");
}
}
return 0;
}
结果:
2、fork函数创建的子进程与父进程的内存关系
示例3
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int data = 10;
printf("partent pid = %d\n",getpid());
//pid_t fork(void);
pid = fork();
if(pid > 0){
printf("this is partent process,partent pid = %d\n",getpid());
data = data + 5;
}else if(pid == 0){
printf("this is child process,child pid = %d\n",getpid());
data = data + 100;
}
printf("data = %d\n",data);
return 0;
}
结果:
父进程跟子进程里的data值是不叠加的,而是各自加各自的,所以我们可以知道fork方式创建的子进程是不跟父进程共用存储空间的。
三、vfork、exit、wait和waitpid
1、函数vfork(函数fork也创建进程,与fork的区别)
关键区别一:
vfork
直接使用父进程存储空间,不拷贝。
关键区别二:
vfork
保证子进程先运行,当子进程调用exit退出后,父进程才执行。
fork
父子进程无规律可循,交替随机运行
示例1:vfork使用
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
//pid_t vfork(void);
pid = vfork();
if(pid > 0){
while(1){
printf("this is partent process,partent pid = %d\n",getpid());
sleep(1);
}
}else if(pid == 0){
while(1){
printf("this is child process,child pid = %d\n",getpid());
sleep(1);
}
}
return 0;
}
结果:
子进程未调用exit退出,所以只有子进程运行,父进程不执行:
2、函数exit(进程退出)
函数原型(三种都可以):
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
正常退出:
1.main函数调用return
2.进程调用exit()
,标准c库
3.进程调用_exit()
或者_Exit()
,属于系统调用
4.进程最后一个线程返回
5.最后一个线程调用pthread_exit
异常退出:
1.调用abort
2.当进程收到某些信号时,如Ctrl+C
3.最后一个线程对“取消”(cancellation)请求做出响应
示例2:exit使用
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int cnt = 0;
pid = fork();
if(pid > 0){
while(1){
printf("this is partent process,partent pid = %d\n",getpid());
sleep(1);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0){
while(1){
printf("this is child process,child pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(0);//_exit(0);和_Exit(0);和exit(0);一样用
}
}
}
return 0;
}
结果:
fork创建子进程,父子进程都运行3次,子进程exit退出,父进程未调用wait等待子进程退出,所以子进程变成僵尸进程(后面有介绍):
3、函数wait和waitpid(等待子进程退出)
(1)函数wait和waitpid介绍
介绍两个字母符号:
僵尸进程:Z+
正常进程:即有父进程等待的进程,S+
函数原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
若成功,返回进程ID
若失败,返回0或-1
参数:
status参数(是一个整型数指针):
非空指针:子进程退出状态放在它所指向的地址中
空指针:不关心退出状态
示例3:wait使用(不关心子进程退出状态)
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int cnt = 0;
pid = fork();
if(pid > 0){
wait(NULL);
while(1){
printf("this is partent process,partent pid = %d\n",getpid());
sleep(1);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0){
while(1){
printf("this is child process,child pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(0);//_exit(0);和_Exit(0);和exit(0);一样用
}
}
}
return 0;
}
fork创建子进程,因为父进程调用wait(不关心子进程的退出状态),所以父进程阻塞,子进程先运行3次后调用exit退出,这时父进程为正常进程,正常运行:
------详细介绍------
1、wait:
示例4:wait使用(关心子进程退出状态)
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0){
wait(&status);
printf("child process quit,child status = %d\n",WEXITSTATUS(status));
while(1){
printf("this is partent process,partent pid = %d\n",getpid());
sleep(1);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0){
while(1){
printf("this is child process,child pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(3);
}
}
}
return 0;
}
结果:
fork创建子进程,因为父进程调用wait(关心子进程的退出状态,利用参数status获取子进程的退出状态),所以父进程阻塞,子进程先运行3次后调用exit退出,退出后返回子进程状态,这时父进程为正常进程,正常运行:
2、waitpid:
示例5:waitpid使用(非阻塞版本)
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0){
//wait(&status);
waitpid(pid,&status,WNOHANG);
printf("child process quit,child status = %d\n",WEXITSTATUS(status));
while(1){
printf("cnt = %d\n",cnt);
printf("this is partent process,partent pid = %d\n",getpid());
sleep(1);
}
}else if(pid == 0){
while(1){
printf("this is child process,child pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(3);
}
}
}
return 0;
}
结果:
fork创建子进程,因为父进程调用waitpid(非阻塞版本),所以父子进程先随机运行3次,子进程运行3次后调用exit退出,因为waitpid不阻塞,所以退出后返回值为0,这时父进程为正常进程,正常运行,子进程为僵尸进程:
vfork创建子进程,只有子进程运行,运行3次后调用exit退出,父进程未调用wait等待子进程退出,子进程exit后,变成僵尸进程:
这里的阻塞就是相当于vfork方式创建子进程,父进程处于阻塞状态,让子进程先执行,当父进程收到子进程的退出信号的时候,父进程开始执行。而waitpid不阻塞的机制就有点像fork方式创建的子进程一样,父子进程交替执行,所以这就搞的以waitpid的方式等待有点多余了,所以等待子进程退出还是使用wait函数多,但也有场景需要用waitpid的,所以只是waitpid用的少。
(2)僵尸进程
为什么要等待子进程退出?
父进程等待子进程的退出并收集子进程的退出状态,子进程的退出状态不被收集,子进程就会变成僵死进程(僵尸进程)
示例6
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int cnt = 0;
//pid_t vfork(void);
pid = vfork();
if(pid > 0){
while(1){
printf("this is partent process,partent pid = %d\n",getpid());
sleep(1);
printf("cnt = %d\n",cnt);
}
}else if(pid == 0){
while(1){
printf("this is child process,child pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(0);//_exit(0);和_Exit(0);和exit(0);一样用
}
}
}
return 0;
}
结果:
vfork创建子进程,只有子进程运行,运行3次后调用exit退出,父进程未调用wait等待子进程退出,子进程exit后,变成僵尸进程:
(3)孤儿进程
孤儿进程:父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时的子进程就叫做孤儿进程。
Linux避免系统存在过多的孤儿进程,init进程(进程ID号为1)收留孤儿进程,变成孤儿进程的父进程。
示例7
代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0){
printf("this is partent process,partent pid = %d\n",getpid());
}else if(pid == 0){
while(1){
printf("this is child process,child pid = %d,my partent pid = %d\n",getpid(),getppid());
sleep(1);
cnt++;
if(cnt == 3){
exit(3);
}
}
}
return 0;
}
结果:
注意:
pid不是1的原因:
平时在图形界面打开的terminal是伪终端,需要切换界面,命令如下:
图形界面切换到字符型界面:Crtl+Alt+F3/Ctrl+Fn+Alt+F3
字符型界面切换到图形界面:Ctrl+Alt+F2/Ctrl+Fn+Alt+F2
再运行程序,父进程pid变为1成功。
------书本介绍:僵尸和孤儿进程------
四、exec族
作用:
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
功能:
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
函数原型:
#include <unistd.h>
extern char **environ;
int execl(const char *pathname, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
参数介绍:
pathname:可执行文件的路径名字
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量
返回值:
exec函数族的函数执行
若成功,不会返回
若调用失败,会设置errno并返回-1
然后从原程序的调用点接着往下执行。
1、带l的一类exec函数(l表示list)
包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾。
示例1:execl
代码:
//文件demo18_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
#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并放在当前路径./目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。
2、带p的一类exec函数
包括execlp、execvp、execvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。举个例子,PATH=/bin:/usr/bin
示例2:execlp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("this is pro ps:\n");
printf("before execlp\n");
//int execlp(const char *file, const char *arg, .../* (char *) NULL */);
if(execlp("ps","ps",NULL) == -1)
{
printf("execlp failed!\n");
perror("error");
}
printf("after execlp\n");
return 0;
}
结果:
如果用execl执行同样代码:
从上面的实验结果可以看出,上面的exaclp函数带p,所以能通过环境变量PATH查找到可执行文件ps。
补充:C 库函数 -
perror()
C 标准库 - <stdio.h>
描述:
C 库函数void perror(const char *str)
把一个描述性错误消息输出到标准错误stderr
。首先输出字符串str,后跟一个冒号,然后是一个空格。
声明:
下面是perror() 函数的声明。
void perror(const char *str)
参数:
str – 这是 C字符串,包含了一个自定义消息,将显示在原本的错误消息之前。 返回值: 该函数不返回任何值。
3、带v不带l的一类exec函数
包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
如char *arg[]这种形式,且arg最后一个元素必须是NULL,例如char *arg[] = {“ls”,”-l”,NULL};
示例3:execvp
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int execvp(const char *file, char *const argv[]);
int main(void)
{
printf("before execvp****\n");
char *argv[] = {"ps","-l",NULL};
if(execvp("ps",argv) == -1)
{
printf("execvp failed!\n");
}
printf("after execvp*****\n");
return 0;
}
结果:
示例4:execv
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int execv(const char *pathname, char *const argv[]);
int main(void)
{
printf("this is pro ps:\n");
printf("before execv\n");
char *argv[] = {"ps",NULL};
if(execv("/usr/bin/ps",argv) == -1)
{
printf("execv failed!\n");
perror("error");
}
printf("after execv\n");
return 0;
}
结果:
4、带e的一类exec函数
尚未实验,博文推荐及参考:linux进程—exec族函数,上面exec族部分也是参考此博客。
示例5:实战exec
与文件学习时修改配置文件的程序相结合。
代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//pid_t getpid(void);
pid_t pid;
int data;
while(1){
printf("please input a data:\n");
scanf("%d",&data);
if(data == 1){
pid = fork();
if(pid > 0)
{
wait(NULL);
}
if(pid == 0)
{
execl("./changeData","changeData","config.txt",NULL);
exit(0);
}
}else{
printf("wait,do nothing!\n");
}
}
return 0;
}
结果:
等待用户输入1后,创建子进程,由子进程执行修改配置文件的可执行文件changeData,修改成功如下,LENG=后面的字符被改成字符8。
修改后config.txt文件:
五、system、popen和pclose
1、函数system
注意:system调用后,程序返回原程序,继续执行
函数原型:
#include <stdlib.h>
int system(const char *command);
参数介绍及返回值:
示例1:system使用
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("this is pro ps:\n");
printf("before system\n");
if(system("ps") == -1)
{
printf("system failed!\n");
perror("error");
}
printf("after system\n");
return 0;
}
结果:
这里调用system后,程序返回原程序,继续执行,这里打印了after system,这一点与exec不同,其余功能相同。
2、函数popen和pclose
与system在应用中相比,可以获得运行的输出结果。
函数原型:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
参数介绍及返回值:
博文推荐:c语言popen介绍
示例2:popen和pclose使用
代码:
#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)
{
char ret[1024] = {0};
FILE *fp = NULL;
fp = popen("ps","r");
int n_fread = fread(ret,1024,1,fp);
printf("read ret %d bytes,ret =\n %s",n_fread,ret);
pclose(fp);
return 0;
}
结果:
本文参考:UNIX环境编程(第3版);其余参考及博文推荐在上文中。
最后谢谢阅读,笔者乃小白,如有错误之处还请指正。