参考资料:
[1] 《嵌入式Linux开发教程(上)周立功主编》
[2] 进程详解
1. 进程环境
多进程举例:比如同时运行QQ、微信、截图工具、视频播放器等
多进程优点:
- 每个进程互相独立,子进程崩溃不影响主程序的稳定性
- 通过增加CPU,就可以容易扩充性能
- 进程能直接获取系统的资源,总体能够达到的性能上限非常大
1.1 程序与进程
程序(program):是一个普通的文件,为了完成特定任务而准备好的指令序列与数据的集合。
进程(Process):是一个已经开始执行但还没有终止的程序实例。同一个程序可以实例化为多个进程实体,操作系统中所有进程实体共享计算机系统的CPU、外设等资源。
进程状态:进程间的状态切换
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)", /* 32 */
"x (dead)", /* 64 */
"K (wakekill)", /* 128 */
"W (waking)", /* 256 */
};
从Linux源码中可以看出,Linux下主要有7种进程状态:
- R运行状态(Running):处于运行状态的进程并不带表一定就正在被CPU调度运行,它包括了正在被CPU运行的进程和可以被CPU调度运行的程序,也就是说改状态包含了三状态模型中的就绪态和运行态。
- S睡眠状态(Sleeping):处于改状态的进程表示该进程正在等待某时间的完成,通常也称为可中断睡眠状态,该状态属于三状态模型中的阻塞态。
- D磁盘休眠状态(disk sleep):该状态也叫做不可中断睡眠状态,处于该状态的进程通常都在等待I/O操作的结束,该状态也属于三状态模型中的阻塞态。
- T停止状态(stopped):我们可以通过向进程发送SIGSTOP信号让目标进程处于停止状态,通过向处于停止状态的进程发送SIGCON信号让目标进程继续运行,该状态也属于三状态模型中的阻塞态。
- t追踪停止状态(tracing stop):
- X死亡状态(dead):该状态只是一个返回状态,不会在任务列表中见到,该状态属于退出状态。
- Z僵尸状态(zombie):当一个进程退出,但它的父进程并没有去收回该进程的信息时,该进程所处的状态叫做僵尸状态,该状态属于退出状态。
1.2. 进程环境
1.2.1 进程ID
每个进程都由一个进程号来标识,其类型为`pid_t`,进程号的范围是:`0~32767`
- 进程号总是唯一的,但是进程号可以重用。当一个进程终止后,其进程号就可以重新再次使用了
- 在linux系统中进程号由0开始
- 进程号为0和1的进程由内核创建。进程号为0的进程通常是调度进程,常被称为交换进程(swapper)。进程号为1的进程通常是init进程。除调度进程以外,在linux下面所有的进程都由nit进程直接或者间接创建。
进程号(PID:processID): 标识进程的一份非负整型数。
父进程号(PPID):任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
进程组号(PGID): 进程组是一个或者多个进程的集合。他们之间相互关联,进程可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。
Linux操作系统提供了三个获得进程号的函数:getpid()、getppid()、getpgid()。
需要包含头文件:
#include <sys/types.h>
#include <unistd.h>
函数接口:
pid_t getpid(void)
- 功能:获取本进程号(PID)
pid_t getppid(void)
- 功能:获取调用此函数的进程的父进程号(PPID)
pid_t getpgid(pid_t pid)
- 功能:获取进程组号(PGID),参数为0时返回当前的PGID,否则返回参数指定的进程PGID。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char **argv,char **env){
printf("current pid = %d\n",getpid());
printf("current's father pid = %d\n",getppid());
return 0;
}
UID和GID:Linux是一个多用户的操作系统,每个用户至少有一个用户ID(User ID,简称UID)及用户组ID(Group ID,简称GID)
1.2.2 环境变量
(1)对于main函数,int main(int argc,char **argv)或者int mian(int argc,char *argv[ ])或者int main(int argc,char **argv,char **env)中,其中argc表示参数数量,例如上面代码argc = 1;argv数组表示参数数组,故对于上面代码,argv[0] = ./pid;env指向环境变量字符串的数组。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char **argv,char **env){
printf("argc = %d\n",argc);
printf("argv[0] = %s\n",argv[0]);
printf("argv[1] = %s\n",argv[1]);
printf("env[0] = %s\n",env[0]);
printf("current pid = %d\n",getpid());
printf("current's father pid = %d\n",getppid());
return 0;
}
(2)进程在运行中,可以使用三种方式来获取运行环境的环境变量
- 通过main函数的第三个参数env来获取
- 通过environ全局变量获取,函数中直接声明extern char **environ,随后即可直接使用environ[0]、environ[1]、......
- 通过getenv( )函数获取:原型 char *getenv(const char* name); 头文件是#include <stdlib.h>。参数name是要获取的环境变量名,返回该变量的值
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("USER = %s\n",getenv("USER"));
return 0;
}
2. 进程创建的基本操作
2.1 创建进程
在Linux环境下,创建进程主要方法是调用以下两个函数:
头文件:
#include <sys/types.h>
#include <unistd.h>
- pid_t fork(void);
- pid_t vfork(void);
2.1.1. fork函数:创建一个新进程 pid_t fork(void)
功能:
- fork()函数用于从一个已经存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
- 返回值:成功:子进程返回0,父进程返回子进程ID(!= 0);失败:返回-1
说明:
- 使用fork()创建的子进程是父进程的一个复制品,他从父进程处继承了整个进程的地址空间。地址空间:包括进程上下文、进程堆栈、带卡的文件描述符、信号控制设定、进程优先级、进程组号等。
- fork:父子进程共享代码段,但是分别拥有自己的数据段和堆栈段。
- 子进程所独有的只有它的进程号、计时器等。因此,使用fork()函数的代价是很大的。
- fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。
- 子进程对变量所做的改变并不影响进程中该变量的值,说明父子进程各自拥有自己的地址空间。
- 一般来说,在fork()之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法
- 如果要求父子进程之间相互同步,则要求某种形式的进程间通信
正确范例:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("create fork()\n");
pid_t fd;
fd = fork();
int number = 0;
if(fd < 0)
printf("create errror!\n");
else if(fd == 0){
number++;
printf("son's number = %d\n",number);
printf("son is running!\n");
sleep(1);
}
else{
number++;
printf("father's number = %d\n",number);
printf("father is running!\n");
sleep(1);
}
printf("run exit!\n");
}
/****** 以下是错误范例 **********/
/*
int main(){
printf("create fork()\n");
pid_t fd;
if(fd = fork() < 0) //此处不能这么写,否则只会去执行子进程(2次),父进程无法执行
printf("create errror!\n");
else if(fd == 0){
printf("son is running!\n");
sleep(1);
}
else{
printf("father is running!\n");
sleep(1);
}
}
*/
输出:
create fork()
father's number = 1
father is running!
son's number = 1
son is running!
run exit!
run exit!
可以看到对于数据段number,在子进程和父进程分别++之后,输出都是1,因为fork的子进程是对父进程的数据段执行的拷贝操作,所以子进程对number+1不会对父进程的数据段产生影响。同样,父进程对数据段的操作也不会对子进程产生影响。
附图:fork创建进程的详细流程:
2.1.2. vfork函数:创建一个新进程:pid_t vfork(void)
功能:
- vfork()函数和fork()函数一样都是在已有的进程中创建一个新进程,但他们创建的子进程是有区别的。
- vfork()函数的子进程与父进程共享数据段。
- vfork()保证子进程先运行,在调用exec或者exit之前与父进程数据共享。在调用exec或者exit之后父进程才可能被调度。
- vofrk()保证子进程先运行,在它调用exec或者exit之后父进程才可能被调度运行,如果在调用这两个函数之前进程依赖于父进程的进一步动作,则会导致死锁。
返回值:
创建子进程成功,则在子进程中返回0,父进程中返回子进程ID。出错则返回-1。(和fork一致)
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("create fork()\n");
pid_t fd;
int number = 0;
fd = vfork();
if(fd < 0)
printf("create errror!\n");
else if(fd == 0){
number++;
printf("son's number = %d\n",number);
printf("son is running!\n");
exit(0);
}
else{
number++;
printf("father's number = %d\n",number);
printf("father is running!\n");
}
}
输出:
create fork()
son's number = 1
son is running!
father's number = 2
father is running!
可以看出子进程对数据number的操作会影响父进程的数据number,因为父子进程之间是共享数据段的。
fork和vfork的区别:
- vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。
- vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问改地址空间。相反,在子进程中调用exec或exit之前,它在父进程的地址空间运行,在exec之后子进程会有自己的进程空间
将上面的程序fork改成vfork,可以看到,在子进程中没有调用exec函数,父进程就得不到执行,如果调用了exec函数,父进程会在子进程执行了exec后执行
2.2 进程挂起
进程挂起:进程在一定的时间内没有任何动作,称之为进程的挂起
#include <unistd.h>
unsigned int sleep(unsigned int sec);
功能:
- 进程挂起指定的秒数(s),直到指定的时间用完或者收到信号才解除挂起。
- 返回值:若进程挂起到sec指定的时间返回0,若有信号中断返回剩余秒数。
- 进程挂起指定秒数后程序并不会立即执行,系统只是将此进程切换到就绪态。
2.3 进程等待
父子进程之间有时候需要简单的进程同步,比如父进程等待子进程的结束,为此,Linux下提供了以下两个等待函数:wait()、waitpid()。头文件
#include <sys/types.h>
#include <sys/wait.h>
2.3.1. pid_t wait(int *status)
功能:
- 等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
- 调用wait( )函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒。
- 若调用进行没有子进程或者它的子进程已经结束,该函数立即返回。
参数:
- 函数返回时,参数status中包含子进程退出时的状态信息。子进程的退出信息在一个int中包含了多个字段,用宏定义可以取出其中每个字段
返回值:
- 如果执行成功则返回子进程的进程号
- 出错返回-1,失败原因存于errno中
注:取出子进程的退出信息
- WIFEXTED(status):如果子进程是正常终止的,取出的字段值非零。
- WEXITSTATUS(status):返回子进程的退出状态。退出状态保存在status变量的8~16位。在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏
- 此status是个wait的参数指向的整型变量
2.3.2. 僵尸进程(Zombie Process)
进程已经结束,但进程所占用的资源未被回收,这样的进程称之为僵尸进程。
子进程已经结束,父进程未调用wait或waitpid函数回收进程的资源是子进程变为僵尸进程的原因(儿子死了,爹没给收尸)。
2.3.3. 孤儿进程(Orphan Process)
父进程运行结束,单子进程未运行结束的子进程(爹死了,儿子没人管了)。孤儿进程直接被init进程收管,由init进程负责收集他们的退出状态。
2.3.4. 守护进程(Daemon 精灵进程)
- 守护进程是一个特殊的孤儿进程(所以守护进程的父进程是init进程),这种进程脱离终端,在后台运行。
- 他独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事情,不需要用户输入就能运行并且提供某种服务。
Linux最常见的创建守护进程的方式:
头文件:
#include <unistd.h>
函数原型:
int daemon(int nochdir, int noclose);
参数说明:
- nochdir:如果传入0,则daemon函数将调用进程的工作目录设置为根目录,否则保持原有的工作目录不变
- noclose:如果传入0,则daemon函数将重定向标准输入、标准输出、标准错误到/dev/null文件中,否则不改变这些文件描述符
返回值:函数成功返回0,否则返回-1,并设置errno。
2.4 终止进程
1. 进程在退出前可以用atexit()函数注册退出处理函数
函数原型:
#include <stdlib.h>
int atexit(void (*function)(void))
功能:
- 注册进程正常结束前调用的函数,进程退出执行注册函数。
- 参数:function:进程结束前,调用函数的入口地址。
- 一个进程中可以多次调用atexit()函数注册清理函数,正常结束前调用函数的顺序和注册时的顺序相反。
2. 在Linux下可以通过以下方式结束正在运行的进程:
① exit函数:结束进程执行
#include <stdlib.h>
void exit(int value)
参数: status:返回给父进程的参数(低八位有效)
② _exit()函数:结束进行执行
#include <unistd.h>
void _exit(int value)
参数: status:返回给父进程的参数(低八位有效)
2.5 exec函数族
参考文章:linux c语言 fork() 和 exec 函数的简介和用法
NAME
execl, execlp, execle, execv, execvp, execvpe - execute a file
SYNOPSIS
#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[]);
- 虽然六个函数名字不同,但是实际上他们的功能都是差不多的,因为要用于接受不同的参数所以要用不同的名字区分它们(c语言没有函数重载的功能)
- exec函数会取代执行它的进程,也就是说,一旦exec函数执行成功,将不会返回,进程结束。但是如果exec函数执行失败,它会返回失败的信息,进程继续执行后面的代码。
- 通常exec会放在fork() 函数的子进程部分, 来替代子进程执行, 执行成功后子程序就会消失,但是执行失败的话,必须用exit()函数来让子进程退出
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **argv){
sleep(1);
printf("proc running!\n");
const char *path = "/home/ztaotao/work/pro/hello";
pid_t fd = vfork();
if(fd < 0)
printf("error!\n");
else if(fd == 0){
printf("running son proc...\n");
execlp(path,0);
printf("no return!\n");
// sleep(1);
}
else{
sleep(1);
printf("running father...\n");
//sleep(1);
}
}
输出:
proc running!
running son proc...
Hello,son exec running...
running father...
hello是hello.c的可执行文件
#include <stdio.h>
void main(void){
printf("Hello,son exec running...\n");
}
3. 信号
信号(signal)又称为软中断信号,用来通知进程发生了异步事件。进程之间可以相互发送中断信号。内核也可以因为内部时间而给进程发送信号,通知进程发生了某个事件。
3.1 常用的信号
Linux系统有30多种信号,每个信号的名称都以SIG三个字符凯欧。
定义域头文件:
#include <signal.h>
3.2 信号函数
3.2.1. sigaction函数
sigaction原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction成功返回0,否则返回-1.
参数signum指出需要改变处理方法的信号,如SIGINT信号,但是SIGKILL和SIGSTOP这两个信号是不可捕捉的
参数act和oldact都是sigaction结构体指针,act为要设置对信号的新的处理方式,而oldact为原来对信号的处理方式。sigaction结构体定义在<siganl.h>头文件中,如下:
/* sigaction结构体 */
struct sigaction {
void (* sd_handler)(int);
void (* sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (* sa_restorer)(void);
};
3.2.2. kill函数
kill()函数可以用来向指定的进程发送一个指定的信号
头文件:
#include <sys/types.h>
#include <signal.h>
函数原型:
int kill(pid_t pid, int sig);
返回值及参数:
kill()函数成功返回0,否则返回-1.参数pid为发送sig信号的进程PID,参数sig为发送的信号