- 一、进程相关概念
- 二、进程创建函数fork的使用
- fork函数:
- getpid函数:
- 三、进程创建的时候发生了什么事?
- 四、创建新进程的的应用场景及fork总结
- 五、vfork函数创建进程
- 六、进程退出
- 七、父进程等待子进程退出
- 孤儿进程(了解,面试):
- 八、exec族函数
- 九、system函数
- 十、popen函数
关于进程,我们需要掌握五个问题:
- 什么是程序,什么是进程,有什么区别?
程序是静态的概念,gcc xxx.c –o pro,磁盘中生成pro文件,叫做程序。进程是程序的一次运行活动,
通俗点意思是程序跑起来了,系统中就多了一个进程。
- 如何查看系统中有哪些进程?
a. 使用ps指令查看实际工作中,配合grep来查找程序中是否存在某一个进程
b. 使用top指令查看,类似windows任务管理器
- 什么是进程标识符?
每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证。
Pid=0: 称为交换进程(swapper) 作用—进程调度 Pid=1:init进程 作用—系统初始化
编程调用getpid函数获取自身的进程标识符,getppid获取父进程的进程标识符。
- 什么叫父进程,什么叫子进程?
进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系。
- C程序的存储空间是如何分配?
a. 正文段也叫代码段,对应上图红色箭头部分,该段主要保存在最低地址的正文部分。
b. 初始化的数据,又称为数据段,对应上图紫色箭头部分,此类变量主要存放在初始化的数据段中。
c. 只定义而未被赋值的变量称为非初始化数据段,也称bss段,此类变量存放在非初始化的数据段中。
d. 通常在堆中进行动态存储分配。
e. 自动变量(函数中局部变量)以及每次函数调用时所保存的信息都存放在栈中。
f. 命令行参数如
a
r
g
c
argc
argc 和
a
r
g
v
argv
argv 这两个变量存放在高地址的命令行变量和环境变量中。
头文件:
#include
函数原型:
pid_t fork(void);
参数:
无参数 返回值:fork函数调用成功,返回两次 返回值为0, 代表当前进程是子进程 返回值非负数,代表当前进程为父进程 调用失败,返回-1getpid函数:
头文件:
#include#include
函数原型:
pid_t getpid(void); //获取子进程pid pid_t getppid(void); //获取父进程pid
参数:
返回进程pid
编程实现创建子进程,并且分别获取父进程和子进程的pid号:
根据父进程和子进程的pid不同的特点,我们可以在创建进程之前获取一次进程pid,这是父进程的pid,创建进程之后再一次获取进程pid,并通过判断两次pid是否相同判断哪个是父进程pid,哪个是子进程pid。
#include#include #include int main() { pid_t pid; pid_t pid2; pid = getpid(); printf("before fork,pid=%dn",pid); fork(); pid2 = getpid(); printf("after fork,pid=%dn",pid2); if(pid == pid2){ printf("this is a father processn"); }else { printf("this is a child process,pid=%dn",pid2); } return 0; }
运行结果:
根据
f
o
r
k
fork
fork 函数的返回值也可以对父子进程进行判断:
#include#include #include int main() { pid_t pid; printf("father:pid=%dn",getpid()); pid = fork(); if(pid > 0){ printf("this is a father process,pid=%dn",getpid()); }else if(pid == 0) { printf("this is a child process,pid=%dn",getpid()); } return 0; }
运行结果:
当子进程被创建的时候,到底发生了什么事呢?在早期的Linux内核中,新进程会将代码段、数据段、bss段、堆、栈、命令行参数和环境变量等全部拿来拷贝一份,放入新进程中执行,而随着Linux内核的更新,当新进程被创建的时候,采用写时拷贝的方式运行新进程,在新进程中发生改变的变量新进程会将其拷贝一份作为自己的数据,而其他未被触及到的内容则于父进程进行共享。我们用代码验证一下:
#include#include #include int main() { pid_t pid; int data = 10; printf("father:pid=%dn",getpid()); pid = fork(); if(pid > 0){ printf("this is a father process,pid=%dn",getpid()); }else if(pid == 0) { printf("this is a child process,pid=%dn",getpid()); data = data + 100; } printf("data=%dn",data); return 0; }
运行结果:
由此可见,新进程写时拷贝了 d a t a data data 数据作为自己的变量,与父进程区分开来,而其他的变量则与父进程共享。
fork创建一个子进程的一般目的:
第一个应用最为常见,我们可以用一幅图来说明:
由于我们尚未了解网络编程相关内容,所以我们用一段代码模拟这个应用:
#include#include #include int main() { pid_t pid; int data; while(1){ printf("please input a datan"); scanf("%d",&data); if(data == 1){ pid = fork(); if(pid > 0){ }else if(pid == 0) { while(1){ printf("do net request,pid=%dn",getpid()); sleep(3); } } }else { printf("wait,do nothing!n"); } } return 0; }
运行结果:
f
o
r
k
fork
fork 总结:
vfork函数 也可以创建进程,与fork有什么区别?
- 关键区别一:vfork 直接使用父进程存储空间,不拷贝。
- 关键区别二:vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
下面我们写段代码验证一下:
这是用 f o r k fork fork 函数创建进程:
#include#include #include int main() { pid_t pid; pid = fork(); if(pid > 0){ while(1){ printf("this is a father process,pid=%dn",getpid()); sleep(1); } }else if(pid == 0) { while(1){ printf("this is a child process,pid=%dn",getpid()); sleep(1); } } return 0; }
运行结果:
这是用 v f o r k vfork vfork 函数创建进程:
#include#include #include #include int main() { pid_t pid; int cnt = 0; pid = vfork(); if(pid > 0){ while(1){ printf("this is a father process,pid=%dn",getpid()); sleep(1); } }else if(pid == 0) { while(1){ printf("this is a child process,pid=%dn",getpid()); sleep(1); cnt++; if(cnt == 3){ exit(0); } } } return 0; }
运行结果:
由此可见,当用 f o r k fork fork 函数创建进程的时候,父子进程会被调度交替运行,而当用 v f o r k vfork vfork 函数创建进程的时候,子进程会不断执行,直到子进程被执行完成后,父进程才会执行。
正常退出:
1. Main函数调用return 2. 进程调用exit(),标准c库 3. 进程调用_exit()或者_Exit(),属于系统调用 补充: 1. 进程最后一个线程返回 2. 最后一个线程调用pthread_exit
异常退出:
1. 调用abort 2. 当进程收到某些信号时,如ctrl+C 3. 最后一个线程对取消(cancellation)请求做出响应
函数原型:
这里建议使用 e x i t ( ) exit() exit() 函数进行退出,因为 e x i t ( ) exit() exit() 是对 e x i t ( ) _exit() exit() 和 E x i t ( ) _Exit() Exit() 函数的封装, e x i t ( ) exit() exit() 会对缓存区先进行冲刷,然后再进行退出。
在上面我们实现了子进程退出,父进程继续执行,但是当创建一个子进程并且子进程执行完以后,父进程需要知道子进程的状态,如果子进程退出状态不被首义,它就会变成僵尸进程(Zombie),上面的代码执行完毕后各进程的状态如图:
这就涉及到父进程等待子进程退出并收集子进程的退出状态, w a i t wait wait 函数就是到父进程等待子进程退出并收集子进程状态的函数,但是对于子进程的退出状态,需要 W E X I T S T A T U S WEXITSTATUS WEXITSTATUS 宏对状态进行解析才能收到正确的子进程退出状态。
相关函数和宏:
status参数: 是一个整型数指针 非空: 子进程退出状态放在它所指向的地址中。 空: 不关心退出状态
编程实现:
#include#include #include #include int main() { pid_t pid; int cnt = 0; int status = 10; pid = fork(); if(pid > 0){ wait(&status); printf("child quit,child status = %dn",WEXITSTATUS(status)); while(1){ printf("cnt=%dn",cnt); printf("this is a father process,pid=%dn",getpid()); sleep(1); } }else if(pid == 0) { while(1){ printf("this is a child process,pid=%dn",getpid()); sleep(1); cnt++; if(cnt == 5){ exit(3); } } } return 0; }
运行结果:
 { pid_t pid; int cnt = 0; pid = fork(); if(pid > 0){ printf("this is father print,pid = %dn",getpid()); }else if(pid == 0) { while(1){ printf("this is a child print,pid=%d,my father pid=%dn",getpid(),getppid()); sleep(1); cnt++; if(cnt == 5){ exit(3); } } } return 0; }
运行结果:
Unix环境高级编程对于进程等待和退出的验证:(了解)
运行结果:
exec族函数函数的作用:
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
exec族函数功能:
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
函数原型:
#includeextern char **environ; 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[]); //不做重点
返回值:
exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
参数说明:
path:可执行文件的路径名字 arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束 file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。 exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助: l : 使用参数列表 p:使用文件名,并从PATH环境进行寻找可执行文件 v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。 e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量
execl编程验证:
主程序:
#include#include #include //函数原型:int execl(const char *path, const char *arg, ...); int main(void) { printf("before execln"); if(execl("./echoarg","echoarg","abc",NULL) == -1) { printf("execl failed!n"); } printf("after execln"); return 0; }
待执行程序:
#includeint main(int argc,char *argv[]) { int i = 0; for(i = 0; i < argc; i++) { printf("argv[%d]: %sn",i,argv[i]); } return 0; }
运行结果:
#include#include #include //函数原型:int execl(const char *path, const char *arg, ...); int main(void) { printf("before execln"); if(execl("/bin/ls","ls","-l",NULL) == -1) { printf("execl failed!n"); } printf("after execln"); return 0; }
运行结果:
execlp编程验证:
#include#include #include //函数原型:int execl(const char *path, const char *arg, ...); int main(void) { printf("before execln"); if(execlp("ls","ls","-l",NULL) == -1) { printf("execl failed!n"); } printf("after execln"); return 0; }
运行结果:
由此可见, e x e c l p execlp execlp 函数在找可执行文件的时候不需要加文件具体路径,而是直接在第一个参数的地方写可执行文件名,因为它会自己在环境变量中寻找对应程序。
execvp编程验证:
#include#include #include //函数原型:int execl(const char *path, const char *arg, ...); int main(void) { printf("before execln"); char *argv[] = {"ps","-l",NULL}; //用这个字符串数组指针argv,代替execlp函数参数中的后三项 if(execvp("ls",argv) == -1) { printf("execl failed!n"); } printf("after execln"); return 0; }
运行结果:
由此可见, e x e c v p execvp execvp 函数和 e x e c l p execlp execlp 函数唯一的区别就是 e x e c v p execvp execvp 将原本 e x e c l p execlp execlp 函数的后三项合并为一个字符串数组指针传进来。
execv编程验证:
#include#include #include //函数原型:int execl(const char *path, const char *arg, ...); int main(void) { printf("before execln"); char *argv[] = {"ps","-l",NULL}; if(execvp("/bin/ls",argv) == -1) { printf("execl failed!n"); } printf("after execln"); return 0; }
运行结果:
该函数无非就是需要加可执行文件的路径,和不加
p
p
p 函数的特点相同。不做仔细论述。
exec配合fork使用:(在一个进程中调用另一个程序)
主程序:
#include#include #include int main() { pid_t pid; int data; while(1){ printf("please input a datan"); scanf("%d",&data); if(data == 1){ pid = fork(); if(pid == 0) { execl("./changeData","changeData","config.txt",NULL); } }else { printf("wait,do nothing!n"); } } return 0; }
被调用的程序:
#include#include #include #include #include #include #include int main(int argc, char **argv) { int fdSrc; char *readBuf = NULL; if(argc != 2){ printf("param error!n"); exit(-1); } fdSrc = open(argv[1],O_RDWR); int size = lseek(fdSrc, 0, SEEK_END); lseek(fdSrc, 0, SEEK_SET); readBuf = (char*)malloc(sizeof(char)*size + 8); int n_read = read(fdSrc, readBuf, size); char *p = strstr(readBuf, "LENG="); p = p + strlen("LENG="); *p = '5'; lseek(fdSrc, 0, SEEK_SET); int n_write = write(fdSrc, readBuf, strlen(readBuf)); close(fdSrc); return 0; }
运行结果:
配置文件被更改前:
更改后:
s y s t e m system system 函数类似于 e x e c exec exec 族函数,我们用fork函数创建新进程后,也能在新进程中调用 s y s t e m system system 去执行另外一个程序。当进程调用 s y s t e m system system 函数时,该进程被替换为新程序。与 e x e c exec exec 族函数不同的一点是, s y s t e m system system 函数调用完一个程序后,还会返回原来的主程序继续执行完。
函数原型:
#includeint system(const char *command);
参数:
const char *command:命令字符串 system()函数的返回值如下: 成功,则返回进程的状态值; 当sh不能执行时,返回127; 失败返回-1;
编程实现:
#include#include #include //函数原型:int execl(const char *path, const char *arg, ...); int main(void) { printf("before execln"); if(system("ls -l") == -1) { printf("execl failed!n"); } printf("after execln"); return 0; }
运行结果:
由此可见, s y s t e m system system 函数的简便之处,而且它所调用的程序执行完以后,主程序还会继续执行,这也是它的特殊之处。
popen() 函数用于创建一个管道:其内部实现为调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程这个进程必须由 pclose() 函数关闭。比system在应用中的好处:可以获取运行的输出结果。
函数原型:
#include “stdio.h” FILE popen( const char command, const char* mode )
参数:
command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。 这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。 mode: 只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。 如果 type 是 “r” 则文件指针连接到 command 的标准输出; 如果 type 是 “w” 则文件指针连接到 command 的标准输入。 如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL,具体错误要根据errno判断。 int pclose (FILE* stream) 参数说明: stream:popen返回的文件指针 返回值: 如果调用失败,返回 -1
编程实现:
#include#include #include //函数原型:int execl(const char *path, const char *arg, ...); int main(void) { char ret[1024] = {0}; FILE *fp; fp = popen("ps","r"); int nread = fread(ret,1,1024,fp); printf("read %d byte,ret=%sn",nread,ret); pclose(fp); return 0; }
运行结果:
主程序
p
o
p
e
n
popen
popen 函数调用的程序运行的内容被捕获保存在
r
e
t
ret
ret 字符串变量中。
本次关于Linux系统编程中进程的学习笔记就分享到这里。多多分享,多多交流。
自己的命运自己掌控,脚下的路不停步。



