大学生寒假在家过于无聊,整理一下以前学过的知识,顺便复习一下,水平较低,专业性差,仅供参考,不喜勿喷(反正也没人看)。尽量一两天更一篇。
一、程序和进程 (1)概念
程序(program)是存放在磁盘文件中的可执行文件。
程序的执行实例被称为进程(process),它是系统拥有资源的基本单位,是操作系统结构 的基础。
(2)区别 程序
·程序是可执行文件,存放在磁盘文件中
·程序是静态存储的
·根据用户操作执行
·利用操作系统的文件管理系统进行管理
进程
·动态储存在内存中,双击程序后产生
·Linux内核的PCB(进程控制模块)进行管理
·内核提供函数进行访问,存在生命周期
·每个linux进程都一定有一个唯一的数字标识符,称为进程 ID(PID),进程ID总是一非负整数
·进程可以有多个,但进程ID不同;程序只有一个
(3)PCB
·为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。
·PCB中记录了操作系统所需的,用于描述进程的当前情况以及控制进程运行的全部息。
程序:程序段+数据段
进程实体:程序段+数据段+PCB
二、进程状态
许多操作系统教科书上用的是三态模型(运行态,就绪态,阻塞态)或五态模型(新建态、终止态,运行态,就绪态,阻塞态),这里我所讲的进程状态特指linux下的6种进程状态。
·运行状态R(TASK_RUNNING)
·可中断睡眠状态S(TASK_INTERRUPTIBLE)
·不可中断睡眠状态D(TASK_UNINTERRUPTIBLE)
·暂停状态T(TASK_STOPPED或TASK_TRACED)
·僵尸状态Z(TASK_ZOMBIE)
·退出状态X(TASK_DEAD)
(1)运行状态R
只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。用一句话来说,正在CPU上运行的进程与正在等待CPU资源调度的进程都属于这个状态。
(2)可中断睡眠状态S
处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。用一句话来说,正在等待某些事件发生的进程就属于这个状态,如果事件发生,就变成运行状态,放入CPU的可执行队列中。
(3)不可中断睡眠状态D
和S状态类似,也处于睡眠状态,但是不可中断。这个状态存在意义在于它可以作为一个保护机制,保护内核的某些处理流程可以顺畅进行。比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互,这时候进程变为D状态,以避免进程与设备交互的过程被打断。用一句话来说,就是一种有保护机制的睡眠状态,无法被打断。
(4)暂停状态T
这个状态有两种情况:第一种情况,向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态(除非该进程本身处于TASK_UNINTERRUPTIBLE状态而不响应信号)。向进程发送一个SIGCONT信号,可以让其从TASK_STOPPED状态恢复到TASK_RUNNING状态。第二种情况,这个进程被其他进程追踪,它处于TASK_TRACED这个特殊的状态,等待跟踪它的进程对它进行操作。用一句话来说,第一种情况,进程收到了暂停的信号,停止下来;第二种情况,被追踪,在等待其他进程对它进行操作。
(5)僵尸状态Z
进程在退出的过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸,在这种状态的进程被称为僵尸进程(下文具体会讲它如何产生的)。
(6)退出状态X
而进程在退出过程中也可能不会保留它的task_struct。进程彻底消失,消失前会把自己的PID传给父进程,于是我们就能在进程列表中看到它的状态。
进程状态转换图
三、init进程所谓的init进程,它是一个由内核启动的用户级进程。内核自行启动(已经被载入内存,开始运行,并已初始化所有的设备驱动程序和数据结构等)之后,就通过启动一个用户级程序init的方式,完成引导进程。所以,init始终是第一个进程(其进程编号始终为1)。
四、父子进程
在Linux里,除了进程0(即PID=0的进程)以外的所有进程都是由其他进程使用系统调用fork创建的,这里调用fork创建新进程的进程即为父进程,而相对应的为其创建出的进程则为子进程,因而除了进程0以外的进程都只有一个父进程,但一个进程可以有多个子进程。
(1)获取进程标识
#include
#include
pid_t getpid(void); 返回:调用进程的进程I D
pid_t getppid(void); 返回:调用进程的父进程I D
uid_t getuid(void); 返回:调用进程的实际用户I D
uid_t geteuid(void); 返回:调用进程的有效用户I D
gid_t getgid(void); 返回:调用进程的实际组I D
gid_t getegid(void); 返回:调用进程的有效组I D
(2)fork()
#include
#include
pid_t fork(void);
返回:子进程中为0,父进程中为子进程I D,出错为-1
这个函数需要注意的是,调用一次,返回两次,当父进程fork()创建子进程失败时,fork()返回-1,当父进程fork()创建子进程成功时,此时,父进程会返回子进程的pid,而子进程返回的是0。所以可以根据返回值的不同让父进程和子进程执行不同的代码。
代码示例
#include
fork调用注意点:
·fork之后的代码在两个进程中运行,但是在f o r k之后是父进程先执行还是子进 程先执行是不确定的。这取决于内核所使用的调度算法。
·使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,拥有和父 进程绝大部分的资源(小部分不继承)。
·如果父进程先退出,子进程还没退出那么子进程的父进程将变为init进程。
·如果子进程先出,父进程还没退出,那么子进程要退出必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。
(3)子进程与父进程的区别
1、父进程设置的锁,子进程不继承
2、各自的进程ID和父进程ID不同
3、子进程的未决告警被清除
4、子进程的未决信号集设置为空集
(4)exec函数组
用exec函数可以把当前进程替换为一个新进程。exec名下是由多个关联函数组成的一个完整系列
包含头文件
原型
int execl(const char *path, const char *arg, ...);// 需要些路径
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg , ..., char * const envp[]);//char * const envp[]环境变量
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
参数
path参数表示你要启动程序的名称包括路径名
arg参数表示启动程序所带的参数
返回值:成功返回0,失败返回-1
注意点:
·execl,execlp,execlv的参数个数是可变的,参数以一个空指针结束。
·execv和execvp的第二个参数是一个字符串数组,新程序在启动时会把在argv数组中 给定的参数传递到main
·这些函数通常都是用execve实现的,这是一种约定俗成的做法,并不是非这样不可。
·名字最后一个字母是“p”的函数会搜索PATH环境变量去查找新程序的可执行文件。 如果可执行文件不在PATH定义的路径上,就必须把包括子目录在内的绝对文件名做为 一个参数传递给这些函数
·由exec启动的新进程继承了原进程的许多东西
代码示例
#include
运行结果
从结果可以看到代码没有执行最后一行,这说明这个进程已经被替换成ls命令了。
(5)system函数
功能:可以让一个程序在另一个程序的内部运行,也就是说,我们创建了一个新的进程。这个工作可以通过库函数system来实现。
包含头文件
原型:
int system(const char *string);
参数
String是你要启动程序的名称
返回值: 如果无法启动shell运行命令,system将返回“127”;出现不能执行system调用的其他错误时返回“-1”。如果system能够顺利执行,返回那个命令的退出码。
代码示例
#include
运行结果
可以看到,执行完ls的命令后,还打印了Done.,在这个进程中创建了一个新的进程去执行ls的操作,这样之后再继续向下执行代码。
局限性
·System函数远非是启动其他进程的理想手段,因为它必须用一个shell来启动预 定的程序。
·对shell的安装情况和它所处的环境的依赖也很大。
·效率很低。
(6)wait和waitpid函数
wait函数用于使父进程阻塞,直到一个子进程结束或者该进程接收到一个指定信号为止。
调用wait或waitpid的进程可能会:
·阻塞(如果其所有子进程都还在运行)。
·带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状 态)。
·出错立即返回(如果它没有任何子进程)。
wait函数说明
#include
#include
pid_t wait(int * status) ;
两个函数返回:若成功则为子进程I D号,若出错则为-1.
Status选项,为空时,代表任意状态结束的子进程,若不为空,则代表指定状态结束的子进程。
waitpid函数说明
pid_t waitpid(pid_t pid, int * status, int options) ;
对于waitpid的p i d参数的解释与其值有关:
pid == -1 等待任一子进程。于是在这一功能方面waitpid与wait等效。
pid > 0 等待其进程I D与p i d相等的子进程。
pid == 0 等待其组I D等于调用进程的组I D的任一子进程。
pid < -1 等待其组I D等于p i d的绝对值的任一子进程。
wait和waitpid的区别
在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞。
waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的特定进程。
实际上wait函数是waitpid函数的一个特例。
(7)如何避免僵尸进程
调用wait或者waitpid函数,此方法父进程会被挂起。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。
五、守护进程 (1)什么是守护进程
·守护进程是在后台运行不受终端控制的进程。
·守护进程能自动转到后台并且脱离与终端的联系。
·Linux系统中一般有很多守护进程在后台运行,执行不同的管理任务。
(2)进程和进程组
·进程属于一个进程组
·进程组号(GID)就是进程组长的进程号(PID)
·进程组通常是从父进程继承过来
·登录会话可以包含多个进程组
·所有的进程组共享一个控制终端
(3)守护进程编程步骤 1、创建子进程,父进程退出
•所有工作在子进程中进行
•形式上脱离了控制终端
2、在子进程中创建新会话
•setsid()函数
•使子进程完全独立出来,脱离控制
3、改变当前目录为根目录
•chdir()函数
•防止占用可卸载的文件系统
•也可以换成其它路径
4、重设文件权限掩码
•umask()函数
•防止继承的文件创建屏蔽字拒绝某些权限
•增加守护进程灵活性
5、关闭文件描述符
•继承的打开文件不会用到,浪费系统资源,无法卸载
•getdtablesize()
•返回所在进程的文件描述符表的项数,即该进程打开的文件数目
Fork 产生一个子进程 子进程调用setsid() 独立门户,成为新的进程组组长
再次fork 产生孙子进程 ---守护进程