Linux-操作系统原理基础知识(一)

进程

进程的由来

单片机裸机开发没有进程的概念,因为单片机里面就只运行着一个裸机程序。而操作系统不同,它可以运行多个程序,就比如Windows操作系统:在打游戏的时候还听着歌。操作系统需要对这些程序进行统一管理。操作系统是通过进程来协调应用程序的运行的。

在Linux系统里面,通过一个 task_struct 结构体抽象表示进程的,在task_struct结构体里面有一个 mm* 的结构体指针,此 mm* 结构体指针就管理一大块虚拟内存。当程序想运行的时候,操作系统就把程序里面几个重要的段加载到虚拟内存上面来运行。(.bss不用加载,它主要用来存放值为0的全局变量,直接在虚拟内存里面开辟一块内存,把这块内存清0用来表示.bss段)

程序被加载到虚拟内存上运行后,随着程序运行,是需要不断的使用操作系统的其它资源的,比如:文件系统里面的其它文件,或者说是动态申请内存,使用操作系统里面的各种硬件设备等等。当程序使用了这些资源之后,也会把这些资源放到进程结构体 (task_struct 结构体)里面保存。因此 task_struct 结构体既能够将程序加载到虚拟内存来运行,也能在 task_struct 结构体里面管理程序所使用到的各种的操作系统资源。这样我们操作系统就能通过进程结构体来管理实体程序。(程序是静态的文件,进程是运行着的实体。)

当程序开始去加载执行时候,进程也就是 task_struct 结构体就诞生了,当程序结束运行的时候,进程也就是 task_struct 结构体也就会随之销毁。

Linux下使用 pstree 命令查看进程间的关系。如下图:

操作系统区分进程通过PID,PID就相当于类似于进程的身份标识符。

查看进程表命令:ps -ef | more【| more让ps -ef命令的输出每次只显示一屏,按enter往下跳一页,按q退出】如下进程表:

进程表就是一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的 PID、进程的状态、命令字符串和其他一些 ps 命令输出的各类信息。操作系统通过进程的 ID 对它们进行管理,这些 PID 是进程表的索引。由 Linux 在启动新进程的时候自动依次分配,当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的 PID,当 PID 的数值达最大时,系统将重新选择下一个未使用的数值,新PID 重新从 2 开始,这是因为 PID 数字为 1 的值一般是为特殊进程 init 保留,即系统在运行时就存在的第一个进程, init 进程负责管理其他进程。(上图进程表可以很明显看到,编号为 1 的进程是 init 进程。它位于 /sbin/init 目录中。)。init 进程可以启动一个子进程,它通过 fork() 函数从原程序中创建一个完全分离的子进程。

创建一个新进程fork

fork 函数

头文件:#include <unistd.h>
函数原型:pid_t  fork(void);
返回值pid_t:成功返回0或其他正整数;失败返回:-1。

fork函数特性

fork() 函数是一个系统调用函数,用于从一个已存在的进程中启动一个新进程,新进程称为子进程,而原进程称为父进程。fork函数会在父进程和子进程中分别返回一次:在父进程中的 fork() 调用后返回的是新创建的子进程的 PID,子进程中的 fork() 函数调用后返回的是 0。先返回的是子进程ID给父进程,然后再返回0给子进程。任何进程(除 init 进程)都是由另一个进程启动,该进程称为被启动进程的父进程,被启动的进程称为子进程。父进程的进程号(PID)即为子进程的父进程号(PPID)。可以通过调用 getppid() 函数来获得当前进程的父进程号。

可以将整个程序的执行过程分为fork之前和fork之后两个阶段,其中fork之前的代码部分只会被执行一次,而fork之后的代码部分会被父进程和子进程分别执行。这样的设计可以实现并行处理、多进程编程等功能。在执行fork函数之前,操作系统只有一个进程,因此fork函数之前的代码只会被执行一次。在执行fork函数之后,操作系统有两个几乎一样的进程。fork函数之后的代码会被父进程和子进程各自执行一次。

使用 fork() 函数的本质是将父进程的内容复制一份,如同细胞分裂,因此这个启动的子进程基本上是父进程的一个复制品,但子进程与父进程有不一样的地方,它们的联系与区别如下:

子进程与父进程一致的内容:

  • 进程的地址空间。
  • 进程上下文、代码段。
  • 进程堆空间、栈空间,内存信息。
  • 进程的环境变量。
  • 标准 IO 的缓冲区。
  • 打开的文件描述符。
  • 信号响应函数。
  • 当前工作路径。

子进程独有的内容:

  • 进程号 PID。 PID 是身份证号码,是进程的唯一标识符
  • 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
  • 挂起的信号。这些信号是已经响应但尚未处理的信号,也就是”悬挂”的信号,子进程也不会继承这些信号。

在 fork() 启动新的进程后,子进程与父进程开始并发执行,谁先执行由内核调度算法决定。 fork()函数如果成功启动了进程,会对父子进程各返回一次,其中对父进程返回子进程的 PID,对子进程返回 0;如果 fork() 函数启动子进程失败,它将返回-1。失败通常是因为父进程所拥有的子进程数目超过了规定的限制(CHILD_MAX),此时 errno 将被设为 EAGAIN。如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足, errno 变量将被设为 ENOMEM。

测试一下fork函数

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 
  4 int main()
  5 {
  6     pid_t i;    //pid_t 是一个数据类型,用于表示进程ID(PID)的整数值。
  7     printf("before fork!\r\n");
  8     i = fork();
  9     printf("aftet fork i=%d\r\n",i);
 10     return 0;
 11 }

编译运行结果:

子进程偷梁换柱exec

事实上,使用 fork() 函数启动一个子进程是并没有太大作用的,因为子进程跟父进程都是一样的,子进程能干的活父进程也一样能干,因此开发者就想方设法让子进程做不一样的事情,于是诞生了exec 系列函数,它主要是用于替换进程的执行程序,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换。这里的可执行文件既可以是二进制文件,也可以是Linux 下任何可执行脚本文件。简单来说就是覆盖进程,举个例子, A 进程通过 exec 系列函数启动一个进程 B,此时进程 B 会替换进程 A,进程 A 的内存空间、数据段、代码段等内容都将被进程 B 占用,然后进程 A 将不复存在。

总结:执行fork函数后子进程仍然与父进程程序相同,使用exec函数可以让子进程运行不同的程序

exec函数族:

头文件:#include <unistd.h>
exec函数族各函数原型:
int execl(const char *path, const char *arg, ...)
int execlp(const char *file, const char *arg, ...)
int execv(const char *path, char *const argv[])
int execve(const char *path, char *const argv[], char *const envp[])
参数:
const char *path:指定应用程序文件的路径
const char *arg, ...:是一系列可变参数,代表执行该文件时传递过去的argv[0],argv[1]…argv[n],
实际是传递给上个参数指定的路径下程序文件的main函数的。
最后一个参数必须用空指针 NULL 作为结束的标志。
返回值:成功无返回值;失败返回-1

 exec函数族使用注意事项:

  • l后缀和v后缀必须二选一来使用。
  • p后缀和e后缀是可选的,可用可不用。
  • 后缀组合使用的函数还有很多。比如:execlp,execle,execvp,execve。
  • exec函数是可能执行失败的,需要预防。

execl:以列表list形式传参;

execp:使用环境变量Path来寻找指定文件;

execv:以矢量数组vector形式传参;

execve:用户提供自定义的环境变量

execl用法

如下示例程序:将子进程替换为  ls 并传递参数 -l【指定ls程序路径】

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 
  4 int main()
  5 {
  6     pid_t result;
  7     result = fork();
  8     if(result > 0)    //父进程
  9     {                            //execl执行完之后就会切换到所替换的进程ls里面去了
 10         execl("/bin/ls","ls","-l",NULL);    //父进程替换为ls
 11         printf("error!\r\n");    //进程替换了的话就不会执行到这里
 12         return -1;
 13     }
 14     return 0;    //子进程执行此代码
 15 }

执行execl.c文件后打印出的信息和直接在当前目录下使用ls -l命令所得到的结果是一样的。

execlp用法

如下示例程序:将父进程替换为  ls 并传递参数 -l【在PATH路径中寻找ls程序】

与execl的区别:execlp函数只需要指定可执行文件的名字 file,而不需要提供完整的路径。函数会在环境变量 PATH 所指定的目录中自动查找可执行文件。运行结果与上面的运行结果相同。

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 
  4 int main()
  5 {
  6     pid_t result;
  7     result = fork();
  8     if(result > 0)    //父进程
  9     {                            //execl执行完之后就会切换到所替换的进程ls里面去了
 10         execlp("ls","ls","-l",NULL);    //第一个参数直接指定文件,不用设置路径了
 11         printf("error!\r\n");    //进程替换了的话就不会执行到这里
 12         return -1;
 13     }
 14     return 0;    //子进程执行此代码
 15 }

execv用法

如下示例程序:将父进程替换为  ls 并传递参数 -l。程序执行结果与上面相同。

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 
  4 int main()
  5 {
  6     pid_t result;
  7     char *arg[]={"ls","-l",NULL};//定义一个char*类型的数组来设置参数
  8     result = fork();
  9     if(result > 0)
 10     {
 11         execv("/bin/ls",arg); //传参方式改为了数组传参,将参数传给/bin/ls的应用程序
 12         printf("error!\r\n");
 13         return -1;
 14     }
 15     return 0;
 16 }

execve用法

如下示例程序:将父进程替换为 env 应用程序。env应用程序作用是:把应用程序里的环境变量打印出来。程序执行结果如下图。

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 
  4 int main()
  5 {
  6     pid_t result;
  7     char *arg[]={"env",NULL};
  8     char *env[]={"PATH=/tmp","name=lllyyyppp",NULL};    //自定环境变量
  9     result = fork();
 10     if(result > 0)
 11     {
 12         execve("/usr/bin/env",arg,env);    //第三个参数是环境变量的数组
 13         printf("error!\r\n");
 14         return -1;
 15     }
 16     return 0;
 17 }

进程的退出exit

正常终止进程:

  • main中调用return关键字。
  • 调用exit函数终止。
  • 调用_exit函数终止。

异常终止进程:

  • 调用 abort() 函数异常终止。
  • 由系统信号终止。

exit和_exit退出函数:

当程序执行到 exit() 或 _exit() 函数时,进程会无条件地停止剩下的所有操作,清除包括进程控制块 PCB 在内的各种数据结构,并终止当前进程的运行。

头文件:
#include <unistd.h>     #include <stdlib.h>
exit()定义在 stdlib.h 中      _exit()定义在 unistd.h 中
函数原型:
void _exit(int status);
void exit(int status);
参数status:子进程的退出状态status通知给父进程。这个参数是进程终止时的状态码,0 表示正常终
止,其他非 0 值表示异常终止,一般可以用-1 或者 1 表示,标准 C 里有 EXIT_SUCCESS 和
EXIT_FAILURE 两个宏,表示正常与异常终止。
无返回值

 exit和_exit函数的区别:

_exit() 函数的作用:直接通过系统调用使进程终止运行,当然,在终止进程的时候会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构。

而exit() 函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,这就是“清除 I/O 缓冲”。

_exit()函数比exit()函数更加底层,它不会进行清理操作,也不会关闭文件描述符等,而只是直接终止进程的运行。

在 Linux 的标准函数库中,有一种被称作“缓冲 I/O(buffered I/O)”操作。

每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;每次写文件的时候,暂时先写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。
这样增加了文件读写的速度,但可能造成缓冲区数据丢失。比如有些数据,程序认为已经被写入文件中,实际上因为没有满足特定的条件,它们还只被保存在缓冲区内,这时若用_exit() 函数直接将进程关闭,缓冲区中的数据就会丢失。此时若想保证数据的完整性,就一定要使用 exit() 函数。

exit和_exit函数使用示例程序:

  1 #include <unistd.h>
  2 #include <stdlib.h>
  3 #include <stdio.h>
  4 
  5 int main()
  6 {
  7     pid_t result;
  8     result = fork();
  9     if(result == -1)
 10             printf("fork error!\r\n");
 11     if(result == 0)       //子进程
 12     {                       //"son"只是写入到了IO缓冲区,不会真正打印出来
 13         printf("son");  //printf函数遇到回车换行符才会到IO缓冲区里面提取数据并打印
 14         _exit(0);        //直接通过系统调用终止进程,不考虑IO缓冲区的数据
 15     }
 16     else                //父进程
 17     {
 18         printf("parent");
 19         exit(0);         //处理IO缓冲区中的数据后再退出
 20     }
 21 }

等待进程wait

在 Linux 中,当我们使用 fork() 函数启动一个子进程时,子进程就有了它自己的生命周期并将独立运行,在某些时候,可能父进程希望知道一个子进程何时结束,或者想要知道子进程结束的状态,甚至是等待着子进程结束,那么我们可以通过在父进程中调用 wait() 或者 waitpid() 函数让父进程等待子进程的结束。

僵尸进程

当一个进程调用了 exit() 之后,该进程并不会立刻完全消失,而是变成了一个僵尸进程。子进程退出后,父进程没有调用wait()函数处理身后事,子进程变成僵尸进程。僵尸进程是一种非常特殊的进程,它已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	int pid;
	if( (pid=fork()) < 0){
		printf("fail to fork\n");
		return -1;
	}
	else if(pid==0){
			printf("child exit now\n");
			exit(0);			//子进程打印后退出
		}
		else{
			while(1)			//父进程没有调用wait,子进程变为僵尸进程
		}
	return 0;
}

托孤进程

父进程比子进程先退出,子进程变为孤儿进程,Linux系统会把子进程托孤给1号进程(init进程)。

父进程要回收这个僵尸进程,调用 wait() 或者 waitpid() 函数将僵尸进程回收,释放僵尸进程占有的内存空间,并且了解一下进程终止的状态信息。

wait函数

头文件:#include <sys/wait.h>
函数原型:pid_t wait(int *wstatus);
返回值:成功返回退出的子进程的pid,失败返回-1

wait() 函数在被调用的时候,系统将暂停父进程的执行,直到有信号来到或子进程结束,如果在调用 wait() 函数时子进程已经结束,则会立即返回子进程结束状态值。子进程的结束状态信息会由参数 wstatus 返回,同时该函数会返子进程的 PID,通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态,如果不在意子进程的结束状态信息,则参数wstatus 可以设成 NULL。
wait() 函数有几点需要注意的地方:

  • wait() 要与 fork() 配套出现,如果在使用 fork() 之前调用 wait(), wait() 的返回值则为-1,正常情况下 wait() 的返回值为子进程的 PID。
  • 参数 wstatus 用来保存被收集进程退出时的一些状态,它是一个指向 int 类型的指针。当我们对这个子进程是如何死掉不在意,只想把此僵尸进程消灭掉,就可设定此参数为 NULL。

Linux 系统提供了关于等待子进程退出状态的一些宏定义,我们可以使用这些宏定义来直接判断子进程退出的状态:

  • WIFEXITED(status) :如果子进程结束,返回一个非零值,该宏为真。
  • WEXITSTATUS(status):如果 WIFEXITED 非零(子进程正常退出),返回子进程退出码。
  • WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值
  • WTERMSIG(status) :如果 WIFSIGNALED 非零,返回信号代码
  • WIFSTOPPED(status):如果子进程被暂停,返回一个非零值
  • WSTOPSIG(status):如果 WIFSTOPPED 非零,返回一个信号代码

父进程里获取子进程退出值的方法: 

  1 #include <sys/types.h>
  2 #include <sys/wait.h>
  3 #include <stdio.h>
  4 #include <unistd.h>
  5 #include <stdlib.h>
  6 
  7 int main()
  8 {
  9     pid_t result;
 10     int status;
 11     result = fork();
 12     if(result == -1)
 13             printf("error!\r\n");
 14     if(result ==0)
 15     {
 16         printf("son!\r\n");
 17         exit(0);
 18     }
 19     else
 20     {     //调用wait之后就阻塞在此,后面代码不会执行。直到子进程执行完exit之后才会结束阻塞
 21             wait(&status);
 22             if(WIFEXITED(status))
 23                 printf("exit value:%d\r\n",WEXITSTATUS(status));
 24             return 0;
 25     }
 26 }

实际上一般不在父进程调用wait函数。我们会在子进程退出之后,让子进程发一个信号通知父进程,然后父进程再去调用wait函数去获取子进程的退出值。避免父进程阻塞。

进程的状态与状态间的切换

进程是动态的活动的实例,其实指的是进程会有很多种运行状态:一会儿睡眠、一会儿暂停、一会儿又继续执行。虽然 Linux 操作系统是一个多用户多任务的操作系统,但对于单核 CPU 系统来说,在某一时刻,只能有一个进程处于运行状态(此处的运行状态是指占用 CPU),其他进程都处于其他状态,等待系统资源,各任务根据调度算法在这些状态之间不停地切换。由于 CPU 处理速率较快,用户感觉每个进程都是同时运行。

Linux 进程从被启动到退出的全部状态,以及这些状态发生转换时的条件:

就绪态、运行态、睡眠态(可中断即响应中断,不可中断即不响应中断)、暂停态(包含暂停态和调试态,收到信号进入暂停态,收到调试命令进入调试态)、僵尸态、死亡态。

一个进程的开始都是从其父进程调用 fork() 开始的,在系统一上电运行的时候, init 进程就开始工作,在系统运行过程中,会不断启动新的进程,这些进程要么是由init 进程启动的,要么是由被 init 进程启动的其他进程所启动的。

状态转换的条件及具体过程:

        一个进程被启动后,都是处于可运行状态(此时进程并未占用 CPU 运行)。处于该状态的进程可以是正在进程等待队列中排队,也可以占用 CPU 正在运行。我们称前者为“就绪态”,称后者为“运行态”(占用 CPU 运行)当系统产生进程调度的时候,处于就绪态的进程可以占用 CPU 的使用权,此时进程就是处于运行态。每个进程运行时间都是有限的,比如 10 毫秒,这段时间被称为“时间片”。当进程的时间片已经耗光了的情况下,如果进程还没有结束运行,那么会被系统重新放入等待队列中等待,此时进程又转变为就绪状态,等待下一次进程的调度。正处于“运行态”的进程即使时间片没有耗光,也可能被别的更高优先级的进程“抢占”,被迫重新回到等到队列中等待。

        处于“运行态”的进程可能会等待某些事件、信号或者资源而进入“可中断睡眠态”,比如进程要读取一个管道文件数据而管道为空,或者进程要获得一个锁资源而当前锁不可获取,甚至是进程自己调用 sleep() 来强制将自己进入睡眠,这些情况下进程的状态都会变成“可中断睡眠态”。“可中断睡眠态”就是可以被中断的,能响应信号,在特定条件发生后,进程状态就会转变为“就绪态”。比如其他进程想管道文件写入数据后,或者锁资源可以被获取,或者是睡眠时间到达等情况。

        处于“运行态”的进程还可能会进入“不可中断睡眠态”,在这种状态下的进程不能响应信号,但是这种状态非常短暂,我们几乎无法通过 ps 命令将其显示出来,一般处于这种状态的进程都是在等待输入或输出(I/O)完成,在等待完成后自动进入“就绪态”。

        当进程收到 SIGSTOP 或者 SIGTSTP 中的其中一个信号时,进程状态会被置为“暂停态”,该状态下的进程不再参与调度,但系统资源不会被释放,直到收到 SIGCONT 信号后被重新置为就绪态。当进程被追踪时(典型情况是使用调试器调试应用程序的情况),收到任何信号状态都会被置为 TASK_TRACED 状态,“调试态”,该状态跟暂停态是一样的,一直要等到 SIGCONT信号后进程才会重新参与系统进程调度。

        进程在完成任务后会退出,那么此时进程状态就变为退出状态,这是正常的退出,比如在main 函数内 return 或者调用 exit() 函数或者线程调用 pthread_exit() 都属于正常退出。进程也会有异常退出,比如进程收到 kill 信号就会被杀死,不管进程怎么死,最后内核都会调用 do_exit() 函数来使得进程的状态变成“僵尸态(僵尸进程)”,这里“僵尸”指的是进程的进程控制块PCB。

        为什么一个进程的死掉之后还要把尸体(PCB)留下?因为进程在退出的时候,系统会将其退出信息都保存在进程控制块中,比如如果他正常退出,那进程的退出值是什么?如果是被信号杀死?那么是哪个信号将其杀死?这些“死亡信息”都被保存在该进程的 PCB 中,好让它的父进程知道他是怎么死的,它的父进程之所以要启动它,很大的原因是要让这个进程去干某一件事情,现在这个孩子已死,那事情办得如何,因此需要把这些信息保存在进程控制块中,等着父进程去查看这些信息。

        当父进程去处理僵尸进程的时候,会将这个僵尸进程的状态设置为 EXIT_DEAD,即“死亡态(退出态)”,这样系统才能去回收僵尸进程的内存空间,否则系统将存在越来越多的僵尸进程,最后导致系统内存不足而崩溃。

若父进程由于太忙而没能及时去处理僵尸进程的时候,要如何处理?

        可能不同的程序员有不同的处理,父进程有别的事情要干,不能随时去处理僵尸进程。在这样的情形下,可以考虑使用信号异步通知机制,让子进程在变成僵尸进程时,给其父进程发一个信号,父进程接收到此信号后,再对其进行处理,在此之前父进程该干嘛就干嘛。

假如在子进程变成“僵尸态”之前,它的父进程已经先它而去了(退出),那么这个子进程变成僵死态由谁处理呢?

        如果一个进程的父进程先退出,那么这个子进程将变成“孤儿进程”(没有父进程),那么这个进程将会被他的祖先进程收养(adopt),它的祖先进程是 init(该进程是系统第一个运行的进程,他的 PCB 是从内核的启动镜像文件中直接加载的,系统中的所有其他进程都是 init 进程的后代)。当子进程退出的时候, init 进程将回收这些资源。

各个状态在Linux系统里面都对应着一个宏来描述的:

  • TASK_RUNNING:就绪/运行状态
  • TASK_INTERRUPTIBLE:可中断睡眠状态
  • TASK_UNINTERRUPTIBLE:不可中断睡眠状态
  • TASK_TRACED:调试态
  • TASK_STOPPED:暂停状态
  • EXIT_ZOMBIE:僵死状态
  • EXIT_DEAD:死亡态 

进程组、会话、终端

进程组:管理相同类型的进程

进程组诞生(三种)

  • shell中直接执行一个应用程序,对于大部分进程来说,自己就是进程组的首进程,。进程组只有一个进程。
  • 如果进程调用了fork函数,那么父子进程属于一个进程组,父进程为首进程。
  • shell中使用管道连接起来的应用程序,两个进程同属一个进程组,第一个程序为进程组的首进程。

进程组也有自己的 ID:叫做 PGID 亦即首进程ID;Ubuntu下使用ps axjf来查看进程组相关信息:

会话:管理进程组

会话诞生

  • 调用setsid函数新建一个会话,应用程序作为会话的第一个进程,称为会话首进程。
  • 用户在终端正确登录之后启动shell时,Linux系统会创建一个新的会话,shell进程作为会话首进程。

会话ID:叫做 SID ,即会话首进程ID。如上图。

会话里面有两大类进程组:前台进程组和后台进程组。

前台进程组

shell进程启动时,默认是前台进程组的首进程。前台进程组的首进程会占用会话所关联的终端来运行。可以尝试一下:在一个shell终端调用slee 10,sleep应用程序会一直占用终端直到运行完毕。(参数10就是sleep10秒钟)

在一个交互式shell中启动其他应用程序的行为:当一个shell进程启动时,它通常成为前台进程组的首进程并控制会话相关的终端。此时,shell进程会占用终端,并负责处理用户输入和输出。当shell进程启动其他应用程序时,这些应用程序成为新的前台进程组,并且会占用终端。意味着shell进程失去了对终端的控制权,它不能再直接与用户进行交互。只有在新的前台进程组退出后,shell进程才能重新获得对终端的控制权。这种机制可以确保在前台运行的程序能够与用户进行交互,而其他后台程序则不会干扰用户操作。

后台进程组

后台进程组中的程序不占用终端;在shell进程里启动程序时,加上 & 符号可以指定程序运行在后台进程组里面。可以尝试一下:在一个shell终端调用slee 10 &,sleep应用程序不会占用终端,会立即把终端的使用权返回给shell进程。

ctrl+z 快捷键可以将前台进程组里面运行的程序进入后台进程组里面,同时停止执行。

jobs命令:查看后台进程组,可以查看后台应用程序的 job ID。

fg  jobID命令可以后台转前台。(通过上面jobs命令查看的后台应用程序的jobID)

终端:分几种不同类型

开发板里的物理终端,物理终端又可以分为:串口终端、LCD终端。物理终端特征:需要具体的设备来进行信息的输出

伪终端,伪终端也分两类:ssh远程连接所产生的终端(ssh实际上是一款软件,通过这款软件可以在其它电脑远程登陆我们本地电脑上的终端)、桌面系统启动的终端:终端命令行窗口就属于此类。

虚拟终端:linux内核自带的,ctrl+alt+f2~f6进入相应终端,ctrl+alt+f1返回ubuntu图形界面。

守护进程

会话管理前后台进程组,会话一般关联1个终端,终端关闭,会话进程全会关闭。

守护进程:不受终端影响,即使终端关闭,它也始终在后台运行。

写一个守护进程步骤:

  • fork() 函数,创建一个子进程,父进程直接退出。

父进程直接退出的原因:让出终端的使用权限,如果父进程并未退出的话,终端使用权限会一直被父进程占有,我们并不希望父进程一直占有终端使用权,让其直接退出,然后将终端的使用权返回给shell。

  • setsid() 函数,在子进程里面创建一个新的会话。

父进程所在的会话一般是关联着一个终端的,父进程会受终端的影响。创建新会话的主要原因是将守护进程与其父进程(通常是终端控制进程)解除关联,使它成为一个独立的进程组,并不再与任何终端或会话关联。脱离终端,不会受终端关闭或断开连接的影响。重设工作目录:创建新会话时,守护进程可以选择重新设置其工作目录,将其更改为适当的路径,以便在后续的操作中不依赖于其他文件系统位置。重设文件权限:新会话还可以重设文件的权限,例如关闭继承的文件描述符,防止守护进程意外地访问或修改不应该被它所操作的文件。总之,创建新会话是为了确保守护进程能够在后台稳定运行,并与任何终端或用户会话解耦。这有助于提高守护进程的可靠性和安全性。

  • chdir() 函数,改变守护进程当前的工作目录,改为根目录 “/”

为什么要改变守护进程当前的工作目录:使用fork函数创建子进程时候,子进程继承了父进程的当前工作目录,会存在一些限制。

  • 脱离文件系统依赖:将守护进程的当前工作目录更改为根目录可以使守护进程与特定的文件系统位置解耦。这样,即使相关的文件系统挂载点发生变化或不可用,守护进程也能够找到所需的文件和资源。

  • 避免卸载文件系统:守护进程可能需要在后台运行而无需依赖任何挂载的文件系统。将当前工作目录设置为根目录有助于避免无法卸载或移除文件系统的情况,因为守护进程不会打开文件系统上的文件或修改文件系统中的数据。

  • 避免限制:某些文件系统可能会对正在使用的文件或路径设置限制,例如限制可打开的文件数或文件路径长度。将当前工作目录设置为根目录可以避免这些限制,确保守护进程能够正常运行。

改变当前工作目录到根目录并非一定需要,取决于具体的守护进程的需求和设计。某些情况下,守护进程可能需要访问特定的文件目录或资源,此时就需要将当前工作目录设置为适当的位置。

  • umask() 函数重设文件权限掩码

umask命令可以查看文件权限掩码的值。

umask    022---只写---000 010 010---当前用户的  当前用户组的  其他用户的

为何重设文件权限掩码?新建文件的权限受文件权限掩码的影响。新建一个文件的时候它的默认的执行权限是666。而真正的文件执行权限是:666&~umask。666与一个umask掩码的非。因此可以通过umask()函数将umask直接设为0,文件执行权限就是666也就是110 110 110了。

  • close() 函数关闭不需要的文件描述符

文件描述符是计算机操作系统中对于打开文件的引用,它是一个非负整数。在Linux操作系统中,文件描述符是唯一标识打开文件的方式。它代表了系统内核中的一个文件表项或文件对象。操作系统使用文件描述符来追踪和管理打开的文件,包括读取、写入、定位、关闭等操作。文件描述符提供了对文件的访问和操作的接口。标准的I/O库(如C语言提供的stdio库)使用文件描述符来代表打开的文件。当打开一个文件时,操作系统会分配一个文件描述符,并将其与该文件相关联。通过文件描述符,程序可以使用诸如读取、写入和定位等操作来处理文件。在Linux系统中,标准输入(stdin)、标准输出(stdout)和标准错误(stderr)通常使用预定义的文件描述符(0、1和2)。其他非标准的文件描述符则是通过打开文件或创建管道等操作获得。总之,文件描述符是用于表示和操作打开文件的整数值,它是操作系统提供给应用程序的一种抽象接口,使其能够操纵文件。

0,1,2:标准输入,输出,出错一般是和终端相关联的,而我们需要守护进程完全脱离终端的影响,因此我们需要把这三个文件描述符关掉。使用close(0),close(1),close(2)。

写一个守护进程的代码:(此代码编译运行的时候一定要加sudo,否则无法创建文件daemon.log)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
 
#define MAXFILE 3
int main()
{
	pid_t pid;
	int fd,len,i,num,cnt=0;
	char *buf="the daemon is running\n";
	char cnt_str[20];
	int cnt_str_len;
 
	len = strlen(buf)+1;
	//1 创建子进程,销毁父进程
	pid = fork();		
	if(pid<0)			
	{
		printf("fork fail\n");
		exit(1);
	}
	if(pid>0)			//退出父进程
		exit(0);

	printf("son ok!\n");//pid=0的情况才会执行到这里
	//2 创建新会话,摆脱终端影响
	setsid();			//子进程
	//3 改变当前工作目录
	chdir("/"); 
	//4 重设文件权限掩码
	umask(0);			//相当于umask无效
	//5 关闭默认的文件描述符
	for(i=0;i<MAXFILE;i++)
		close(i);
	//6 实现守护进程的功能
	while(1)
	{
		cnt++;
		cnt_str_len = snprintf(cnt_str,20,"%d\t",cnt);		//相当于行号
 
		fd = open("/var/log/daemon.log",O_CREAT|O_WRONLY|O_APPEND,0666);
		write(fd,cnt_str,cnt_str_len);
		write(fd,buf,len);
		close(fd);
		sleep(3);
	}
}

普通进程伪装成守护进程

nohup命令,让进程不再受终端的影响,关闭终端之后,进程仍会运行。

关闭终端再打开,执行:ps axjf | grep sleep会发现sleep 888仍在运行。

ps命令详解

ps命令最常用的两个选项ps  aux、ps  axjf 字母选项的含义:

a--显示1个终端所有进程。

u--显示进程的归属用户及内存使用情况。

x--显示没有关联控制终端的进程。

j--显示进程归属的进程组ID、会话ID、父进程ID。

f--以ASCII形式显示出进程的层次关系。

ps aux命令

打印出的信息表头的各项内容:

USER:进程是哪个用户产生的

PID:进程的身份证号码

%CPU:表示进程占用了cpu计算能力的百分比

%MEM:表示进程占用了系统内存的百分比

VSZ:进程使用的虚拟内存大小

RSS:进程使用的物理内存大小

TTY:进程关联的终端

STAT:表示进程当前状态,如下面表格所列进程的各种状态。

START:表示进程的启动时间

TIME:记录进程的运行时间

COMMAND:表示进程执行的具体程序

Linux 系统中进程状态说明(STAT所表示的意思)

状 态                                                                        说明
R运行状态。严格来说,应该是“可运行状态”,即表示进程在运行队列中,处于正在执行或即将运行状态,只有在该状态的进程才可能在 CPU 上运行,而同一时刻可能有多个进程处于可运行状态。
S可中断的睡眠状态。处于这个状态的进程因为等待某种事件的发生而被挂起,比如进程在等待信号。
D不可中断的睡眠状态。通常是在等待输入或输出(I/O)完成,处于这种状态的进程不能响应异步信号。
T停止状态。通常是被 shell 的工作信号控制,或因为它被追踪,进程正处于调试器的控制之下。
Z退出状态。进程成为僵尸进程。
X退出状态。进程即将被回收。
s进程是会话其首进程。
l进程是多线程的。
+进程属于前台进程组。
<高优先级任务

ps axjf命令

PPID:表示的父进程的ID

PID:进程的身份证号码

PGID:进程的所在进程组的ID

SID:进程所在会话的ID

TTY:表示进程关联的终端

TPGID:值为-1,表示进程为守护进程

STAT:表示进程当前状态

UID:启动进程的用户ID

TIME:记录进程的运行时间

COMMAND:表示进程的层次关系

ps aux和ps axjf两命令的使用场景

关注进程本身使用ps aux。

因为此命令详细记录了进程占用CPU,占用内存的百分比等信息。

关注进程间关系使用ps axjf。

写在最后:操作系统原理知识很多,本文以及本篇紧接着下一篇博文只是对其一些基础以及常用到的知识点进行了记录,这两篇博文有助于对操作系统原理有个大体框架的认知。希望本文可以帮助到各位读者,文章如有不足,欢迎大家指出,如果文章帮到你了,请一定帮忙点个赞哦。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值