一、进程
1、进程的定义
程序:程序是存放在存储介质(例如磁盘)上的一个可执行文件
进程:进程是程序执行的实例,包括程序计数器、寄存器和变量的当前值
2、进程的状态及转换
按生命周期划分为三种状态:
就绪态:进程已经具备执行的一切条件,等待CPU的时间片
执行态:进程正在占用CPU
等待态:进程因不具备某些执行条件而暂时无法继续执行
进程的调度机制:
时间片轮转,上下文切换
进程的转换
3、进程控制块
操作系统是根据PCB(process control block)来对并发执行的进程进行控制和管理的。
系统在创建一个进程的时候会开辟一段内存空间存放与此进程相关的PCB数据结构
Linux中PCB存放在task_struct结构体中,可以在sched.h中查到该结构体
二、进程控制
1、进程号
每个进程都由一个进程号来标识,类型为pid_t,进程号的范围是:0~32767
0号进程是调度进程,用于交换
1号进程是init进程,是所有进程的祖先
Linux系统中使用ps ajx可以查询到进程的信息,类似于Windows的任务管理器
主要参数有
PPID:父进程ID
PID:进程ID
PGID:进程组ID
COMMAND:进程名
Linux操作系统提供了3个获得进程号的函数getpid()、getppid()、getpgid()
#include <sys/types.h>
#include <unistd.h>
包含这两个头文件即可调用
pid_t getpid(void);
pid_t getppid(void);
pid_t getpgid(pid_t pid);
//案例演示
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("pid = %d\n", getpid());
printf("ppid = %d\n", getppid());
printf("pgid = %d\n", getpgid(getpid()));
while (1)
;
return 0;
}
/*
运行结果:
pid = 7406
ppid = 7349
pgid = 7406
最后使用Ctrl+c 结束进程
*/
2、进程的创建(fork)
在Liunx下,创建进程的主要方法是调用以下两个函数:
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
pid_t vfork(void);
功能:
fork()函数用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程为父进程
返回值:
成功:子进程中返回0,父进程返回子进程ID;失败返回-1,返回值如何应用后续会介绍
使用fork函数得到的子进程是父进程的一个复制品,直接继承整个父进程的地址空间(栈区堆区BSS区数据区代码区),因此使用fork函数的的代价是很大的。
//进程的创建实例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//使用fork函数创建进程
fork();
printf("Hello World\n");
//结果会打印两次Hello World,表明进程创建了,验证后使用Ctrl+c终止进程
//因此需要注意,执行一次fork,就会在原有的进程中创建一个新进程,如果fork之后不进行区分父子进程,则后面的代码都会执行
while (1)
;
return 0;
}
//那么如何进行父子进程的区分呢?通过返回值,以下是实例验证
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork!\n");
return -1;
}
else if (pid > 0) //父进程代码区
{
while (1)
{
printf("父进程运行中\n");
sleep(1);
printf("*************\n");//此行代码可用于验证父子进程是交替运行的,谁先谁后是不确定的
}
}
else //子进程代码区
{
while (1)
{
printf("子进程运行中\n");
sleep(1);
printf("--------------\n");
}
}
return 0;
}
//父子进程拥有独立的地址空间
//子进程会复制父进程fork之前的所有内容
//但是fork之后,父子进程完全独立,无论堆区、栈区、数据区如何改变,都不受对方影响;如果是对文件操作的话,会受对方影响
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int gVar = 10;
int main(int argc, char const *argv[])
{
pid_t pid;
static int s_s32Var = 11;
int s32Var = 12;
pid = fork();
if (pid < 0)
{
perror("fail to fork!\n");
return -1;
}
else if (pid > 0) //父进程代码区
{
printf("父进程运行中\n");
gVar++;
s_s32Var++;
s32Var++;
printf("gVar=%d,s_s32Var=%d,s32Var=%d\n", gVar, s_s32Var, s32Var);
}
else //子进程代码区
{
sleep(1); //进程睡眠1s,让父进程运行完毕
printf("子进程运行中\n");
printf("gVar=%d,s_s32Var=%d,s32Var=%d\n", gVar, s_s32Var, s32Var);
}
while (1)
;
return 0;
}
//子进程和父进程有一些公有区域,例如磁盘空间,内核空间
//文件描述符的偏移量保存在内核空间中,所以父进程改变偏移量,则子进程获取的偏移量是改变之后的
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
int fd;
if ((fd = open("test.txt", O_RDONLY)) == -1) //test.txt内容自定义,建议50bits
{
perror("fail to open test.txt\n");
return -1;
}
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork!\n");
return -1;
}
else if (pid > 0) //父进程代码区
{
printf("父进程运行中\n");
char buf[50] = "";
if (read(fd, buf, 32) == -1)
{
printf("fail to read\n");
return -1;
}
printf("buf = [ %s ]\n", buf);
}
else //子进程代码区
{
sleep(1); //进程睡眠1s,让父进程运行完毕
printf("子进程运行中\n");
char buf[50] = "";
//取消lseek的注释就可以把偏移量设置为文件开头
//lseek(fd, 0, SEEK_SET);
if (read(fd, buf, 32) == -1)
{
printf("fail to read\n");
return -1;
}
printf("buf = [ %s ]\n", buf);
}
while (1)
;
return 0;
}
3、进程的挂起
进程在一定的时间内没有任何动作,称为进程的挂起(man 3 sleep)
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
功能:
进程挂起指定的秒数,直到指定的时间用完或收到信号才解除挂起。
返回值:
若进程挂起到sec指定的时间则返回0,若有信号中断则返回剩余秒数
进程挂起到指定秒数后,进程状态切换到就绪态
4、进程的等待
父子进程有时候需要简单的进程间同步,例如父进程等待子进程结束
Linux下提供了以下两个等待函数wait()、waitpid()----使用man 2 wait查询具体信息
需要包含头文件
#include <sys/types.h>
#include <sys/wait.h>
wait函数:
pid_t wait(int *status);
功能: 等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
调用wait函数的进程会挂起,直到他的一个子进程退出或收到一个不能被忽视的信号时才被唤醒
若调用进程没有子进程或它的子进程已经结束,该函数立即返回
参数: 函数返回时,参数status中包含子进程退出时的状态信息。
子进程的退出信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段
返回值: 如果执行成功则返回子进程的进程号。
出错返回-1,失败原因可以在errno中查找
取出子进程的退出信息
WIFEXITED(status)
returns true if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning from main().
如果子进程是正常终止的,取出的字段非零,子进程可以通过exit或者_exit函数发送退出状态
WEXITSTATUS(status)
returns the exit status of the child. This consists of the least significant 8 bits of the status argument that the child specified in a call to
exit(3) or _exit(2) or as the argument for a return statement in main(). This macro should be employed only if WIFEXITED returned true.
返回子进程的退出状态,退出状态保存在status变量的8~16位。在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork!\n");
return -1;
}
else if (pid > 0) //父进程代码区
{
#if 0
wait(NULL); //等待子进程运行结束后再运行父进程
printf("进入父进程\n");
#endif
//如果通过接收子进程的退出状态来执行父进程,子进程就要使用exit或_exit函数退出进程时发送退出状态
int status, ret;
wait(&status);
ret = WIFEXITED(status);
printf("ret = %d \n", ret);
if (ret != 0) //子进程正常退出
{
printf("子进程返回值:%d\n", WEXITSTATUS(status));
}
printf("父进程结束运行\n");
}
else //子进程代码区
{
printf("进入子进程\n");
for (int i = 0; i < 2; i++)
{
printf("子进程运行中\n");
sleep(1);
}
}
return 0;
}
/*
waitpid函数(拓展了解,不要求掌握)
pid_t waitpid(pid_t pid, int *status, int options);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源
参数
pid的值:
pid>0:等待进程ID等于pid的子进程
pid=0:等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会等待他
pid=-1:等待任一子进程,此时waitpid和wait作用一样
pid<-1:等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值
status参数中包含子进程退出时的状态信息
options参数能进一步控制waitpid的操作:
0--同wait,阻塞父进程,等待子进程退出。
WNOHANG--没有任何已经结束的子进程,则立即返回
WUNTRACED--如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态
返回值:
执行状态改变了的子进程的ID;如果设置了WNOHANG且pid指定的进程存在则返回0
出错返回-1,当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD
*/
5、进程的终止
僵尸进程(Zombie Process):
进程已经运行结束,但进程占用的资源未被回收,这样的进程称为僵尸进程
子进程已运行结束,父进程未调用wait函数回收子进程的资源是子进程变为僵尸进程的原因
孤儿进程(Orphan Process):
父进程运行结束,但子进程未运行结束的子进程
守护进程(Daemon Process):
守护进程是个特殊的孤儿进程,这种进程脱离中断,在后台运行
在Linux中,可以通过以下方式结束正在运行的进程:
void exit(int value);
void _exit(int value);
exit函数:
#include <stdlib.h>
void exit(int status);
status是返回给父进程的参数
_exit函数:
#include <unistd.h>
void _exit(int status);
status也是返回给父进程的参数
那么两者有什么区别呢?
exit为库函数,而_exit为系统调用
exit会刷新缓冲区,但是_exit不会刷新缓冲区
return 与exit、_exit的区别:
return在子函数中使用只能退出当前函数,无法终止进程
exit退出进程且可以刷新缓冲区
_exit仅退出进程
6、进程退出清理
进程在退出前可以用atexit函数注册退出处理函数
#include <stdlib.h>
int atexit(void(*function)(void));
功能:进程结束时调用的进程退出执行注册函数
参数function:进程结束前,调用函数的入口地址。一个进程中可以多次调用atexit函数注册清理函数,正常结束前调用函数的顺序和注册时的顺序相反。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void clear1(void)
{
printf("clear1 func excute\n");
}
void clear2(void)
{
printf("clear2 func excute\n");
}
void clear3(void)
{
printf("clear3 func excute\n");
}
int main(int argc, char const *argv[])
{
//atexit函数在进程结束才会调用
//多个atexit函数进行调用,执行顺序与调用顺序相反
atexit(clear1);
atexit(clear2);
atexit(clear3);
printf("process sleep for 3 sec\n");
sleep(3);
return 0;
}
7、进程的创建–vfork函数
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:vfork函数和fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的
返回值:创建子进程成功,则在子进程中返回0,父进程中返回子进程ID,出错返回-1
fork函数和vfork函数的区别:
vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。
vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是就不访问地址空间了
相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
//使用vfork创建完子进程后,子进程会先执行,直到子进程运行exit或者exec后,父进程才会执行
pid_t pid;
pid = vfork();
if (pid < 0)
{
printf("fail to vfork\n");
exit(1);
}
if (pid == 0) //子进程
{
for (int i = 0; i < 2; i++)
{
printf("son process excute\n");
sleep(1);
}
exit(0);
}
else //父进程
{
printf("father process excute\n");
}
return 0;
}
//在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间 这句话的验证实例如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int gVar = 10;
int main(int argc, char const *argv[])
{
pid_t pid;
static int s_s32Var = 11;
int s32Var = 12;
pid = vfork();
if (pid < 0)
{
perror("fail to vfork!\n");
exit(1);
}
else if (pid == 0) //子进程代码区
{
printf("子进程运行中\n");
gVar++;
s_s32Var++;
s32Var++;
printf("gVar=%d,s_s32Var=%d,s32Var=%d\n", gVar, s_s32Var, s32Var);
sleep(1);
exit(1);
}
else //父进程代码区
{
printf("父进程运行中\n");
sleep(1);
printf("gVar=%d,s_s32Var=%d,s32Var=%d\n", gVar, s_s32Var, s32Var);
}
return 0;
}
8、进程的替换
进程的替换
exec函数族,是由六个exec函数组成的
1、exec函数族提供了六种在进程中启动另一个程序的方法
2、exec函数族可以根据指定的文件名或目录名找到可执行文件
3、调用exec函数的进程并不创建新的进程,故调用exec前后,进程的进程号并不会改变,其执行的程序完全由新的程序替换,而新程序则从其main函数开始执行
exec函数族取代调用进程的数据段、代码段和堆栈段
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, 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[]);
六个函数中只有execvpe是真正意义上的系统调用,其他函数都是在此基础上封装的库函数
l即list,参数地址列表,以空指针结尾
v即vector,存有各参数地址的指针数组的地址
p即path,按PATH环境变量指定的目录搜索可执行文件
e即environment,存有环境变量字符串地址的指针数组的地址
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
int main(int argc, char const *argv[])
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork!\n");
exit(1);
}
else if (pid == 0) //子进程
{
printf("This is the child process!\n");
//whereis 命令可以查路径
//不带p的函数,命令的路径一定要写绝对路径
#if 0
if (execl("/bin/ls", "ls", "-l", NULL) == -1)
{
printf("fail to execl\n");
exit(1);
}
#endif
#if 0
if (execlp("ls", "ls", "-al", NULL) == -1)
{
printf("fail to execlp\n");
exit(1);
}
#endif
#if 1
char *str[] = {
"ls", "-l", NULL};
if (execv("/bin/ls", str) == -1)
{
printf("fail to execv!\n");
exit(1);
}
#endif
//除了shell命令外,参数还可以可执行文件/shell脚本,这个试着自己写一下,eg:execl("./hello", "./hello", NULL);
//用于验证 exec函数族取代调用进程的数据段、代码段和堆栈段
//当exec函数执行完毕后,当前进程就结束了,所以下面的HelloWorld打印不会出现
printf("Hello World\n");
}
else //父进程
{
printf("This is the parent process!\n");
wait(NULL);
printf("The child process has been quited!\n");
}
return 0;
}