1、前言
操作系统借助于进程来管理计算机的软、硬件资源,支持多任务的并行执行。而操作系统最核心的概念就是进程。以下对进程相关的学习内容进行简单记录。
2、进程概述
2.1、进程概念
进程是操作系统资源管理的最小单位,简单地讲就是运行中的程序。Linux系统可以同时启动多个进程。
进程和程序的区别:
进程:是动态的,运行中的程序;
程序:是静态的,一些保存在硬盘上的可执行的代码。
进程和线程关系:
计算机为在同一时间执行更多任务,在进程内部又划分了许多线程。线程是比进程更小的能独立运行的基本单位。一个线程可以创建和撤销另一个线程,同一个进程中可以有多个线程并行执行。
LInux下可通过命令 ps
或 pstree
查看当前系统中的进程。
2.2、进程标识
Linux操作系统中,每个进程都是通过唯一的进程id标识的。而这个id是一个非负数,每个进程除了id还有其他的标识信息,可以通过相应的函数获得。这些函数在unistd.h
头文件中。
pid_t getpid(id) 获得进程id
pdi_t getppid(id) 获得进程父进程的id
pid_t getuid() 获得进程的实际用户id
pid_t geteuid() 获得进程的有效用户id
pid_t getgid() 获得进程的实际组id
pid_t getegid() 获得进程的有效组id
实际用户id: 标识运行该进程的用户。
有效用户id: 标识以什么用户身份来运行进程。
实际组id: 它是实际用户所属的组id
有效组id:有效组id是有效用户所属组的id
2.3、进程结构
- 代码段:存放程序的可执行代码。
- 数据段:存放程序的全局变量、常量、静态变量。
- 堆栈段:
- 堆用于存放动态分配的内存变量。
- 栈用于函数调用,存放函数的参数,内部定义的局部变量。
2.4、进程状态
-
运行状态R:进程正在运行或在运行队列中等待运行。
-
可中断等待状态S:进程正在等待某个事件完成,等待过程中可以被信号或定时器唤醒。
-
不可中断等待状态D:进程也在等待某个事件完成,在等待中不可以被信号或定时器唤醒,必须等待直到等待的事件发生。
-
僵死状态Z:进程已终止,但进程描述符依然存在,直到父进程调用
wait()
函数后释放。 -
停止状态T:进程因为收到SIGSTOP、SIGSTP、SIGTIN、SIGTOU信号后停止运行或者该进程正在被跟踪(调试程序时,进程处于被跟踪状态)。
运行时还会有一些后缀字符,其意义分别为<(高优先级进程),N(低优先级队列),L(内存锁页,即页不可以被换出内存),s(该进程为会话首进程),l(多线程进程),+(进程位于前台进程组)。
2.5、进程控制
Linux进程控制包括创建进程、执行新程序、退出进程及改变进程优先级等。
在Linux系统中,用于对进程进行控制的主要系统调用以下所示:
- fork:用于创建一个新进程
- exit:用于终止进程
- exec:用于执行一个应用程序
- wait:将父进程挂起,等待子进程终止
- getpid: 获取当前进程的ID
- nice: 改变进程的优先级
3、进程操作
3.1、创建进程
3.1.1方式
创建进程有两种方式,一种是由操作系统创建,二是由父进程创建。
由操作系统创建的进程。它们是平等的,不存在资源继承关系。而对于父进程创建的进程他们和父进程存在隶属关系。子进程又可以创建进程,这样形成了一个进程家族。子进程可以继承其父进程几乎所有的资源。在系统启动的时候会,操作系统会创建一些进程,它们承担着管理和分配系统资源的任务,这些进程通常被称为系统进程。
系统调用
fork
是创建一个新进程的唯一方法,除了极少数的方式创建的进程,如init
进程,它是内核启动时以特殊方式创建的。进程调用fork
函数就是创建了一个子进程。
3.1.2、返回值
fork
函数调用的时候会返回两个值,实际上调用成功后,当前进程分裂为两个进程,一个是原来的父进程,另一个是刚刚创建的子进程。父子进程在调用fork
函数的地方分开,fork
函数有两个返回值,一个是函数的父进程调用fork
函数后的返回值,这个返回值是刚刚创建的子进程的ID,另一个是子进程中fork
函数的返回值,这个返回值是0。fork
函数返回两个值的前提是进程创建成功,,如果失败只会返回-1。
例如 fork.c
#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid; //fpid表示fork函数的返回值
int count = 0;
fpid = fork();
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0)
{
printf("I am the child process, my process id is %d\n", getpid());
printf("我是爹的儿子\n");
count++;
}
else
{
printf("I am the parent process, my process id is %d\n", getpid());
printf("我是孩子他爹\n");
count++;
}
printf("统计结果是: %d\n", count);
return 0;
}
运行结果如下:
wyw@linux:~/linux_c$ ./fork
I am the parent process, my process id is 36738
我是孩子他爹
统计结果是: 1
I am the child process, my process id is 36739
我是爹的儿子
统计结果是: 1
注意:用
fork
创建子进程后,父子进程的执行顺序是不确定的。
属性继承
fork
创建的子进程会继承父进程的很多属性,主要包括用户ID、组ID、当前工作目录、根目录、打开的文件、创建的文件使用的屏蔽字、信号屏蔽子、上下文环境、共享存储段、资源限制等。
3.1.3、孤儿进程
孤儿进程就是一个子进程的父进程先于子进程结束,子进程就成为了一个孤儿进程,它由init
进程收养,成为init进程的子进程。
3.1.4、vfork函数
vfork
也可以创建新进程,与fork
相比,它有自己独特的用处。
vfork
和fork
一样都是调用一次,返回两次。- 使用
fork
创建一个子进程时,子进程只是完全复制父进程的资源。这样得到的子进程独立于父进程,并具有良好的并发行。而使用vfork
创建一个子进程时,操作系统并不将父进程的地址空间完全复制到子进程,用vfork
创建的子进程共享父进程的地址空间,也就是子进程完全运行在父进程地址空间上,子进程对地址空间任何修改对父进程都是可见的。 fork
创建的子进程,哪个进程先运行取决于系统的调度算法,但是vfork
一个进程时,保证子进程先运行,当他调用exec
或者exit
之后,父进程才可能被调度运行。如果在exiec
或者exit
之前要依赖父进程的某个行为,就会导致死锁。- 同时
fork
创建一个子进程所有父进程的资源都要复制,有时候子进程只是调用一个exec
,所以这样会浪费大量的系统资源。而vfork
不会拷贝父进程的地址空间,这样减小了系统开销。
例如:vfork.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int a = 10;
int main(int argc, char *argv[])
{
pid_t pid;
int b = 20;
pid = vfork(); // 创建进程
if(pid < 0){ // 出错
perror("vfork");
}
if(0 == pid){ // 子进程
sleep(3); // 延时 3 秒
printf("i am son\n");
a = 100, b = 200;
printf("son: a = %d, b = %d\n", a, b);
_exit(0); // 退出子进程,必须
}else if(pid > 0){ // 父进程
printf("i am father\n");
printf("father: a = %d, b = %d\n", a, b);
}
return 0;
}
运行结果如下:
wyw@linux:~/linux_c$ ./vfork
i am son
son: a = 100, b = 200
i am father
father: a = 100, b = 200
说明:上面的代码,已经让子进程延时 3 s, 结果还是子进程运行结束后,父进程才执行。
子进程修改 a, b 的值,会影响到父进程的 a, b,
注意:用 vfork() 创建进程,子进程里一定要调用 exec(进程替换) 或 exit(退出进程),否则,程序会出问题,没有意义。
3.2、创建守护进程
守护进程(daemon)是指在后台运行的、没有控制终端与之相连的进程。他独立于控制终端,通常周期性地执行某种任务。Linux大多数服务器就是用守护进程方式实现的,类似windows的系统服务。
会话和进程组
进程组是一组相关进程的集合,会话是一组相关进程组的集合。
当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。在登录shell时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。
通常,会话开始于用户登录,终止于用户退出,期间的所有进程都属于这个会话。一个会话一般包含一个会话首进程、一个前台进程组和一个后台进程组,控制终端可有可无;此外,前台进程组只有一个,后台进程组可以有多个,这些进程组共享一个控制终端。
创建步骤:
- fork()创建子进程,父进程exit()退出
- 在子进程调用setsid()创建新会话
- 再次 fork() 一个子进程,父进程exit退出
- 在子进程中调用chdir()让根目录“/”成为子进程的工作目录
- 在子进程中调用umask()重设文件权限掩码为0
- 在子进程中调用close()关闭不需要的文件描述符
- 守护进程退出处理
例如:daemon.c
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/syslog.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int init_daemon(void)
{
int pid;
int i;
// 1)屏蔽一些控制终端操作的信号
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP ,SIG_IGN);
// 2)在后台运行
if( pid=fork() ){ // 父进程
exit(0); //结束父进程,子进程继续
}else if(pid< 0){ // 出错
perror("fork");
exit(EXIT_FAILURE);
}
// 3)脱离控制终端、登录会话和进程组
setsid();
// 4)禁止进程重新打开控制终端
if( pid=fork() ){ // 父进程
exit(0); // 结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
}else if(pid< 0){ // 出错
perror("fork");
exit(EXIT_FAILURE);
}
// 5)关闭打开的文件描述符
// NOFILE 为 <sys/param.h> 的宏定义
// NOFILE 为文件描述符最大个数,不同系统有不同限制
for(i=0; i< NOFILE; ++i){
close(i);
}
// 6)改变当前工作目录
chdir("/");
// 7)重设文件创建掩模
umask(0);
// 8)处理 SIGCHLD 信号
signal(SIGCHLD,SIG_IGN);
return 0;
}
int main(int argc, char *argv[])
{
init_daemon();
while(1);
return 0;
}
运行结果如下:
3.3、进程退出
方式:
- 正常退出
- 在main函数中执行return
- 调用
exit
函数 (头文件stdlib.h
) - 调用
_exit
函数 (头文件unistd.h
)
- 异常退出
- 调用
about
函数 - 进程收到某个信号,该信号使程序终止。
- 调用
3.4、改变进程的优先级
改变进程的优先级是通过系统调用nice
函数改变。
man 2 nice
命令获取该函数的声明:
#include <unistd.h>
int nice(int inc);
相关的两个重要函数:
getpriority 该函数返回一组进程的优先级。
setpriority 该函数用来设置制定进程的优先级。
3.5、setuid函数
可以用 setuid
设置实际用户 ID 和有效用户 ID 。
内核检查一个进程是否具有访问某文件的权限时,是使用进程的有效用户 ID 来进行检查的。
su
程序的文件属主是 root,普通用户运行su
命令时,su
进程的权限是root权限。
注意,因为Linux系统中root用户拥有最高权力,黑客们往往喜欢寻找设置了
set_uid
位的可执行程序的漏洞。这样的程序如果存在缓冲区溢出漏洞,并且该程序是一个网络程序。那么黑客可以从远程的地方轻松的利用该漏洞获得运行该漏洞程序的主机的root权限。即使这样的程序不是网络程序,那么也可以使本机上的恶意普通用户提升为root用户。
4、结语
关于章末的编程实践:实现自己的 myshell,奈何能力不够,对我来说是很难实现了。这里依然给出学长的代码,可作日后欣赏。(滑稽-> <-)
https://github.com/Evil-crow/Linux_C/tree/master/Chapter_VII/Shell
参考资料
[1] Linux C 编程实战第七章进程控制
[2] 创建守护进程的步骤