Linux 程序、进程和线程

一、程序、进程和线程的概念

在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算运行,进程则是运行着的程序,是操作系统执行的基本单位。线程是为了节省资源而可以在同一个进程中共享的一个执行单位。

程序和进程的差别

  1. 进程的出现最初是在UNlX下,用于表示多用户多任务的操作系统环境下,应用程序在内存环境中基本执行单元的概念。

  2. 进程是UNlX操作系统环境中的基本概念,是系统资源分配的最小单位。

  3. UNlX操作系统下的用户管理和资源分配等工作几乎都是操作系统通过对应用程序进程的控制实现的。

  4. C 、C + +、Java 等语言编写的源程序经相应的编译器编译成可执行文件后,提交给计算机处理器运行。应用程序的运行状态称为进程

  5. 进程从用户角度来看是应用程序的一个执行过程。从操作系统核心角度来看,进程代表的是操作系统分配的内存、CPU 时间片等资源的基本单位,是为正在运行的程序提供的运行环境。

  6. 进程与应用程序的区别在于应用程序作为一个静态文件存储在计算机系统的硬盘等存储空间中,而进程则是处于动态条件下由操作系统维护的系统资源管理实体。

进程概念和程序概念最大的不同之处在于:

  1. 进程是动态的,而程序是静态的。

  2. 进程有一定的生命期,而程序是指令的集合,本身无”运动” 的含义。没有建立进程的程序不能作为1个独立单位得到操作系统的认可;

  3. 一个进程只能对应一 个程序,一个程序可以对应多个进程。进程和程序的关系就像戏剧和剧本之间的关系。

Linux环境下的进程

Linux 的进程操作方式主要有产生进程、终止进程,并且进程之间存在数据和控制的交互,即进程间通信和同步。

1.进程的产生过程

进程的产生有多种方式,其基本过程是一致的。

  1. 首先复制其父进程的环境配置。
  2. 在内核中建立进程结构。
  3. 将结构插入到进程列表,便于维护。
  4. 分配资源给此进程。
  5. 复制父进程的内存映射信息。
  6. 管理文件描述符和链接点。
  7. 通知父进程。

2.进程的终止方式

  1. 从 main返回。
  2. 调用exit。
  3. 调用_exit。
  4. 调用abort。
  5. 由一个信号终止。

进程在终止的时候,系统会释放进程所拥有的资源,例如内存、文件符和内核结构等。

3.进程之间的通信

进程之间的通信有多种方式,其中管道共享内存消息队列是最常用的方式。

  1. 管道是UNIX族中进程通信的最古老的方式,它利用内核在两个进程之间建立通道,它的特点是与文件的操作类似,仅仅在管道的一 端只读,另一 端只写。利用读写的方式在进程之间传递数据。

  2. 共享内存是将内存中的一 段地址,在多个进程之间共享。多个进程利用获得的共享内存的地址来直接对内存进行操作。

  3. 消息队列则是在内核中建立一个链表,发送方按照一 定的标识将数据发送到内核中,内核将其放入量表后,等待接收方的请求。接收方发送请求后,内核按照消息的标识,从内核中将消息从链表中摘下,传递给接收方。消息队列是一 种完全的异步操作方式。

4.进程之间的同步

  • 多个进程之间需要协作完成任务时,经常发生任务之间的依赖现象,从而出现了进程的同步问题。 Linux 下进程的同步方式主要有消息队列、信号量等。

    • 信号量是一 个共享的表示数量的值。用于多个进程之间操作或者共享资源的保护,它是进程之间同步的最主要方式。

进程和线程

线程和进程是另一 对有意义的概念,主要区别和联系如下:

  1. 进程是操作系统进行资源分配的基本单位,进程拥有完整的虚拟空间。进行系统资源分配的时候,除了 CPU 资源之外,不会给线程分配独立的资源,线程所需要的资源需要共享。

  2. 线程是进程的一 部分,如果没有进行显式地线程分配,可以认为进程是单线程的;如果进程中建立了线程,则可以认为系统是多线程的。

  3. 多线程和多进程是两种不同的概念,虽然二者都是并行完成功能。但是,多个线程之间像内存、变量等资源可以通过简单的办法共享,多进程则不同,进程间的共享方式有限。

  4. 进程有进程控制表 PCB , 系统通过 PCB 对进程进行调度;线程有线程控制表TCB 。是,TCB 所表示的状态比PCB 要少得多。

二、进程产生的方式

进程是计算机中运行的基本单位,要产生一个进程,有很多种方式,例如使用fork()函数、system()函数、exec()函数等,这些函数的不同在于其运行环境的构造之间存在差别,其本质都是对程序运行的各种条件进行设置,在系统之间建立一个可以运行的程序。

进程号

每个进程初始化时,系统都分配一个ID号,用于标识此进程。在Linux中进程是唯一的,系统可以用这个值来表示一个进程,描述进程的ID号通常叫做PID,即进程ID(process id)。PID的变量类型为pid_t。

1.getpid()、getppid()函数介绍

getpid()、getppid()函数返回当前进程的ID号,getppid()返回当前进程的父进程ID号。类型pid_t是个typedef类型,定义为unsigned int。getpid()函数和getppid()函数的原型如下:

#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);

2.getpid()函数的例子

下面例子使用getpid()函数和getppid()函数例子。程序获取当前程序的PID和父程序的PID:

#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
   
        pid_t pid,ppid;

        pid = getpid();
        ppid = getppid();

        printf("当前进程的ID为:%d\n",pid);
        printf("当前进程的父进程号ID为:%d\n",ppid);
        return 0;
}

在这里插入图片描述

进程复制fork()

fork()函数是产生进程的一 种方式 。fork()函数 以父进程为蓝本复制一个进程,其ID号和父进程ID号不同。在Linux环境下,fork()是以写复制实现的,只有内存等与父进程不同时,其他与父进程共享,只有在父进程或者子进程进行了修改后,才重新生成一 份。

1.fork()函数介绍

fork()函数的原型如下,当成功时,fork()函数的返回值是进程的ID;失败则返回-1。

#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);

特点:执行一次,返回两次。在父进程和子进程中返回的是不同的值,父进程中返回的子进程的ID号,而子进程中则返回0。

2.fork()函数的例子

fork()函数的例子·,在调用fork()函数之后,判断fork()函数的返回值:如果为-1,打印失败信息;如果为0,打印子进程信息;大于0,打印父进程信息。

include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
   
        pid_t pid;
        pid=fork();

        if(-1 == pid)
        {
   
                printf("失败!\n");
                return -1;
        }else if(pid == 0){
   
                printf("子进程,fork:%d, ID:%d , 父进程:%d\n",pid,getpid(),getppid());
        }else{
   
                printf("父进程,fork:%d,ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
        }
        return 0;
}

在这里插入图片描述
Fork出来的子进程的父进程ID号是执行fork()函数的进程的ID号。

system()函数方式

system()函数调用shell的外部命令在当前进程中开始另一个进程。

1.system()函数调用"/bin/sh-c command"执行特定的命令,阻塞当前进程直到command命令执行完毕。system()函数的原型如下:

#include<stdlib.h>
int system(const char *command);

执行system()函数,会调用fork()、execve()、waitpid()等函数,其中任意一个调用失败,将导致system()函数调用失败。system()函数的返回值如下:

  1. 失败,返回-1。

  2. 当sh不能执行时,返回127。

  3. 成功,返回进程状态值。

2.system()函数的例子

例如下面的代码获得当前进程的ID,并使用system()函数进行系统调用ping网络上的某个主机,程序将当前系统分配的PID值和进行system()函数调用的返回值都进行了打印:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
   
        int ret;
        printf("系统分配的进程号:%d\n",getpid());
        ret = system("ping www.baidu.com -c 2");
        printf("返回值为:%d\n",ret);
        return 0;
}

在这里插入图片描述
系统分配给当前进程的ID号为58957;然后系统ping了网络上的某个主机,发送和接收两个ping的请求包,再退出ping程序;此时系统的返回值在原来的程序中才返回。在测试的时候返回512。

进程执行exec()函数系列

使用 fork()函数和 system ()函数的时候,系统中都会建立一 个新的进程,执行调用者的操作,而原来的进程还会存在,直到用户显式地退出;
而 exec()族的函数与之前的fork()和system ()函数不同,exec()族函数会用新进程代替原有的进程,系统会从新的进程运行,新进程的PID值会与原来进程的PID值相同。

1.exec()函数介绍

exec()函数共有6个,其原型如下:

#include<unistd.h>
extern char **environ;
int execl(const char *path,const char *arg,...);
int exevlp(const char *file,const char *arg,...);
int execle(const char *path,const char *arg,...,char * const envp[]);
int execv(const char *path,char *const argv[]);
int execvp(const char *file,char *const argv[]);
  1. 只有 **execve()**函数是真正意义上的系统调用,其他 5 个函数都是在此基础上经过包装的库函数。

  2. exec()函数族的作用是,在当前系统的可执行路径中根据指定的文件名来找到合适的可执行文件名,并用它来取代调用进程的内容,即在原来的进程内部运行一 个可执行文件。上述的可执行文件既可以是二进制的文件,也可以是可执行的脚本文件。

  3. 与fork()函数不同,exec()函数族的函数执行成功后不会返回,这是因为执行的新程序已经占用了当前进程的空间和资源,这些资源包括代码段、数据段和堆栈等,它们都已经被新的内容取代,而进程的ID等标识性的信息仍然是原来的东西,即 exec()函数族在原来进程的壳上运行了自己的程序,只有程序调用失败了,系统才会返回-1。

  4. 使用exec()函数比较普遍的一 种方法是先使用fork()函数分叉进程,然后在新的进程中调用exec()函数,这样exec()函数会占用与原来一 样的系统资源来运行。

  5. Linux 系统针对上述过程专门进行了优化。由于fork()的过程是对原有系统进行复制,然后建立子进程,这些过程都比较耗费时间。如果在 fork()系统调用之后进行exec()系统调用,系统就不会进行系统复制,而是直接使用 exec()指定的参数来覆盖原有的进程。上述的方法在 Linux 系统上叫做“写时复制”,即只有在造成系统的内容发生更改的时候才进行进程的真正更新。

2.ececve()函数得例子

先打印调用进程的进程号,然后调用execve()函数,这个函数调用可执行文件"/bin/ls"列出当前目录下的文件:

#include<stdio.h>
#include<unistd.h>
int main()
{
   
        char *args[]={
   "/bin/ls",NULL};
        printf("系统分配进程号:%d\n",getpid());
        if(execve("/bin/ls",args,NULL)<0)
                printf("失败!\n");
        return 0;
}

在这里插入图片描述

所有用户态进程的产生进程init
  1. 在Linux 系统中,所有的进程都是有父子或者堂兄关系的,除了初始进程init, 没有哪个进程与其他进程完全独立。系统中每个进程都有一 个父进程,新的进程不是被全新地创建,通常是从一 个原有的进程进行复制或者克隆的。

  2. Linux 操作系统下的每一 个进程都有一个父进程或者兄弟进程,并且有自己的子进程。可以在 Linux 下使用命令pstree 来查看系统中运行的进程之间的关系,如下所示。可以看出,init进程是所有进程的祖先,其他的进程都是由init进程直接或者间接fork()出来的。

在这里插入图片描述
在这里插入图片描述
新的系统使用:systemd
在这里插入图片描述

三、进程间通信和同步

  • Linux下的多个进程之间的通信叫IPC,是多个进程之间相互沟通的一种方法。

    • Linux下有多种进程间通信方法:半双工管道、FIFO(命名管道)、消息队列、信号量、共享内存等。

半双工管道

管道是一种把两个进程之间的标准输入和标准输出连接起来的机制。管道是一种历史悠久的进程间通信的方法。

1.基本概念

由于管道仅仅是将某个进程的输出另一个进程的输入****相邻的单向通信的方法,因此称其为"半双工"。在shell中管道用"|"表示,如下所示,管道的一种使用方式。
在这里插入图片描述

ls -l | grep *.c
  1. ls -l的输出当做"grep * .c"的输入,管道在前 一 个进程 中建立输入通道 ,在后一 个进程建立输出通道,将数据从管道的左边传输到管道的右边,将Is -I的输出通过管道传给 “grep *.c”。

  2. 进程创建管道,每次创建两个文件描述符来操作管道。其中一个对管道进行写操作。另一 个描述符对管道进行读操作。

  3. 如下图所示,显示了管道如何将两个进程通过内核连接起来,从图中可以看出这两个文件描述符是如何连接在一 起的。如果进程通过管道fda[0] 发送数据,它可以从**fdb[0]**获得信息。

在这里插入图片描述

  1. 由于进程A和进程B都能够访问管道的两个描述符,因此管道创建完毕后要设置在各个进程中的方向,希望数据向那个方向传输。

  2. 这需要做好规划,两个进程都要做统一 的设置,在进程A中设置为读的管道描述符,在进程B中要设置为写;

  3. 反之亦然,并且要把不关心的管道端关掉。对管道的读写与一 般的IO 系统函数一 致,使用write()函数写入数据, read() 函数读出数据,某些特定的IO 操作管道是不支持的,例如偏移函数lseek()。

2.pipe()函数介绍

创建管道的函数原型:

#include<unistd.h>
int pipe(int filedes[2]);

数组中的filedes是一个文件描述符的数组,用于保存管道返回的两个文件描述符。

数组中的第1个元素(下标为0)是为了读操作而创建和打开的,而第2个元素(下标为1)是为了写操作而创建和打开的。
直观地说,fd1的输入是fd0的输入。当函数执行成功时,返回0,失败时返回值为-1,建立管道的代码如下2:

#include<stdio.h>
#include<unistd.h>
#include<sys/types>
int main(void)
{
   
	int result = -1;
	result = pipe(fd);
	if(-1 ==result)
	{
   
		printf("建立管道失败\n");
		return -1;
		}
		...
		...
}

要使用管道有切实的用处,需要于进程的创建结合起来,利用两个管道在父进程和子进程之间通信。如下图所示,在父进程和子进程之间建立一个管道,子进程向管道中写入数据,父进程从管道中读取数据。要实现这样的模型,在父进程中需要关闭写端,在子进程中需要关闭读端。

在这里插入图片描述

3.pipe()函数的例子

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
int main(void)
{
   
        int result = -1;
        int fd[2],nbytes;
        pid_t pid;
        char string[] = "你好,管道";
        char readbuffer[80];

        int *write_fd = &fd[1];/*写文件描述符*/
        int *read_fd = &fd[0];/*读文件描述符*/

        result = pipe(fd);
        if(-1 == result)
        {
   
                printf("建立管道失败\n");
                return -1;
        }
        pid=fork();/*分叉出现*/

        if(-1 == pid)
        {
   
                printf("fork进程失败\n");
                return -1;
        }
        if(0 == pid)
        {
   
                close(*read_fd);
                result = write(*write_fd,string,strlen(string));
                return 0;
        }
        else
        {
   
                close(*write_fd);
                nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
                printf("接受到%d个数据,内容为:%s\n",nbytes,readbuffer);
                return 0;
        }
}

编译运行:
在这里插入图片描述

4.管道阻塞和管道操作的原子性

  1. 当管道的写端没有关闭时,如果写请求的字节数目大于阈值PIPE_BUF, 写操作的返回值是管道中目前的数据字节数。

  2. 如果请求的字节数目不大于PIPE_BUF, 则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量;或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。

  3. 注意:PIPE_BUF在include/Linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.l要求PIPE_BUF至少为 5 12 宇节。

  4. 管道进行写入操作的时候,当写入数据的数目小于 128K 时写入是非原子 的,如果把父进程中的两次写入字节数都改为 128K , 可以发现:写入管道的数据量大于128K 字节时,缓冲区的数据将被连续地写入管道,直到数据全部写完为止,如果没有进程读数据,则一直阻塞。

5.管道操作原子性的代码

例如,下面的代码为一个管道读写的例子。在成功建立管道后 ,子进程向管道 中写入数据,父进程从管道中读出数据。子进程一次写入 128K 个字节的数据 ,父进程每次读取10K字节的数据。当父进程没有数据可读的时候退出。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>

#define K 1024
#define WRITELEN (128*K)

int main(void)
{
   
        int result = -1;/*创建管道结果*/
        int fd[2],nbytes;/*文件描述符,字符个数*/
        pid_t pid;/*PID值*/
        char string[WRITELEN] = "你好,管道";

        char readbuffer[10*K];/*读缓冲区*/


        int *write_fd = &fd[1];
        int *read_fd = &fd[0];

        result = pipe(fd);/*建立管道*/
        if(-1== result)
        {
   
                printf("建立管道失败\n");
                return -1;
        }
        pid = fork();

        if(-1 == pid)
        {
   
                printf("fork进程失败\n");
                return -1;
        }

        if(0==pid)

        {
   
                int write_size = WRITELEN;
                result = 0;
                close(*read_fd);/*关闭读端*/
                while(write_size >= 0)
                {
   
                        result = write(*write_fd,string,write_size);

                        if(result >0)
                        {
   
                                write_size -=result;
                                printf("写入%d个数据,剩余%d个数据\n",result,write_size);
                        }
                        else
                        {
   
                                sleep(10);          
                        }
                }
                return 0;
        }
        else/*父进程*/
        {
   
                close(*write_fd);
                while(1)
                {
   
                        nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));

                        if(nbytes <= 0)
                        {
   
                                printf("没有数据写入了\n");
                                break;
                        }
                        printf("接受到%d个数据,内容为:%s\n",nbytes,readbuffer);
                }
        }
        return 0;
}

编译运行:
在这里插入图片描述

  • 父进程每次读取10K字节的数据,读了13次全部读出,最后一次读数据,由于缓冲区只有8K字节的数据,所以仅读取了8K字节。
  • 字进程一次性地写入128K字节的数据,当父进程将全部数据读取完毕时,字进程的write()函数才返回将写入信息(“写入131072个数据,剩余0个数据”)打印出来。

函数popen和pclose

常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输 入端发送数据,为此,标准I/O库提供了两个函数popen和pclose。这两个函数实 现的操作是:创建一个管道, fork一个子进程,关闭未使用的管道端,执行一 个shell运行命令,然后等待命令终止。

  1. 函数popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O 文件指针。

  2. 若type是"r",则文件指针连接到cmdstring的标准输出;若type是"w",则文件指针连接到cmdstring的标准输入,如下图所示。

在这里插入图片描述

#include <stdio.h> 

FILE *popen(const char *cmdstring, const char *type); 
//type是"r",则返回的文件指针是可读的,是"w", 则是可写的。
//返回值:若成功,返回文件指针;若出错,返回NULL 
int pclose(FILE *fp); //关闭标准I/O流,等待命令终止,然后返回shell的终止状态。
// shell不能被执行,则pclose返回的终止状态与shell已执行exit一样。
//返回值:若成功,返回cmdstring的终止状态;若出错,返回-1
//cmdstring由Bourne shell以下列方式执行:

sh -c cmdstring
//表示shell将扩展cmdstring中的任何特殊字符,例:
fp = open("ls *.c","r");
//或
fp = popen("cmd 2>&1","r");

例:用popen向分页程序传送文件。

#include "apue.h"
#include <sys/wait.h>

#define	PAGER	"${PAGER:-more}" /* 环境变量,或默认值 */

int main(int argc, char *argv[])
{
   
	char	line[MAXLINE];
	FILE	*fpin, *fpout;

	if (argc != 2)
		err_quit("usage: a.out <pathname>");
	if ((fpin = fopen(argv[1], "r")) == NULL)
		err_sys("can't open %s", argv[1]);

	if ((fpout = popen(PAGER, "w")) == NULL)
		err_sys("popen error");

	/* copy argv[1] to pager */
	while (fgets(line, MAXLINE, fpin) != NULL)
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值