进程定义:一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。
简言之:进程是程序的一次执行过程
进程特性:
- 动态性:进程是动态产生的,往往都会经历创建、运行、消亡三个状态
- 独立性:各个进程之间的地址空间相互独立
- 并发性:任何进程都可以一起向前推进
- 异步性:每个进程都以其不可预知的速度向前推进
- 结构化:进程 = 代码段 + 数据段 + PCB(进程控制块)
进程与程序的区别:
- 进程是动态的执行过程;程序是静态的代码
- 进程是暂时的,运行在内存中的一个状态变化的过程;程序是永久的,保存在外存中
- 通过多次执行一个程序,进而产生多个进程,所以一个程序可以对应多个进程;
- 进程通过调用多个程序,进而一个进程可以执行多个程序,所以一个进程可以对应多个程序
进程是竞争计算机资源的基本单位
进程控制块(PCB)是用来记录进程相关信息和管理进程而设置的一种数据结构
进程控制块(PCB)是由操作系统(OS)维护的
系统通过PCB感知进程的存在
PCB随进程的创建而创建并填写,随着进程的消亡而释放
PCB的组成:
- 进程标识符:用于唯一标识该进程的整数
- 进程名:通常是可执行文件名
- 优先级:进程优先运行的权重
- 寄存器值:用于保存当前进程运行到某一时刻各种数据信息
- …
在Linux操作系统中,每一个进程都有一个PCB,每一个PCB都对应一个task_struct结构体,简言之,每创建一个进程就相当于创建一个task_struct结构体并填写其中的数据。
Linux中的PCB定义在sched.h文件中
若想获得sched.h文件,需要到Linux内核官网下载内核源码
以Linux Kernel Source Code 2.6.32 为例展示task_struct部分源码
参数:
- -a : 显示所有用户的进程
- -u : 以用户为主的进程状态
- -x : 列出较完整的信息
打印显示出来的信息:
PID:进程标识符
TTY:命令所运行的位置
STAT:进程状态
TIME:运行该命令所占用的CPU处理时间
COMMAND:该进程所运行的命令
1.显示当前所有进程及详细信息
[gongruiyang@localhost ~]$ ps -ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:01 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
2 ? S 0:00 [kthreadd]
3 ? S 0:00 [ksoftirqd/0]
5 ? S< 0:00 [kworker/0:0H]
7 ? S 0:00 [migration/0]
...................................................
2.显示某用户的进程及详细信息
[gongruiyang@localhost ~]$ ps -u gongruiyang
PID TTY TIME CMD
1736 ? 00:00:00 gnome-keyring-d
1741 ? 00:00:00 gnome-session-b
1748 ? 00:00:00 dbus-launch
1749 ? 00:00:00 dbus-daemon
1807 ? 00:00:00 gvfsd
1812 ? 00:00:00 gvfsd-fuse
1904 ? 00:00:00 ssh-agent
1923 ? 00:00:00 at-spi-bus-laun
1928 ? 00:00:00 dbus-daemon
...................................................
3.显示进程及其CPU和内存占用情况
[gongruiyang@localhost ~]$ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 128212 6836 ? Ss 11:00 0:01 /usr/li
root 2 0.0 0.0 0 0 ? S 11:00 0:00 [kthrea
root 3 0.0 0.0 0 0 ? S 11:00 0:00 [ksofti
root 5 0.0 0.0 0 0 ? S< 11:00 0:00 [kworke
root 7 0.0 0.0 0 0 ? S 11:00 0:00 [migrat
root 8 0.0 0.0 0 0 ? S 11:00 0:00 [rcu_bh
pid_t fork(void)
fork调用一次,会在父进程中返回一个值,会在子进程中返回一个值
父进程中:返回子进程PID
子进程中:返回 0
Q:为什么fork会返回两次呢?
A:子进程复制父进程的堆和栈中的内容,此时,两个进程都处于fork函数中,都在等待fork函数执行结束并返回一个pid_t,所以会有两个返回值
pid_t实际上就是int,被定义在sys/typpes.h中
代码演示:
getpid()函数是获得当前进程的进程号
getppid()函数是获得当前进程父进程的进程号
#include
#include
#include
#include
int main()
{
printf(" [fpid] [pid] [ppid]\n");
pid_t fpid = fork();
if(fpid < 0) //创建进程失败
perror("fork");
else if(fpid == 0)//子进程执行这一段代码
printf("child: %4d %4d %4d\n",fpid,getpid(),getppid());
else //父进程执行这一段代码
printf("father: %4d %4d %4d\n",fpid,getpid(),getppid());
while(1) //父子进程都执行
{
sleep(1);
}
return 0;
}
运行结果:
[fpid] [pid] [ppid]
father: 5081 5080 2717
child: 0 5081 5080
解释:pid为5080的父进程 创建了一个 pid为5081的子进程,其中 父进程的 父进程号为2717,子进程的 父进程号为5080
fork特性:
子进程将父进程中打开的所有文件描述符都复制了一遍,父子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用数量增加了
子进程不会继承父进程的一些数据:
- 子进程不复制父进程设置的锁(若继承会导致排它锁矛盾)
- 子进程不复制父进程的pid,而是产生自己的pid
- 子进程不复制父进程中的pending alarms和pending signals,而是将自己的pending alarms清除,将pending signals置为空
fork产生失败可能的两个原因:
- 当前运行中的进程数已经达到了系统规定的上限,此时错误码(errno)的值为EAGAIN
- 当前系统内存容量不足以开辟一个新的进程,此时错误码(errno)的值为ENOMEM
pid_t vfork(void)
该函数返回值特点与fork相同
vfork创建出来的子进程并不会直接将父进程的虚拟空间内容拷贝一份,而是与父进程共享一份虚拟空间,当子进程需要修改数据时,才会进行拷贝,这称之为写时拷贝技术
父进程使用vfork创建子进程后,父进程会被挂起,直到子进程终止或被替换后,才能继续推进
应当使用exit或_exit来终止vfork创建的子线程,不能使用return来终止。若使用return来终止子线程会导父进程回到调用vfork处,进而无限创建子进程进而产生段错误
代码演示:
#include
#include
#include
#include
int main()
{
pid_t fpid = vfork();
if(fpid < 0)
perror("vfork");
else if(fpid == 0)
{
printf("child:%d\n",getpid());
while(1)
{
printf("while\n");
sleep(1);
}
}
else
printf("father:%d\n",getpid());
printf("========================\n");
return 0;
}
输出结果:
child:5898
while
while
while
while
while
^C
解释:由于子进程处于循环sleep中,导致父进程一直被挂起无法执行下面的代码
异:
同:
传统拷贝方法:
- 为子进程的页表分配页帧
- 为子进程的页分配页帧
- 初始化子进程的页表
- 把父进程的页内容复制到子进程对应页中
写时拷贝(Copy - On - Write)技术:
子进程并不拷贝父进程的数据资源,而是父子进程共享父进程原有的数据资源,只有当要写入的时候,才进行资源的复制
实际上,COW不但在Linux进程上有实际应用,而且在C++的String类在g++环境下也支持COW技术
#include
#include
#include
using namespace std;
int main()
{
string str1 = "Hello\n";
string str2 = str1;
printf("Befor Write: str1[%x] str2[%x]\n", str1.c_str(), str2.c_str());
str2.clear();
str2 = "H\n";
printf("After Write: str1[%x] str2[%x]\n", str1.c_str(), str2.c_str());
return 0;
}
输出结果:
[gongruiyang@localhost TestCOW]$ g++ COWtest.cpp -o test
[gongruiyang@localhost TestCOW]$ ./test
Befor Write: str1[18cec38] str2[18cec38]
After Write: str1[18cec38] str2[18cec98]
解释:从输出结果来看,str1和str2一开始指向同一内存地址,共享Hello这个数据,当str2要进行写入的时候,str2指向的地址发生改变,重新开辟空间存放数据,即只有写入东西的时候才进行内存的再分配
void exit(int status)
代码测试:
#include
#include
#include
#include
#include
int main()
{
pid_t fpid = fork();
int status;
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
printf("ChildP: childPID[%d]\n",getpid());
exit(EXIT_SUCCESS); //正常退出子进程
}
else
{
int ret_pid = wait(&status); //父进程阻塞等待子进程退出
printf("FatherP: %d is normally exited with status:%d\n",ret_pid,status);
}
return 0;
}
输出结果:
ChildP: childPID[8071]
FatherP: 8071 is normally exited with status:0
void _exit(int status);
代码测试:
#include
#include
#include
#include
#include
int main()
{
pid_t fpid = fork();
int status;
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
printf("ChildP: childPID[%d]\n",getpid());
_exit(EXIT_SUCCESS);
}
else
{
int ret_pid = wait(&status);
printf("FatherP: %d is normally exited with status:%d\n",ret_pid,status);
}
return 0;
}
输出结果:
ChildP: childPID[8586]
FatherP: 8586 is normally exited with status:0
异:
同:
代码测试:
#include
#include
int main()
{
printf("Hello!\n");
printf("World!");
exit(0);
return 0;
}
#include
#include
int main()
{
printf("Hello!\n");
printf("World!");
_exit(0);
return 0;
}
exit输出结果:
Hello!
World!
_exit输出结果:
Hello!
程序解释:由测试代码可以看出_exit由于不刷新缓冲区导致了数据丢失,而exit将__exit进行封装了,便得到更加安全,保全了数据
等待子进程退出->获取子进程返回值->释放子进程资源->防止僵尸进程产生->防止资源泄漏
pid_t wait (int* status)
功能:阻塞父进程一直等待子进程的退出,当子进程退出后,父进程才脱离阻塞继续推进
WIFEXITED(int status)
该宏是用于检测进程是否正在退出:
宏值若为0代表非正常退出,若为非0则代表正常退出
代码演示:
#include
#include
#include
#include
#include
int main()
{
pid_t fpid = fork();
int status = 0;
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
printf("Child:[%d]\n",getpid());
exit(3);
}
else
{
printf("Father:[%d]\n",getpid());
pid_t ret_pid = wait(&status);
if(WIFEXITED(status))
printf("%d has exited with return code %d\n",ret_pid,WEXITSTATUS(status));
else
perror("wait");
}
return 0;
}
输出结果:
Father:[6985]
Child:[6986]
6986 has exited with return code 3
pid_t waitpid(pid_t pid, int* status, int options)
参数:
> 0 | 只等待进程ID为指定pid的子进程 |
---|---|
= -1 | 等待任何一个子进程,与wait功能一样 |
= 0 | 等待任一个【子进程组ID=父进程组ID】的子进程 |
< -1 | 等待任一个【子进程组ID= |父进程组ID|】的子进程 (||意为绝对值) |
WNOHANG | 指定子进程结束才阻塞父进程,返回值为0 |
---|---|
0 | 与wait相同,阻塞父进程,一直等待子进程退出 |
WUNTRACED | 子进程若处于暂停状态:立刻返回 子进程若处于结束状态:不予理会 |
> 0 | 已经结束的子进程的pid |
---|---|
0 | 使用options为WUNTRACED且无子进程退出 |
-1 | 调用出错(例如:无子进程) |
功能:非阻塞父进程版本wait
wait中其实调用了waitpid
代码测试:
#include
#include
#include
#include
#include
int main()
{
pid_t fpid = fork();
pid_t ret_waitpid = 0;
int status;
if(fpid < 0)
perror("fork");
else if(fpid == 0) //子进程
{
printf("Child:%d\n",getpid());
sleep(3);
exit(0); //子进程退出
}
else //父进程
{
while(!ret_waitpid) //循环waitpid等待子进程的退出
{
ret_waitpid = waitpid(fpid,&status,WNOHANG);
if(ret_waitpid == 0)
{
printf("%d has not exit\n",fpid); //子进程未退出就打印
sleep(1);
}
}//while结束:说明此事ret_waitpid接收到了子进程exit后返回的pid
if(ret_waitpid == fpid)
printf("%d has exited with return code:%d\n",fpid,status);
}
return 0;
}
输出结果:
9311 has not exit
Child:9311
9311 has not exit
9311 has not exit
9311 has exited with return code:0
创建子进程的目的是为了完成其他的事情,完成其他任务,这个时候就用到了程序替换
辅助代码:子进程需要做的任务,可执行文件名为child_task,绝对路径为:/home/gongruiyang/ClassLinunx/pidTest/child_task
#include
#include
#include
void child_task(int argc, char* argv[])
{
for(int i = 0; i < argc; i++)
printf("argv[%d] = %s\n",i,argv[i]);
}
//main函数有俩参数:argc表示程序的运行参数个数,argv用于保存程序参数
int main(int argc,char* argv[])
{
child_task(argc,argv );
return 0;
}
int execl(const char *path, const char *arg, ...);
代码测试:
#include
#include
#include
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
execl("/home/gongruiyang/ClassLinunx/pidTest/child_task","-a","-b");
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
输出结果:
[gongruiyang@localhost pidTest]$ ./exectest
Father Do!
argv[0] = -a
argv[1] = -b
int execv(const char *path, char *const argv[]);
代码测试:
#include
#include
#include
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
char* child_argv[32];
child_argv[0] = "-a";
child_argv[1] = "-b";
child_argv[2] = "-c";
child_argv[3] = NULL; //必须要有
execv("/home/gongruiyang/ClassLinunx/pidTest/child_task",child_argv)
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
输出结果:
Father Do!
argv[0] = -a
argv[1] = -b
argv[2] = -c
加了p之后,其中path可以不填绝对路径,该函数会去PATH环境变量中寻找
int execlp(const char *path, const char *arg, ...);
代码测试:
#include
#include
#include
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
execlp("ls","ls","-l",NULL); //在环境变量中寻找ls并执行
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
输出结果:
Father Do!
总用量 144
-rwxrwxr-x. 1 gongruiyang gongruiyang 8561 12月 30 17:33 child_task
-rw-rw-r--. 1 gongruiyang gongruiyang 348 12月 30 17:34 child_task.c
............................................................
int execvp(const char *path, char *const argv[]);
测试代码:
#include
#include
#include
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
char* child_argv[10];
child_argv[0] = "ls";
child_argv[1] = "-l";
child_argv[2] = NULL;
execvp("ls",child_argv);
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
结果输出:
Father Do!
总用量 144
-rwxrwxr-x. 1 gongruiyang gongruiyang 8561 12月 30 17:33 child_task
-rw-rw-r--. 1 gongruiyang gongruiyang 348 12月 30 17:34 child_task.c
辅助程序:打印环境变量
#include
#include
int main(int argc,char* argv[],char* env[])
{
printf("env-var:");
for(int i = 0 ; env[i] != NULL; i++)
printf("env[%d]:%s\n",i,env[i]);
return 0;
}
int execle(const char *path, const char *arg,..., char * const envp[]);
int execvpe(const char *path, char *const argv[], char *const envp[]);
加了e之后,相比于之前,增加了一个环境变量参数,可以使用自定义环境变量
测试代码:
#include
#include
#include
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
char* child_env[10];
child_env[0] = "MYVAL=1000";
child_env[1] = "TMP=12";
child_env[2] = NULL;
execvpe("/home/gongruiyang/ClassLinunx/pidTest/envtest",NULL,child_env);
//execvle("/home/gongruiyang/ClassLinunx/pidTest/envtest",NULL,child_env);
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
输出结果:
Father Do!
env-var:env[0]:MYVAL=1000
env[1]:TMP=12
用于保存想要执行的可执行文件绝对路径
int execvpe(const char *path, char *const argv[], char *const envp[]);
加了e之后,相比于之前,增加了一个环境变量参数,可以使用自定义环境变量
测试代码:
#include
#include
#include
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
char* child_env[10];
child_env[0] = "MYVAL=1000";
child_env[1] = "TMP=12";
child_env[2] = NULL;
execvpe("/home/gongruiyang/ClassLinunx/pidTest/envtest",NULL,child_env);
//execvle("/home/gongruiyang/ClassLinunx/pidTest/envtest",NULL,child_env);
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
输出结果:
Father Do!
env-var:env[0]:MYVAL=1000
env[1]:TMP=12