学习笔记6

Unix/Linux进程管理

基本概念

1、进程与程序
程序:存储在磁盘上的文件,包含可执行指令和数据的静态实体。
进程:运行中的程序(一个程序可以执行多次,加载出多个进程)。
进程就是处于活动状态的计算机程序。
2、进程的分类:
交互进程:有输入输出,用户可以根据自己的情况输入数据,得到想要的结果(一般进程)。
批处理进程:由脚本加载执行的程序(比如Linux下shell,Windows下bat文件)。
守护进程:总是活跃的、后台运行,一般由系统开机时加载执行或root用户手动加载执行。
3、查看进程
简单方式:ps 显示出当前用户有终端控制权的进程信息。
列表形式:ps aux 以列表形式显示详细信息
a 所有用户
u 以详细方式显示
x 所有无终端控制的进程
4、进程的详细信息列表

USER	进程的用户名
PID		进程id
%cpu	cpu使用率
%MEM	内存使用率
VSZ		占用虚拟内存的大小
RSS		占用物理内存大小
TTY		有终端显示终端的次设备号,如果无终端控制权显示“?”
STAT	进程的状态:
			0	就绪态,等待被系统调用
			R	运行态,Linux下没有就绪态,就绪态用R表示
			S	休眠态,可以被系统中断(信号)唤醒转入运行态
			T	暂停态,是被SIGSTOP信号暂停,当收到SIGCONT信号时,才能再转入运行态
			Z	僵尸态,已经结束停止运行,但父进程还没有回收
			<	高优先级进程
			N	低优先级进程
			l	多线程化的进程
			+	在前台进程组中的进程
			s	会话的首进程
START TIME	进程的开始时间
COMMAND	进程的可执行文件名

5、父进程与子进程,孤儿进程与僵尸进程
一个进程A可以创建出另一个进程B,创建者叫父进程,被创建者叫子进程,父进程启动子进程后,在操作系统的调用下父子进程同时执行(同步)。
如果子进程先于父进程结束,会向父进程发送一个信号SIGCHLD,父进程收到信号后,就应该去回收子进程的相关资源,但在默认情况下,父进程忽略该信号。
也就是说,当子进程结束后,父进程没有回收子进程的资源,那么子进程就变成了僵尸进程。
如果父进程先于子进程结束,子进程变成了孤儿进程,同时被孤儿院(init)收养,然后变成了init的子进程。

进程标识符

操作系统会为每个进程分配一个唯一的标识符,而且采用无符号整数表示,即为进程ID。
进程ID在任何时候都是唯一的,但是可以重用,当一个进程结束,新创建的进程才可以使用它的进程ID(延时重用)。

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:获取进程ID
pid_t getppid(void);
功能:获取父进程ID

uid_t getuid(void);
功能:获取当前进程用户ID

gid_t getgid(void);
功能:获取当前进程的组ID

创建进程

#include <unistd.h>
pid_t fork(void);
功能:创建一个新进程
返回值:一次调用两次返回,失败返回-1(当进程数超出系统的限制,进程创建结果失败)

注意:
1、两次返回分别是进程ID(子进程)和0,父进程会拿到子进程的ID,子进程返回0,借此可以分别出父子进程,编写不同的处理分支。
2、通过fork创建子进程就是父进程的副本(拷贝)。子进程会获取父进程的数据段、BSS段、堆、栈、IO流(共享文件指针和文件描述符)、缓冲区的拷贝,与父进程共享代码段。
3、子进程会继承父进程的信号处理方式。
4、fork函数调用后,父子进程各自执行,谁先返回不一定,但可以使用一些手法来确保谁先执行
5、僵尸进程与孤儿进程的实现。

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:与fork功能基本一致
区别:通过vfork创建的进程不复制父进程的地址空间(数据段、BSS段、堆、栈、IO流、缓冲区),必须通过execl系列函数加载自己的可执行程序。
注意:当执行vfork时,子进程先返回,此时它占用了父进程的地址空间,当子进程成功创建后,通过execl加载可执行程序,父进程才返回。

execl系列函数
功能:加载子进程的可执行文件。
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
path:可执行文件路径
arg:第一个main函数参数,最后一个必须时以NULL结尾。
int execlp(const char *file, const char *arg, ...);
file:可执行文件的文件名,会从PATH环境变量的路径中查找可执行文件并执行
argc:第一个main函数的参数,最后一次必须以NULL结尾。
int execle(const char *path, const char *arg,..., char * const envp[]);
path:可执行文件路径
argc:第一个main函数的参数,最后一次必须以NULL结尾。
exvp:父进程的环境变量表,传递给子进程
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[]);

进程的正常退出

1、从main函数中return。
2、调用标准库中的exit函数

#include <stdlib.h>
void exit(int status);
功能:调用者立即结束该进程
status:退出状态码,可以在父进程中获取到,子进程留给父进程的遗言。
退出前做的事情:
1、先调用事先注册的函数(通过atexit/on_exit)。
	#include <stdlib.h>
	int on_exit(void (*function)(int , void *), void *arg);
	功能:注册一个函数,当进程通过exit函数开始结束时调用。
	function:函数指针,无返回值,参数1为exit函数的参数,参数2为on_exit的第二个参数。
	arg:当function函数被调用时传递给它第二个参数
		
	int atexit(void (*function)(void));
	功能:注册一个函数,当进程通过exit函数结束时调用。
	function:函数指针,无返回值无参数
	成功返回0失败返回-1
	这两个注册函数都会加入同一个栈中,谁最后等级的谁先执行,它们的区别只有注册的函数格式不同,最多可以登记32个函数。
2、冲刷所有处在未关闭状态的标准IO流(fflush)。
3、返回一个整数(EXIT_SUCCESS/EXIT_FAILURE)给操作系统。
4、该函数不会返回,它的功能实现借助了_exit/_Exit

3、_exit/_Exit函数退出
注意:这两个函数的功能是一样的。

#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);//调用系统的_exit
功能:调用的进程会结束,没有返回值。
status:会被父进程获取到(低八位,1个字节)
1)进程结束前会关闭所有处于打开状态的文件描述符。
2)把所有子进程托付给孤儿院init
3)向它的父进程发送SIGCHLD信号
注意:exit函数也会执行以上操作,因为它底层调用了_exit/_Exit。

4、进程的最后一个线程执行了最后一条语句。
5、进程的最后一个线程调用了pthread_exit函数。

进程的异常退出

1、调用了abort函数,该函数会产生SIGABRT信号。
2、进程接收到一些信号(无法捕获处理)。
3、进程的最后一个线程收到“取消”请求,并做出响应,相当于线程收到了结束“信号”。

进程结束

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
功能:等待所有子进程结束,并获取到最终的状态码,只要有一个进程结束就立即返回。
返回值:现在回收的子进程id

1、应该是父进程收到子进程发送来的SIGCHLD信号时,调用wait函数回收子进程的资源并获取结束状态。
2、如果所有子进程都在运行,则wait阻塞。
3、如果已有僵尸进程,wait也会立即返回,回收资源获取结束状态码。
4、如果没有子进程则返回失败-1。

pid_t waitpid(pid_t pid, int *status, int options);
功能:等待指定的进程结束,并获取到最终的状态码。
pid:
	=-1 等待任意子进程的结束,此时与wait等价
	>0	等待进程号为pid的进程结束,此时只等待一个进程结束
	=0	等待同组的任意子进程结束,此时等待的是整个进程组
	<-1	等待的是进程组id是pid绝对值中的任意子进程结束,此时等待的是整个进程组
	
options:
	WNOHANG	非阻塞模式,如果没有子进程结束则立即退出。
	WUNTRACED	如果子进程处于暂停状态,则返回它的状态。
	WCONTINUED	如果子进程从暂停后转为继续,则返回它的状态。
1、wait函数只能孤独的等待子进程结束,而waitpid可以有更多的选择。
2、waitpid不光可以等待子进程也可以等待同组进程
3、waitpid可以阻塞也可以不阻塞。
4、也可以监控子进程的暂停或继续状态。

system函数

#include <stdlib.h>
int system(const char *command);
功能:执行系统命令的,也可以加载可执行程序。
	相当于创建了一个子进程,但子进程不结束,该函数不返回,父子进程不会同时进行。
该函数的实现应该调用了:vfork、exec、wait等函数。

进程组

进程组是由一个或多个进程的集合,每个进程除有一个进程ID还有一个进程组ID,进程组中的进程归属同一个作业控制(负责完成同一个任务)。
同一进程组的进程,会统一接收到终端的信号,由fork创建的子进程,默认就加入了父进程的进程组。
每个进程组都有一个组长,组长的进程ID就是组ID

#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
功能:获取某个pid进程的进程组ID

pid_t getpgid(pid_t pid);
功能:设置进程pid进程的进程组ID,就相当于加入pgid进程组。pgid就是它的组长

Linux与Unix管道技术

1 管道(pipe)

Linux 中的管道可用于不同进程之间的通信,其操作符为 “|”。 通常管道只能在具有新缘关系(父子或拥有相同祖先)的进程间通信。而有名管道克服了管道没有名字的限制,因此它可允许无亲缘关系进程间的通信。
1.1 实现机制
管道是由内核管理的一个缓冲区,管道的一端连接一个进程的输出,这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

管道的利用POSIX系统中的fork机制建立的。fork机制会将原进程复制到新进程,并且将原进程对其缓冲区的连接也一块复制过来。这样原进程和新进程都拥有了对同一缓冲区的(即管道)的读写能力,同时,每个进程关闭自己不需要的一个连接,一个只留读连接,一个只留写连接,这样就形成了管道。
1.2 操作举例
显示前三个文件的文件名:

ls | head -3
barry.txt
bob
example.png

将一个程序的输出传递给less命令使其易于查看:

ls -l /etc | less
(Full screen of output you may scroll. Try it yourself to see.)

列出当前目录下所有具写权限的文件:

ls -l | grep '^.....w'
drwxrwxr-x 3 ryan users 4096 Jan 21 04:12 dropbox

1.3 命名管道
由于基于fork机制的限制,管道只能用于父进程和子进程之间,或者有相同祖先的两个子进程之间的通信。为了解决这一问题,Linux提供了FIFO方式连接进程。FIFO又叫做命名管道(named PIPE)。

FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。FIFO只是借用了文件系统来为管道命名。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,当删除FIFO文件时,管道连接也随之消失。所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。命名管道的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。

用ls命令查看所创建的管道:

$ ls -lF /tmp/my_fifo

prwxr-xr-x 1 root root 0 05-08 20:10 /tmp/my_fifo|

2 fork

2.1 进程
在说fork之前,我们先来复习一下操作系统中进程的相关内容:
进程可以看做程序的一次执行过程,且是拥有资源的最小单位和调度单位(在引入线程的操作系统中,线程是最小的调度单位)。在linux中,每个进程有唯一的PID(进程标识符)标识。PID是一个从1到32768的正整数,其中1是特殊进程init,其它进程从2开始依次编号。当用完32768后,从2重新开始。

Linux中有一个叫进程表的结构用来存储当前正在运行的进程。可以使用“ps aux”命令查看所有正在运行的进程。

进程在linux中呈树状结构,init为根节点,其它进程均有父进程,某进程的父进程就是启动这个进程的进程,这个进程叫做父进程的子进程。
2.2 fork
在Linux系统中创建进程的方式有两种:一是由操作系统创建,二是由父进程创建进程(通常为子进程),即fork。一个进程(父进程)调用fork()函数后,系统先给新的进程(子进程)分配资源,然后把原来进程的所有数据(变量、环境变量、程序计数器等)都复制到新的新进程中,只有少数值与原来的进程的值不同,相当于克隆了一个自己。

子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本,子进程数据空间中的内容是父进程的完整拷贝。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间,它们之间共享的存储空间只有代码段。,但只有一点不同,如果fork成功,子进程中fork的返回值是0, 父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。

从性能方面考虑,父进程到子进程的数据拷贝并不是创建时就拷贝了的,而是采用了写时拷贝(copy-on -write)技术来处理。用fork创建的子进程和父进程作为异步的并发进程而单独执行,它们都有独自的进程标识符(PID)。异步是指它们各行其事,相互间不进行同步;并发是指它们可同时执行。所以我们无法知道子进程和父进程哪一个先执行完。
2.3 举例
C语言版fork例子:

#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'm child process, my process id is %d
",getpid()); 
count++;
    }  
    else {  
        printf("I'm parent process, my process id is %d
",getpid()); 
count++; 
    } 
    printf("Count value: %d
", count);
    return 0;  
}  

运行的结果是:

    i'm child process, my process id is 5574

    Count value: 1
    i'm parent process, my process id is 5573

    Count value: 1

每个进程的PID都可以通过getpid()函数获得,另外还可以通过getppid()函数获得其父进程的PID.

调用fork()(fpid=fork())后生成一个子进程,在子进程中,fork()函数的返回值为0,在父进程中,fork()的返回值为子进程的PID。此后,两个进程根据不同的判断条件(fpid<0; fpid==0)执行不同的代码指令,它们的执行是相互独立的。这两个进程都有一个count变量,这两个变量虽然值相等,但其实它们属于不同的进程,是不同的变量,存放在不同的内存地址中。另外,子进程生成之后是从fork()函数之后的代码开始执行的,而不是从#include <unistd.h>处。 这是因为fork操作复制并使用了原进程的程序计数器的缘故。

3. 文件描述符(file descriptor)

当fork函数生成两个进程之后,这两个进程可以利用相同的文件描述符对同一个文件进行读/写操作,这样就可以在两个进程之间传递数据了。
3.1 文件描述符
对于linux/Unix而言,任何事物都以文件的形式存在。通过文件不仅可以访问常规数据,还可以访问网络连接和硬件设备。而文件描述符就是用来访问这些文件的入口。像TCP和UDP等网络应用程序,系统在后台都为该应用程序分配了一个文件描述符,无论这个文件的本质如何。该文件描述符为应用程序与基础操作系统之间的交互提供了通用接口。
文件描述符是一个非负的整数,它是一个索引值,指向内核中每个进程打开文件的记录表。当进程打开一个文件或创建一个文件时,内核就向进程返回一个文件描述符。当进程需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
每一个文件描述符会与一个打开的文件相对应,同时,不同的文件描述符也可能指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。

3.2 与文件有关的三个表
在Linux系统中,内核维护着3个与文件有关的数据结构,它们是:
1)进程级文件描述符表(file descriptor table)
内核为每个进程维护一个进程级别的文件描述符表,该表记录了单个文件描述符的信息,包括:控制标志,打开文件指针等。
2)系统级打开文件表(open file table)
内核为所有打开的文件维护一个系统级别的打开文件描述表,简称打开文件表。记录了每个打开文件的信息,包括:
文件偏移量(file offset), 可调用read()和write()更新,调用lseek()直接修改。
访问模式(status), 可调用open()设置,其状态有只读,只写或读写等。
i-node 对象指针
3)文件系统 i-node 表(i-node table)
每个文件系统会为存储于其上的所有文件(包括目录)维护一个 i-node 表,单个 i-node 包含的信息有:文件类型(常规文件,目录,套接字或FIFO)、访问权限(读、写、执行等)、文件锁列表,文件大小等。
i-node 存储在磁盘设备上,内核在内存中维护了一个副本,这里的i-node表为后者。内存中的副本除了原有信息,还包括:引用计数、所在设备号以及一些临时属性(例如文件锁)。

这3个数据结构的关系如下图所示:

3.3 输入输出重定向
通常,一个进程启动时,Linux/Unix系统会首先为其分配3个文件描述符: 0,1和2,它们分别对应系统中的3个文件:标准输入(STDIN),标准输出(STDOUT),标准错误输出(STDERR)。
Linux中可以使用重定向操作来指定文件描述符,这分为输入重定向和输出重定向。在使用输入重定向(>)时,linux会用重定向指定的文件来替换标准输入文件描述符,它会读取文件并提取数据,如同是在键盘上输入的。在使用输出重定向(>)时,linux会用重定向指定的文件来替换标准输出文件描述符。(>>)表示追加到文件。“&”表示引用文件描述符。
1.临时重定向

echo "This is only in the file" > file 此消息将只输出到file文件,而不输出到屏幕。它的原理就是将标准输出重定向到了文件file. 这里省略了标准输出文件描述符“1”。实际应为: echo "This is only in the file" 1>file.

2.永久重定向

exec 1> file将标准输出重定向到文件file
echo "This is only in the file"   

3.输入重定向

cat <file  将file文件作为cat命令的输入

4.重定向文件描述符

exec 3>&1      #将文件描述符3重定向至1,任何发送给文件描述符3的内容都将输出至终端显示器

exec 1>file     #将发送至文件描述符1的内容重定向至文件file

echo "this should be put in the file"

exec 1>&3 #将此时的标准输出重定向至文件描述符3,而3指向的是终端显示器,因此此时正常输出至显示器

echo "this is the normal output"

3.4 文件描述符的设置
为了防止系统资源的耗尽,linux内核对文件打开的数量进行了限制。这种限制有两个层面,一个是用户层面的限制,一个是系统层面的限制。

ulimit命令看到的是用户级的最大文件描述符限制,也就是说每一个用户登录后执行的程序占用文件描述符的总数不能超过这个限制

[root@localhost ~]# ulimit -n

10240

设置进程能打开的最大文件句柄数:ulimit -n xxx

[root@localhost ~]# ulimit -n 10240

10240

sysctl命令和proc文件中查看到的数值是一样的,这属于系统级限制,它是限制所有用户打开文件描述符的总和

[root@localhost ~]# sysctl -a | grep -i file-max --color

fs.file-max = 392036

[root@localhost ~]# cat /proc/sys/fs/file-max

392036

修改系统层面的限制需要修改/proc/sys/fs/file-max中的值并且使用"sysctl -p"使之永久生效。

3.5 文件描述符复制与管道的建立
为了生成linux中的管道,首先使用pipe()函数得到一对文件描述符,它们是只读文件描述符和只写文件描述符。fork()函数执行之后,子进程会将父进程的数据拷贝一份,同样,子进程也会拥有父进程所有文件描述符的副本。这时在父进程中关闭读文件描述符,只留下写文件描述符;而在子进程则关闭写文件描述符,只留下读文件描述符。当父进程进行写操作而子进程进行读操作时,就相当于两个进程在通信,管道就形成了。其实,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

代码练习

https://gitee.com/zhang_yu_peng/practice-code/blob/master/冒泡排序.cpp

原文地址:https://www.cnblogs.com/1208499954qzone/p/15441449.html