Linux介绍进程的概念和代码编写
前言
什么是程序:编译后产生的,格式为ELF的,存储于硬盘的文件
什么是进程:程序中的代码和数据,被加载到内存中运行的过程
程序是静态的概念,进程是动态的概念
进程的复刻(fork)
父子进程是不一样的:
A) 进程号PID。PID是身份证号码,哪怕亲如父子,也要区分开。
B) 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
C) 挂起的信号。这是所谓“悬而未决”的信号,等待着进程的响应,子进程不会继承这些信号。
1\ 父子进程的内存空间的内容是一致的,但分属不同的区域各自独立
2\ 当进程退出时(不管是主动退出还是被动退出),进入僵尸态(EXIT_ZOMBIE),僵尸态下的进程无法运行,也无法被调度,但其所占据的系统资源未被释放。
3\ 僵尸态进程要等待其父进程对其资源进程回收后,才能变成死亡态(EXIT_DEAD)
进程的创建
#include <sys/types.h>
#include <unistd.h>// 在父进程中,pid将是子进程的PID
// 在子进程中,pid将是0
pid_t fork(void);//所有的代码、变量都会复制成两份。
父子进程是并发执行的,没有先后次序,若要控制次序,要依赖于信号量、互斥锁、条件量等其他条件。
gec@ubuntu:$ ./a.out
[5140]: fork之前
[5140]: pid=5141
gec@ubuntu:$ [5141]: pid=0
在父进程5140输出后,bash立即输出了命令行提示符"gec@ubuntu:$",把子进程的输出信息挤到了后面,这是因为在bash中只要判断其子进程5140退出了,它就立即输出命令行提示符,而不管它是否还有孙子进程还在运行。从中我们也知道,bash是本程序中父进程的父进程,这三个进程的关系实际上是祖孙关系:
bash-┬ (终端shell进程)
│
a.out-┬ (程序中的父进程,是bash的子进程)
│
a.out (程序中的子进程,是bash的孙子进程)
进程的回收
#include <sys/types.h>
#include <sys/wait.h>//阻塞当前进程
pid_t wait(int *wstatus);//等待其子进程退出并回收其系统资源;
若wstatus指针为NULL,则代表当前进程放弃其子进程的退出状态。
示例代码:
#include <unistd.h>
#include <sys/wait.h>
int main()
{
if(fork() == 0)
{
printf("[%d]: 我将在3秒后正常退出,退出值是88\n", getpid());
for(int i=3; i>=0; i--)
{
fprintf(stderr, " ======= %d =======%c", i, i==0?'\n':'\r');
sleep(1);
}
exit(88);
}
else
{
printf("[%d]: 我正在试图回收子进程的资源...\n", getpid());
int status;
wait(&status); //等待子进程执行完
if(WIFEXITED(status))
{
printf("[%d]: 子进程正常退出了,其退出值是:%d\n", getpid(), WEXITSTATUS(status));
}
}
}
执行结果是:
gec@ubuntu:$ ./a.out
[3611]: 我正在试图回收子进程的资源…
[3612]: 我将在3秒后正常退出,退出值是88
======= 0 =======
[3611]: 子进程正常退出了,其退出值是:88
gec@ubuntu:$
宏功能WIFEXITED(status)判断子进程是否正常退出WEXITSTATUS(status)获取正常退出的子进程的退出值WIFSIGNALED(status)判断子进程是否被信号杀死WTERMSIG(status)获取杀死子进程的信号的值
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
与wait()的区别:
可以通过参数 pid 用来指定想要回收的子进程。
可以通过 options 来指定非阻塞等待。
pid作用options作用<-1等待组ID等于pid绝对值的进程组中的任意一个子进程0阻塞等待子进程的退出-1等待任意一个子进程WNOHANG若没有僵尸子进程,则函数立即返回0等待本进程所在的进程组中的任意一个子进程WUNTRACED当子进程暂停时函数返回>0等待指定pid的子进程WCONTINUED当子进程收到信号SIGCONT继续运行时函数返回
注意:options的取值,可以是0,也可以是上表中各个不同的宏的位或运算取值。
加载并执行指定程序
#include <unistd.h>
extern 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, ...);
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[]);
给进程加载指定的程序,如果成功,进程的整个内存空间都被覆盖。
各个后缀字母的含义:
l : list 以列表的方式来组织指定程序的参数
v: vector 矢量、数组,以数组的方式来组织指定程序的参数
e: environment 环境变量,执行指定程序前顺便设置环境变量
p: 专指PATH环境变量,这意味着执行程序时可自动搜索环境变量PATH的路径
execl(const char *path, const char *arg, …) 为例,参数path是需要加载的指定程序,而arg则是该程序运行是的命令行参数
execl("./a.out", “./a.out”, “123”, “abc”, NULL);
这其中:
第一个./a.out是程序本身,第二个./a.out是第一个参数。
参数列表以NULL结尾。
示例代码:
// child.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
// 倒数 n 秒 n=3
for(int i=atoi(argv[1]); i>0; i--)
{
printf("%d\n", i);
sleep(1);
}
// 程序退出,返回 n
exit(atoi(argv[1])); //
}
// main.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
// 子进程
if(fork() == 0)
{
printf("加载新程序之前的代码\n");
// 加载新程序,并传递参数3
execl("./child", "./child", "3", NULL);
printf("加载新程序之后的代码\n");//没有打印
}
// 父进程
else
{
// 等待子进程的退出 -1:任意进程
int status;
int ret = waitpid(-1, &status, 0);
if(ret > 0)
{
if(WIFEXITED(status))
printf("[%d]: 子进程[%d]的退出值是:%d\n",
getpid(), ret, WEXITSTATUS(status));
}
else
{
printf("暂无僵尸子进程\n");
}
}
}
程序运行结果:
gec@ubuntu:$ gcc child.c -o child //生成./child
gec@ubuntu:$ gcc main.c -o main
gec@ubuntu:$ ./main
加载新程序之前的代码
3
2
1
[5634]: 子进程[5635]的退出值是:3
gec@ubuntu:$
程序解析
子进程中加载新程序之后的代码无法运行,因为已经被覆盖了。
waitpid()中指定了options的值为0,意味着阻塞等待子进程,效果跟直接调用wait()相当。
僵尸进程
产生的原因
以下代码可以查看处于僵尸态的进程:
// zombie.c
int main()
{
// 子进程退出(变僵尸)
if(fork() == 0)
return 0;
// 父进程不退出
pause();
return 0;
}
执行上述代码,并使用ps命令查看进程状态:
gec@ubuntu:~$ ./zombie
gec@ubuntu:~$ ps ajx
… …
8390 8422 8422 8422 pts/2 8439 Ss 1000 0:00 bash
8422 8439 8439 8422 pts/2 8439 S+ 1000 0:00 ./zombie
8439 8440 8439 8422 pts/2 8439 Z+ 1000 0:00 [zombie]
8406 8441 8441 8406 pts/1 8441 R+ 1000 0:00 ps ajx
gec@ubuntu:~$ ps ajx
由上述结果可见,zombie父进程处于睡眠状态[S+],而其子进程[zombie]处于僵尸态[Z+]。
释放僵尸进程
如果父进程一直对其子进程不管不顾,那么其子进程的确会长期处于僵尸态,浪费系统资源。僵尸进程只有当以下情形之一发生时,才会释放其资源:
父进程对其调用 wait()/waitpid()。
父进程退出,被孤儿进程组收养。
子进程等待父进程对其执行wait()/waitpid()
释放对应僵尸子进程的系统资源
获取对应僵尸子进程的退出状态
阻塞父进程(可选)
只要父进程代码做如下修改即可避免僵尸的产生:
// no-zombie.c
int main()
{
// 子进程退出(变僵尸)
if(fork() == 0)
return 0;
// 父进程调用wait()释放子进程资源
wait(NULL);
pause();
return 0;
}
子进程主动告知父进程前来收尸
具体而言,子进程在进入僵尸态时,会自动向父进程发送信号SIGCHILD,而父进程可以利用异步信号响应函数来及时处理这些僵尸子进程。参考代码如下:
void cleanup(int sig)
{
// 僵尸子进程会被自动清除
wait(NULL); //这个函数回收子进程
}
int main()
{
// 在产生子进程之前,准备好处理它们的SIGCHILD信号
signal(SIGCHLD, cleanup);
// 子进程退出,成为僵尸进程
if(fork() == 0)
return 0;
// 父进程干自己的活,无需关注子进程
while(1)
pause();
//由于pause()函数会在收到信号后退出
//为了能查看父进程在清除子进程资源后仍在运行
//上述代码中使用了循环来执行pause()函数
return 0;
}
计算进程个数:
pid_t p1 = fork();
pid_t p2 = fork();
pid_t p3 = fork();