1.系统的启动运行进程
- 当用户进程init开始运行,就开始扮演用户进程的祖先角色,永远不会被终止。
- 所以:计算机上的所有进程都是有上下亲属关系的,他们组成一个庞大的家族树。
※ 观察linux下的进程间父子关系:
- pstree
以树状结构方式列出系统中正在运行的各进程间的父子关系。 - ps ax -o pid,ppid,command
2. 基本进程编程
(1)新建进程——fork
- pid_t fork(void);
- 一次克隆,两个返回值
- 两个执行线路
//示例1:执行一次fork语句,父亲睡眠2秒
[flora@localhost ~]$ vi fork_1.c
[flora@localhost ~]$ gcc -o myfork_1 fork_1.c
[flora@localhost ~]$ cat fork_1.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void){
pid_t pid;
char *message;
int x;
pid = fork();
if (pid < 0)
{ perror("fork failed");
exit(1); }
if (pid == 0)
{ message = "This is the child\n";
x = 0; }
else
{ message = "This is the parent\n";
x = 10;
sleep(2); } //注意
printf("%s I'm %d, x=%d,my father is:%d\n",message,getpid(),x,getppid());
return 0;
}
[flora@localhost ~]$ ./myfork_1
This is the child
I'm 3928, x=0,my father is:3927
This is the parent
I'm 3927, x=10,my father is:2725
上例运行结果:先输出孩子语句,等待两秒后输出父亲语句。
//示例2:执行一次fork语句,孩子睡眠2秒
[flora@localhost ~]$ vi fork_2.c
[flora@localhost ~]$ cat fork_2.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void){
pid_t pid;
char *message;
int x;
pid = fork();
if (pid < 0)
{ perror("fork failed");
exit(1); }
if (pid == 0)
{ message = "This is the child\n";
x = 0;
sleep(2); } //注意
else
{ message = "This is the parent\n";
x = 10;
}
printf("%s I'm %d, x=%d,my father is:%d\n",message,getpid(),x,getppid()); return 0;
}
[flora@localhost ~]$ gcc -o myfork_2 fork_2.c
[flora@localhost ~]$ ./myfork_2
This is the parent
I'm 3168, x=10,my father is:3084
[flora@localhost ~]$ This is the child
I'm 3169, x=0,my father is:1
ps
PID TTY TIME CMD
2721 pts/0 00:00:00 bash
2893 pts/0 00:00:00 bash
3084 pts/0 00:00:00 bash
3179 pts/0 00:00:00 ps
上例结果分析:先输出父亲语句,父亲语句执行完后,bash获得了cpu,把命令行输出了一下,然后孩子睡两秒后输出语句。
注意:可以看到父亲的父亲是bash,而孩子的父亲是1(因为父亲执行完后就结束了,孩子睡完再执行时,孩子的父亲就是祖先进程了,由祖先进程回收进程)
注意:当循环语句中含fork语句时,循环n次,则有2^n个进程。
但是当子进程中含有while语句时情况则不同,如下图:
产生的进程数:循环n次,则有n个子进程,再加一个父进程(因为由于fork语句产生的新子进程中有while语句,所以当执行子进程时就会一直while下去,后续就不再执行fork语句循环产生新的子进程了)
(2)执行新工作——exec
- int execve()是最根本的系统调用
- 需要路径、命令行参数、和环境变量三种参数
- p:利用PATH环境变量查找可执行的文件;
- l:希望接收以逗号分隔的形式传递参数列表,列表以NULL指针作为结束标志;
- v:希望以字符串数组指针( NULL结尾)的形式传递命令行参数;
- e:传递指定参数envp,允许改变子进程的环境,后缀没有e时使用当前的程序环境
注意:只有execve是系统调用;其他函数调用最终都由execve完成工作。无论是哪个exec类函数,都是将要执行程序的路径、命令行参数、和环境变量3个参数传递给execve
示例:
[flora@localhost ~]$ vi forkexec_1.c
[flora@localhost ~]$ cat forkexec_1.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void){
pid_t a;
a = fork();
if (a < 0) {
perror("fork failed");
exit(1);
}
if (a == 0) {
execlp ("ps" ,"ps",NULL);
execlp ("ls" ,"ls","-al","/etc/passwd ",NULL);
return 1;
}
else {
wait(NULL);
printf("father exit\n");
}
return 0;
}
[flora@localhost ~]$ gcc -o myforkexec_1 forkexec_1.c
[flora@localhost ~]$ ./myforkexec_1
PID TTY TIME CMD
2721 pts/0 00:00:00 bash
2893 pts/0 00:00:00 bash
3084 pts/0 00:00:00 bash
6591 pts/0 00:00:00 myforkexec_1
6592 pts/0 00:00:00 ps
father exit
上例结果分析:因为父进程有wait语句,相当于阻塞态,所以cpu会先给孩子进程,执行第一条execlp语句,但是当执行完这条语句后,已经覆盖掉了孩子进程中的代码,所以不会执行后续的指令了,当孩子进程执行完后,系统会触发刚才的父亲进程,父亲进程被唤醒,输出它的语句。
注意:
- exec函数执行成功会装入新代码,原来的代码在其进程空间中已不存在。
- 只有exec调用失败返回-1,其后的代码才有可能得到执行。
拓展:若修改子进程代码:在开始时加入输出语句
if (a == 0) {
printf("I am child\n");
execlp ("ps" ,"ps",NULL);
execlp ("ls" ,"ls","-al","/etc/passwd ",NULL);
return 1;
}
则结果如下图:
(3)进程的消亡——exit
- void exit(int status);
- 需要路径、命令行参数、和环境变量三种参数
注意:程序执行结束或调用exit后并不是马上消失,而是变为僵死状态——放弃了几乎所有内存空间,不再被调度,但保留有PCB信息供wait收集。
示例:注意要想看子进程的僵死态,必须让父进程睡眠一段时间,否则父进程结束后,就只能由祖先进程回收子进程,就看不到僵死态了
[flora@localhost ~]$ vi fork_1.c
[flora@localhost ~]$ cat fork_1.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void){
pid_t pid;
char *message;
int x;
pid = fork();
if (pid < 0)
{ perror("fork failed");
exit(1); }
if (pid == 0)
{ message = "This is the child\n";
x = 0; }
else
{ message = "This is the parent\n";
x = 10;
sleep(20); }
printf("%s I'm %d, x=%d,my father is:%d\n",message,getpid(),x,getppid());
return 0;
}
[flora@localhost ~]$ gcc fork_1.c
[flora@localhost ~]$ ./a.out& //注意:&使进程在后台执行
[1] 7202
[flora@localhost ~]$ This is the child
I'm 7203, x=0,my father is:7202
ps -x|grep a.out //此时父亲在睡眠,此契机下,用ps命令观察子进程死亡前状态
7202 pts/0 S 0:00 ./a.out //睡眠态
7203 pts/0 Z 0:00 [a.out] <defunct> //僵死态
7205 pts/0 R+ 0:00 grep --color=auto a.out
[flora@localhost ~]$ This is the parent //父亲睡眠后返回,打印其输出
I'm 7202, x=10,my father is:3084
[1]+ 完成 ./a.out
(4)进程间的等待——wait
- pid_t wait(int *status)
- 使调用它的进程等待,进入阻塞状态
- 子进程死亡会唤醒阻塞的父进程(但注意:一个孩子只能唤醒一次父亲,如果有多个孩子的话,则需要加多个wait语句)
示例:
[flora@localhost ~]$ vi forkwait_cp.c
[flora@localhost ~]$ cat forkwait_cp.c
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(){
pid_t pc1,pc2,pw1,pw2;
pc1=fork();
pc2=fork();
if (pc1>0&&pc2>0) { //*父进程
pw1=wait(NULL);
printf("***Catch a dead child process with pid: %d\n",pw1);
pw2=wait(NULL);
printf("***Catch a dead child process with pid: %d\n",pw2);
printf("***I'M %d,THE MAIN PROCESS LEAVE!\n",getpid()); \
}
if (pc1==0&&pc2>0) { //*子进程1
printf("I'M the first child PID:%d,my father is:%d\n",getpid(),getppid());
sleep(10);
}
if (pc1>0&&pc2==0) //*子进程2
printf("I'M the 2nd child PID:%d,my father is:%d,i don't sleep.\n",getpid(),getppid());
if (pc1==0&&pc2==0) //*孙进程
printf("I'M grandson PID:%d,my father is:%d,no one is waiting for – me.\n",getpid(),getppid());
exit(0);
}
[flora@localhost ~]$ gcc forkwait_cp.c -o myforkwait_cp
[flora@localhost ~]$ ./myforkwait_cp
I'M the 2nd child PID:7852,my father is:7850,i don't sleep.
***Catch a dead child process with pid: 7852
I'M the first child PID:7851,my father is:7850
I'M grandson PID:7853,my father is:7851,no one is waiting for – me.
***Catch a dead child process with pid: 7851
***I'M 7850,THE MAIN PROCESS LEAVE!
上例结果分析:
可以看出由于父进程有wait语句,所以cpu会先给子进程,而子进程1有睡眠时间,因此子进程2会先输出执行,结束后会唤醒一次父进程,输出对应的语句;然后由于父进程还有一个wait语句,所以子进程1和孙进程会先后执行输出对应的语句,停顿10s后,子进程1结束再次唤醒父进程,输出对应语句,最后再输出自己的语句,而孙进程则由祖先进程回收。