Linux编程入门四进程

进程的创建有两种方式:一种是由操纵系统创建,一种是由父进程创建。在系统启动时,操作系统会创建一些进程,它们承担着管理和分配系统资源的任务,这些进程通常被称为系统进程。系统允许一个进程创建新进程(即为子进程),子进程还可以创建新的子进程,形成进程树结构。整个Linux系统的所有进程也是一个树形结构。树根是系统自动构造的,即在内核态下执行的0号进程,它是所有进程的祖先。由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟存储管理的内核线程。随后,1号进程调用execve()运行可执行程序init(),并演变成用户态1号进程,即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号等若干终端注册进程getty。每个getty进程设置其进程组标识号,并检测配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可输入注册名和密码进入登陆过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。

进程组:是一个或多个进程的集合。进程组由进程组ID来唯一标识,除了进程号(PID)之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,组长进程的进程号等于进程组ID,且该进程组ID不会因组长进程的退出而受到影响。
会话期:是一个或多个进程组的集合。通常,一个会话开始与用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。

进程创建

Linux系统下使用fork()函数创建一个子进程,返回值是一个进程标识符(PID)。fork函数会创建一个新的进程,并从内核中为此进程分配一个新的可用的进程标识符,为这个新进程分配进程空间,并将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此,fork函数会返回两次,一次是在父进程中,一次是在子进程中。对于父进程,fork()函数返回新创建的子进程的ID。对于子进程,fork()函数返回0.如果创建出错,则fork()函数返回-1。

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

子进程继承资源的情况

资源父子进程是否相同资源父子进程是否相同
进程ID实际组ID
实际用户ID有效组ID
有效用户ID附加组ID
进程组ID.bss段
父进程ID连接的共享存储段
会话ID存储映射
设置用户ID标志资源限制
设置组ID标志tms_utime
当前工作目录tms_stime
根目录tms_cutime
文件权限屏蔽字tms_ustime
信号的屏蔽fork函数的返回值
打开的文件描述符设置的文件锁
数据段未处理的闹钟信号
代码段未决信号集
堆栈段

fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。父进程的用户根目录、当前工作目录等变量的引用计数均会加1。

vfork

借尸还魂

在子进程中执行其他程序,即替换当前进程映像

#include <unistd.h>
extern char** environ;
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[]);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execve(const char* path, char* const argv[], char* const envp[]);

path参数指定可执行文件的完整路径,file参数可以接受文件名,该文件的具体位置则在环境变量PATH中搜寻。arg接受可变参数,argv则接受参数数组,它们都会被传递给新程序的main函数。envp参数用于设置新程序的环境变量。如果未设置它,则新程序将使用由全局变量environ指定的环境变量。一般情况下,exec函数是不返回的,除非出错。它出错时返回-1,并设置errno。如果没出错,则原程序中exec调用之后的代码都不会执行,因此此时原程序已经被exec的参数指定的程序完全替换(包括代码和数据)。exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC的属性。

进程结束

Linux中进程退出分为正常退出和异常退出:

  • 正常退出 1.在main函数中执行return 2.调用exit函数 3.调用_exit函数
  • 异常退出 1.调用abort函数 2.进程收到某个信号,而该信号使程序终止

exit()函数的参数表示进程的退出状态,这个状态的值是一个整型,保存在全局变量$?中。

#include <stdlib.h>
void exit(int status);

$?是Linux shell中的一个内置变量,其中保存的是最近一次运行的进程的返回值。这个返回值有以下3种情况:1.程序中的main()函数运行结束,$?中保存main函数的返回值; 2.程序运行中调用exit函数结束运行,$?中保存exit函数的参数; 3.程序异常退出,$?中保存异常出错的错误号。可以通过shell执行echo $?来得到已结束进程结束状态。

exit和_exit函数

  1. 都是用来终止进程的,当程序执行到exit或_exit时,系统无条件地停止剩下所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。
  2. exit是在头文件stdlib.h中声明,而_exit声明在头文件unistd.h中声明。exit中的参数exit_code为0时,代表进程正常终止,若为其他值,表示程序执行过程中有错误发生。在调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin stdout stderr …)的数据。exit函数是_exit函数之上的一个封装,其会自动调用_exit,并在调用前先刷新流数据。
  3. exit函数与_exit函数最大区别就在于exit函数在调用_exit之前要检查文件打开情况,把文件缓存区的内容写回文件。

孤儿与僵尸进程

对于多进程程序,父进程一般需要跟踪子进程的退出状态。因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。
孤儿进程是指一个父进程退出后,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)收养,并由init进程对它们完成状态收集工作。
僵尸进程是指一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵尸进程。
孤儿进程是父进程已退出,而子进程未退出;僵尸进程是父进程未退出,而子进程已退出。

孤儿进程举例

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 int main()
  5 {
  6         pid_t pid;
  7         pid = fork();
  8         if(pid < 0)
  9         {
 10                 perror("fail to fork");
 11                 exit(-1);
 12         }else if(pid == 0){
 13                 //子进程
 14                 sleep(2);
 15                 printf("Sub-process: PID: %u, PPID: %u\n", getpid(), getppid());
 16         }else{
 17                 //父进程        
 18                 printf("Parent: PID: %u, Sub-process PID: %u\n", getpid(), pid);
 19         }
 20 }

在这里插入图片描述

僵尸进程举例

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 int main()
  5 {
  6         pid_t pid;
  7         pid = fork();
  8         if(pid < 0)
  9         {
 10                 perror("fail to fork");
 11                 exit(-1);
 12         }else if(pid == 0){
 13                 //子进程
 14                 printf("Sub-process: PID: %u, PPID: %u\n", getpid(), getppid());
 15                 exit(0); //子进程退出,成为僵尸进程
 16         }else{
 17                 //父进程        
 18                 printf("Parent: PID: %u, Sub-process PID: %u\n", getpid(), pid);
 19                 sleep(3);
 20                 printf("Parent is alive\n");
 21         }
 22 }

在这里插入图片描述
从图中可以看见僵尸进程,但等父进程睡眠醒来并退出,再查看会发现僵尸进程不见了。因为这个时候该进程变为了孤儿进程,过继给了init进程,而init进程会周期性地调用wait系统调用来清除各个僵尸的子进程。

进程一旦调用wait函数就阻塞自己,直到有信号来到或子进程结束。由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到已经变成僵尸的子进程,wait会收集这个子进程的信息,并把它彻底销毁后返回。子进程的结束状态值会由参数status返回,而子进程的进程码也会返回。如果不需要结束状态值,则参数status可设置成NULL。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int* status, int options);

参数status指出了子进程是正常退出还是非正常结束,以及正常结束时的返回值或被哪个信号结束等信息。有一套专门的宏来提取这些信息。WIFEXITED(status)用来指出子进程是否正常退出,如果是,它会返回一个非零值。WEXITSTATUS(status),当子进程正常退出时,可以用这个宏提取子进程的返回值。如果进程异常退出,返回0,这个值没有意义。
在这里插入图片描述

  1 #include <sys/types.h>
  2 #include <sys/wait.h>
  3 #include <stdio.h>
  4 #include <stdlib.h>
  5 #include <unistd.h>
  6 int main()
  7 {
  8         pid_t pid = fork();
  9         if(pid < 0)
 10         {
 11                 perror("fail to fork");
 12                 exit(-1);
 13         }else if(pid == 0){
 14                 //子进程
 15                 printf("Sub-process: PID: %u, PPID: %u\n", getpid(), getppid());
 16                 exit(3);
 17         }else{
 18                 //父进程        
 19                 printf("Parent: PID: %u, Sub-process PID: %u\n", getpid(), pid);
 20                 int status = -1;
 21                 pid_t pr = wait(&status);
 22                 if(WIFEXITED(status)){
 23                         printf("the child process %d exit normally.\n",pr);
 24                         printf("the return code is %d.\n",WEXITSTATUS(status));
 25                 }else{
 26                         printf("the child process %d exit abnormally.\n",pr);
 27                 }
 28         }
 29         return 0;
 30 }

在这里插入图片描述
waitpid是wait的封装,多出了可由用户控制的参数pid和options。
参数pid为欲等待的子进程识别码

  • pid < -1:等待进程组识别码为pid绝对值的任何子进程

  • pid = -1:等待任何子进程,相当于wait

  • pid = 0:等待进程组识别码与目前进程相同的任何子进程

  • pid = 0:等待任何子进程识别码为pid的子进程
    参数options的值有以下几种类型

  • options=WNOHANG 即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去

  • options=WUNTRACED 子进程进入暂停则马上返回,但结束状态不予理会
    waitpid返回值

  • 当正常返回的时候waitpid返回收集的子进程的进程ID

  • 如果设置了WNOHANG,而调用waitpid发现没有已退出的子进程可收集,则返回0

  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

  • 当pid所指示的子进程不存在,或此进程存在,当不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD

  1 #include <sys/types.h>
  2 #include <sys/wait.h>
  3 #include <stdio.h>
  4 #include <stdlib.h>
  5 #include <unistd.h>
  6 int main()
  7 {
  8         pid_t pid = fork();
  9         if(pid < 0)
 10         {
 11                 perror("fail to fork");
 12                 exit(-1);
 13         }else if(pid == 0){
 14                 //子进程
 15                 printf("Sub-process: PID: %u, PPID: %u\n", getpid(), getppid());
 16                 sleep(10);
 17                 exit(0);
 18         }else{
 19                 //父进程        
 20                 printf("Parent: PID: %u, Sub-process PID: %u\n", getpid(), pid);
 21                 pid_t pr;
 22                 do{
 23                         pr = waitpid(pid,NULL,WNOHANG);
 24                         if(pr==0)
 25                         {
 26                                 printf("No child exited\n");
 27                                 sleep(1);
 28                         }
 29                 }while(pr==0);
 30                 if(pr==pid){
 31                         printf("successfully get the child process %d\n",pr);
 32                 }else{
 33                         printf("some error occured\n");
 34                 }
 35         }
 36         return 0;
 37 }

在这里插入图片描述

守护进程

Linux系统中,每个系统与用户进行交互的界面称为终端,每个从此终端开始运行的进程都回依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端关闭时,相应的进程都会自动关闭。守护进程能脱离终端并在后台运行,守护进程脱离终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。

  1. 创建子进程,父进程退出 子进程由1号进程收养
  2. 在子线程中创建新会话,使用系统函数setsid用于创建一个新的会话,并担任会话组的组长。调用该函数有3个作用:让进程摆脱原会话的控制,让进程摆脱原进程组的控制,让进程摆脱原控制终端的控制。由于在调用fork函数时,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,setsid函数能够使进程完全独立出来。
  3. 改变当前目录为根目录
  4. 重设文件权限掩码
  5. 关闭文件描述符
    在第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能到达守护进程,守护进程中用常规方法输出的字符也不可能中终端上显示出来。所以,文件描述符0、1、2已经失去存在的价值,需要关闭这些文件描述符。

实现一个守护进程,每隔10s在/tmp/dameon.log中写入一句话

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXFILE 65535
int main()
{
	pid_t pc;
	int i,fd,len;
	char* buf = "this is a Dameon\n";
	len = strlen(buf);
	pc = fork(); //第一步
	if(pc<0){
		printf("error fork\n");
		exit(1);
	}else if(pc>0){
		exit(0);
	}
	setsid(); //第二步
	chdir("/"); //第三步
	umask(0); //第四步
	for(i=0;i<MAXFILE;i++) //第五步
	{
		close(i);
	}
	while(1)
	{
		if((fd=open("/tmp/dameon.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
		{
			perror("open error");
			exit(1);
		}
		write(fd,buf,len+1);
		close(fd);
		sleep(10);
	}
	return 0;
}

信号

信号是UNIX和Linux系统响应某些条件而产生的事件。接收到该信号的进程会相应地采取一些行动。可以由shell和终端处理器生成中断,也可以作为在进程间传递消息或修改行为的一种方式,明确地由一个进程发送给另一个进程。

进程间通信

进程间通信(Inter Process Communication, IPC):管道、消息队列、共享内存、信号量、套接字等。最初由AT&T System V2版本的UNIX引入的是消息队列、共享内存、信号量。

管道

管道是一种两个进程间进行单向通信的机制(半双工)。数据只能由一个进程流向另一个进程(其中一个读管道,一个写管道),如果要进行双工通信,则需要建立两个管道。管道的缓冲区大小是受限制的,所传输的是无格式的字节流等。通过管道通信的两个进程,一个进程向管道写数据,另一个从中读数据。写入的数据每次都添加到管道缓冲区的末尾,读数据的时候都是从缓冲区的头部读出数据。

无名管道有名管道
管道只能用于父子进程或兄弟进程间通信,也就是说管道只能用于具有亲缘关系的进程间通信。可以使互不相关的进程实现通信
存在于内核中,不可见可以通过路径名指出,在文件系统中可见

无名管道

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

描述字fd[0]称为管道读端,描述字fd[1]称为管道写端。一般的文件I/O函数都可以用于管道,如close、read、write等。

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 #include <stdlib.h>
  4 #include <string.h>
  5 #define INPUT 0
  6 #define OUTPUT 1
  7 int main()
  8 {
  9         int fd[2];
 10         pid_t pid;
 11         char buf[256];
 12         //创建无名管道
 13         pipe(fd);
 14         pid = fork();
 15         if(pid<0)
 16         {
 17                 printf("Error in fork\n");
 18                 exit(1);
 19         }else if(pid == 0){
 20                 printf("in the child process...\n");
 21                 //子进程向父进程写数据,关闭管道的读端
 22                 close(fd[INPUT]);
 23                 write(fd[OUTPUT],"hello world", strlen("hello world"));
 24                 exit(0);
 25         }else{
 26                 printf("in the parent process...\n");
 27                 //父进程从管道读取子进程写的数据,关闭管道写端
 28                 close(fd[OUTPUT]);
 29                 int returned_count = read(fd[INPUT],buf,sizeof(buf));
 30                 printf("%d bytes of data received from child process: %s\n", returned_count,bu    f);
 31         }
 32         return 0;
 33 }

在这里插入图片描述

有名管道(named pipe或FIFO)

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);

该函数的第一个参数是一个普通的路径名,也就是创建FIFO的名字。第二个参数与打开普通文件的open()函数中的mode参数相同。如果mkfifo的第一个参数是一个已存在的路径名时,会返回EEXIST错误。
mkfifo_r.cpp

  1 #include <stdio.h>
  2 #include <sys/stat.h>
  3 #include <fcntl.h>
  4 #include <unistd.h>
  5 #include <string.h>
  6 #include <stdlib.h>
  7 #define P_FIFO "/tmp/p_fifo"
  8 int main()
  9 {
 10         char cache[100];
 11         int fd;
 12         memset(cache,0,sizeof(cache));
 13         if(access(P_FIFO,F_OK)==0) //管道文件存在
 14         {
 15                 execlp("rm","-f",P_FIFO,NULL);
 16                 printf("remove FIFP file\n");
 17         }
 18         if(mkfifo(P_FIFO,0777)<0)
 19         {
 20                 printf("createnamed pipe failed\n");
 21                 return 0;
 22         }
 23         fd = open(P_FIFO,O_RDONLY|O_NONBLOCK); //非阻塞方式打开,只读
 24         while(1)
 25         {
 26                 memset(cache,0,sizeof(cache));
 27                 if(read(fd,cache,100)==0)
 28                 {
 29                         printf("nodata\n");
 30                 }else{
 31                         printf("getdata: %s\n",cache); //读到数据
 32                 }
 33                 sleep(1);
 34         }
 35         close(fd);
 36         return 0;
 37 }

mkfifo_w.cpp

  1 #include <stdio.h>
  2 #include <fcntl.h>
  3 #include <unistd.h>
  4 #define P_FIFO "/tmp/p_fifo"
  5 int main(int argc,char **argv)
  6 {
  7         int fd;
  8         if(argc < 2)
  9                 printf("please input the write data/\n");
 10         fd = open(P_FIFO,O_WRONLY|O_NONBLOCK);
 11         write(fd,argv[1],100);
 12         close(fd);
 13         return 0;
 14 }

消息队列

消息队列用于运行于同一台机器上的进程间通信,每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,而不一定像管道和命名管道那样必须以先进先出的方式接收数据。消息队列是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。

创建新消息队列或取得已存在消息队列

int msgget(ket_t key, int msgflg);

参数中key可以认为是一个端口号,也可以由函数ftok生成,msgflg如果等于IPC_CREAT,若没有该队列,则创建一个并返回新标识符,若已存在则返回原标识符;msgflg如果等于IPC_EXCL,若没有该队列,则返回-1,若已存在,则返回0。
向队列读/写消息

ssize_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg);
int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);

参数中msqid是消息队列的识别码,msgp是指向消息缓冲区的指针。此位置用来暂时存储发送和接收的消息,是一个用户可定义的通用结构。一般定义为如下结构:

 struct msgstru{
          long msg_type;
         char mtext[512];
 };

msgsz是指消息的数据部分mtext的大小;msgtyp是指从消息队列内读取的消息形态。msgflg用来指明核心程序在队列没有数据的情况下所应采取的行动。如果msgflg和常数IPC_NOWAIT合用,则在msgsnd执行时若是消息队列已满,则msgsnd将不会阻塞,而会立即返回-1,如果执行的是msgrcv,则在消息队列呈空时,立刻返回-1,并设定错误码为ENOMSG。当msgflg为0时,msgsnd及msgrcv在队列呈满或呈空的情况时,采用阻塞等待的处理模式。
在这里插入图片描述

设置消息队列属性

int msgctl(int msgqid, int cmd, struct msqid_ds *buf);

参数中的msgctl系统调用对msgqid标识的消息队列执行cmd操作,系统定义了3种cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。IPC_STAT用来获取消息队列对应的msqid_ds数据结构,并将其保存到buf指定的地址空间;IPC_SET用来设置消息队列的属性,要设置的属性存储在buf中;IPC_RMID用来从内核中删除msqid标识的消息队列。

  1 #include <unistd.h>
  2 #include <stdlib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 #include <errno.h>
  6 #include <sys/msg.h>
  7 #define MAX_TEXT 512
  8 struct msg_st{
  9         long int msg_type;
 10         char text[MAX_TEXT];
 11 };
 12 int main()
 13 {
 14         int running = 1;
 15         int msgid = -1;
 16         struct msg_st data;
 17 
 18         //建立消息队列
 19         msgid = msgget((key_t)1234,0666|IPC_CREAT);
 20         if(msgid == -1)
 21         {
 22                 fprintf(stderr,"msgget failed with error: %d\n",errno);
 23                 exit(EXIT_FAILURE);
 24         }
 25         //从队列中获取消息,直到遇到end消息
 26         while(running)
 27         {
 28                 if(msgrcv(msgid,(void*)&data,BUFSIZ,0,0)==-1)
 29                 {
 30                         fprintf(stderr,"msgrcv failed with errno: %d\n", errno);
 31                         exit(EXIT_FAILURE);
 32                 }
 33                 printf("You wrote: %s\n",data.text);
 34                 if(strncmp(data.text,"end",3) == 0)
 35                 {
 36                         running = 0;
 37                 }
 38         }
 39         //删除消息队列
 40         if(msgctl(msgid,IPC_RMID,0)==-1)
 41         {
 42 
 43                 fprintf(stderr,"msgctl failed");
 44                 exit(EXIT_FAILURE);
 45         }
 46         exit(EXIT_SUCCESS);
 47 }
  1 #include <unistd.h>
  2 #include <stdlib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 #include <errno.h>
  6 #include <sys/msg.h>
  7 #define MAX_TEXT 512
  8 struct msg_st{
  9         long int msg_type;
 10         char text[MAX_TEXT];
 11 };
 12 int main()
 13 {
 14         int running = 1;
 15         int msgid = -1;
 16         struct msg_st data;
 17         char buffer[MAX_TEXT];
 18         //建立消息队列
 19         msgid = msgget((key_t)1234,0666|IPC_CREAT);
 20         if(msgid == -1)
 21         {
 22                 fprintf(stderr,"msgget failed with error: %d\n",errno);
 23                 exit(EXIT_FAILURE);
 24         }
 25         //向队列中写入消息,直到遇到end消息
 26         while(running)
 27         {
 28                 printf("Enter some text: ");
 29                 fgets(buffer, MAX_TEXT, stdin);
 30                 data.msg_type = 1;
 31                 strcpy(data.text,buffer);
 32                 if(msgsnd(msgid,(void*)&data,MAX_TEXT,0)==-1)
 33                 {
 34                         fprintf(stderr,"msgsnd failed\n");
 35                         exit(EXIT_FAILURE);
 36                 }
 37                 if(strncmp(data.text,"end",3) == 0)
 38                 {
 39                         running = 0;
 40                 }
 41                 sleep(1);
 42         }
 43         exit(EXIT_SUCCESS);
 44 }

在这里插入图片描述
在这里插入图片描述

一种正逐渐被淘汰的通信方式,完全可以用流管道或者套接口的方式来取代它。

共享内存

共享内存就是允许两个不相关的进程访问同一个逻辑内存。不同进程之间共享的内存通常安排在同一物理内存中。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。不过,共享内存并未提供同步机制,也就是,在第一个进程对共享内存的写操作结束之前,并无自动机制可以阻止第二个进程对它进行读取。

创建共享内存

#include <sys/shm.h>
int shmget(key_t key, int size, int flag);

第一个参数程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数运行成功时会返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数;调用失败返回-1。第二个参数,size以字节为单位指定需要共享的内存容量。第三个参数是权限标志,作用和open函数的mode参数一样。
将共享内存链接到自身的地址空间

void *shmat(int shmid, void* addr, int flag);

shmid为shmget函数返回的共享存储标识符。函数的返回值即是该进程数据段所链接的实际地址。

将共享内存从当前进程中分离

int shmdt(const void* shmaddr);

将共享内存分离并不是删除它,而是使共享内存对当前进程不可用。

sharememconsumer.cpp

  1 #include <unistd.h>
  2 #include <stdlib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 #include <sys/shm.h>
  6 #define TEXT_SZ 2048
  7 struct shared_use_st
  8 {
  9         int written;
 10         char text[TEXT_SZ];
 11 };
 12 int main()
 13 {
 14         int shmid;
 15         shmid = shmget((key_t)1234,sizeof(struct shared_use_st),0666|IPC_CREAT);
 16         if(shmid==-1)
 17         {
 18                 fprintf(stderr,"shmget failed\n");
 19                 exit(EXIT_FAILURE);
 20         }
 21         void* shared_memory = (void*)0;
 22         shared_memory = shmat(shmid,(void*)0,0);
 23         if(shared_memory==(void*)-1)
 24         {
 25                 fprintf(stderr,"shmat failed\n");
 26                 exit(EXIT_FAILURE);
 27         }
 28         printf("Memory attached at %X\n",(long)shared_memory);
 29         struct shared_use_st *shared_stuff;
 30         shared_stuff = (struct shared_use_st*)shared_memory;
 31         shared_stuff->written = 0;
 32         int running = 1;
 33         while(running)
 34         {
 35                 if(shared_stuff->written)
 36                 {
 37                         printf("You wrote: %s", shared_stuff->text);
 38                         sleep(1);
 39                         shared_stuff->written = 0;
 40                         if(strncmp(shared_stuff->text,"end",3)==0)
 41                         {
 42                                 running = 0;
 43                         }
 44                 }
 45         }
 46         if(shmdt(shared_memory)==-1)
 47         {
 48                 fprintf(stderr,"shmdt failed\n");
 49                 exit(EXIT_FAILURE);
 50         }
 51         if(shmctl(shmid,IPC_RMID,0)==-1)
 52         {
 53                 fprintf(stderr,"shmctl failed\n");
 54                 exit(EXIT_FAILURE);
 55         }
 56         exit(EXIT_SUCCESS);
 57 }

sharememproducer.cpp

  1 #include <unistd.h>
  2 #include <stdlib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 #include <sys/shm.h>
  6 #define TEXT_SZ 2048
  7 struct shared_use_st
  8 {
  9         int written;
 10         char text[TEXT_SZ];
 11 };
 12 int main()
 13 {
 14         int shmid;
 15         shmid = shmget((key_t)1234,sizeof(struct shared_use_st),0666|IPC_CREAT);
 16         if(shmid==-1)
 17         {
 18                 fprintf(stderr,"shmget failed\n");
 19                 exit(EXIT_FAILURE);
 20         }
 21         void* shared_memory = (void*)0;
 22         shared_memory = shmat(shmid,(void*)0,0);
 23         if(shared_memory==(void*)-1)
 24         {
 25                 fprintf(stderr,"shmat failed\n");
 26                 exit(EXIT_FAILURE);
 27         }
 28         printf("Memory attached at %X\n",(long)shared_memory);
 29         struct shared_use_st *shared_stuff;
 30         shared_stuff = (struct shared_use_st*)shared_memory;
 31         shared_stuff->written = 0;
 32         int running = 1;
 33         char buffer[BUFSIZ];
 34         while(running)
 35         {
 36                 while(shared_stuff->written == 1)
 37                 {
 38                         sleep(1);
 39                         printf("waiting for client...\n");
 40                 }
 41                 printf("Enter some text: ");
 42                 fgets(buffer,BUFSIZ,stdin);
 43                 strncpy(shared_stuff->text,buffer,TEXT_SZ);
 44                 shared_stuff->written = 1;
 45                 if(strncmp(buffer,"end",3)==0)
 46                 {
 47                         running = 0;
 48                 }
 49 
 50         }
 51         if(shmdt(shared_memory)==-1)
 52         {
 53                 fprintf(stderr,"shmdt failed\n");
 54                 exit(EXIT_FAILURE);
 55         }
 56         exit(EXIT_SUCCESS);
 57 }

在这里插入图片描述
在这里插入图片描述

信号量

当多个进程同时访问系统上的某个资源的时候,比如同时写一个数据库的某条记录,或者同时修改某个文件,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。即P(passeren 传递,就像进入临界区)和V(vrijgeven 释放 就像退出临界区)操作。
假设有信号量SV,则对它的P、V操作:

  • P(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行
  • V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1
    信号量的取值可以是任何自然数。但最常用的、最简单的信号量是二进制信号量,它只能取0和1这两个值。
    在这里插入图片描述
    当关键代码段可用时,二进制信号量SV的值为1,进程A和B都有机会进入关键代码段。如果此时进程A执行P(SV)操作将SV减1,则进程B若再执行P(SV)操作就会被挂起。直到进程A离开关键代码段,并执行V(SV)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待SV而处于挂起状态,则它将被唤醒。

共享内存是进程间通信的最快的方式,但是共享内存的同步问题自身无法解决(即进程该何时去共享内存取得数据,而何时不能取),但用SYSTEM V信号量可轻易解决这个问题(使用信号量解决共享内存的同步问题)。

创建和打开信号量集

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

该函数执行成功返回信号量标识符,失败则返回-1。参数key是函数通过调用ftok函数得到的键值,用来标识一个全局唯一的信号量集,nsems代表创建信号量的个数,如果只是访问而不创建则可以指定该参数为0;但一旦创建了该信号量,就不能更改其信号量个数。只要不删除该信号量,就可以重新调用该函数创建该键值的信号量,该函数只是返回以前创建的值,而不会重新创建。semflg指定该信号量的读写权限,它低端的9个比特是该信号量的权限,其格式和含义都与系统调用open的mode参数相同。

改变信号量的值,即执行P、V操作

int semop(int semid, struct sembuf *sops, unsigned nsops);

semid是由semget返回的信号量标识符,sembuf结构的定义如下:

struct sembuf{
	unsigned short int sem_num; 
	short int sem_op;     //信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作
	short int sem_flg;    //通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量
};

其中,sem_num成员是信号量集中信号量的编号,0表示信号量集中的第一个信号量。sem_op成员指定操作类型,其可选值为正整数、0和负整数。每种类型的操作的行为受到sem_flg成员的影响。

直接控制信号量信息

int semctl(int semid, int semnum, int cmd, ...);

cmd通常是SETVAL或IPC_RMID。SETVAL用来把信号量初始化为一个已知的值,值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。IPC_RMID用于删除一个已经无须继续使用的信号量标识符。如果有第4个参数,它通常是一个union semum结构,定义如下:

union semun{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
};

semreader.cpp

  1 #include <sys/types.h>
  2 #include <sys/ipc.h>
  3 #include <sys/sem.h>
  4 #include <stdio.h>
  5 #include <unistd.h>
  6 #include <stdlib.h>
  7 #include <string.h>
  8 #include <sys/shm.h>
  9 #include <errno.h>
 10 #define SEM_KEY 4001
 11 #define SHM_KEY 5678
 12 union semun{
 13         int val;
 14 };
 15 int main(void)
 16 {
 17         int semid,shmid;
 18         //创建共享内存
 19         shmid = shmget(SHM_KEY,sizeof(int),IPC_CREAT|0666);
 20         if(shmid < 0)
 21         {
 22                 printf("create shm error\n");
 23                 return -1;
 24         }
 25         void* shmptr = (void*)0;
 26         shmptr = shmat(shmid,NULL,0); //连接到自身空间中
 27         if(shmptr == (void*)-1)
 28         {
 29                 printf("shmat error: %s\n",strerror(errno));
 30                 return -1;
 31         }
 32         int* data = (int*)shmptr;
 33         //创建一个semid,有两个信号量
 34         semid = semget(SEM_KEY,2,IPC_CREAT|0666);
 35         union semun semun1;
 36         semun1.val = 0;
 37         semctl(semid,0,SETVAL,semun1);
 38         semun1.val = 1;
 39         semctl(semid,1,SETVAL,semun1);
 40         struct sembuf sembuf1;
 41         while(1)
 42         {
 43                 sembuf1.sem_num = 0; //sem_num指的是下面操作指向第一个信号量
 44                 sembuf1.sem_op = -1; //设置为-1为等待
 45                 sembuf1.sem_flg = SEM_UNDO;
 46                 semop(semid,&sembuf1,1); //阻塞,直到收到信号
 47                 printf("the num: %d\n",*data);
 48                 sembuf1.sem_num = 1; //sem_num指的是下面操作指向第二个信号量
 49                 sembuf1.sem_op = 1; //设置为1为发送信号
 50                 sembuf1.sem_flg = SEM_UNDO;
 51                 semop(semid,&sembuf1,1);
 52         }
 53         return 0;
 54 }

semwriter.cpp

  1 #include <sys/types.h>
  2 #include <sys/ipc.h>
  3 #include <sys/sem.h>
  4 #include <stdio.h>
  5 #include <unistd.h>
  6 #include <stdlib.h>
  7 #include <string.h>
  8 #include <sys/shm.h>
  9 #include <errno.h>
 10 #define SEM_KEY 4001
 11 #define SHM_KEY 5678
 12 union semun{
 13         int val;
 14 };
 15 int main(void)
 16 {
 17         int semid,shmid;
 18         //创建共享内存
 19         shmid = shmget(SHM_KEY,sizeof(int),IPC_CREAT|0666);
 20         if(shmid < 0)
 21         {
 22                 printf("create shm error\n");
 23                 return -1;
 24         }
 25         void* shmptr = (void*)0;
 26         shmptr = shmat(shmid,NULL,0); //连接到自身空间中
 27         if(shmptr == (void*)-1)
 28         {
 29                 printf("shmat error: %s\n",strerror(errno));
 30                 return -1;
 31         }
 32         int* data = (int*)shmptr;
 33         //获得一个semid,有两个信号量
 34         semid = semget(SEM_KEY,2,0666);
 35         union semun semun1;
 36         struct sembuf sembuf1;
 37         while(1)
 38         {
 39                 sembuf1.sem_num = 1; //sem_num指的是下面操作指向第二个信号量
 40                 sembuf1.sem_op = -1; //设置为-1为等待,因为第2个信号量初始值为1,所以下面不会>    阻塞
 41                 sembuf1.sem_flg = SEM_UNDO;
 42                 semop(semid,&sembuf1,1);
 43                 scanf("%d",data);
 44                 sembuf1.sem_num = 0; //sem_num指的是下面操作指向第一个信号量
 45                 sembuf1.sem_op = 1; //设置为1为发送信号
 46                 sembuf1.sem_flg = SEM_UNDO;
 47                 semop(semid,&sembuf1,1);
 48                 //执行加1后,reader阻塞是由于第一个信号量为0,无法减1,而现在加1后,reader就绪
    后writer继续循环,发现第二个信号量已经减为0,则阻塞了
 49         }
 50         return 0;
 51 }

在这里插入图片描述
在这里插入图片描述

dbus

上面三种System V IPC进程间通信方式都使用一个全局唯一的键值来描述一个共享资源。当程序调用semget、shmget或msgget时,就创建了这些共享资源的一个实例。Linux提供了ipcs命令,以观察当前系统上拥有哪些共享资源实例。可以使用ipcrm命令删除遗留在系统中的共享资源。
ipcs -a 用于列出本用户所有相关的ipcs参数
ipcs -q 用于列出进程中的消息队列
ipcs -s 用于列出所有信号量
ipcs -m 用于列出所有的共享内存信息
ipcs -l 用于列出系统限额
ipcs -t 用于列出最后的访问时间
ipcs -u 用于列出当前使用情况

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux编程从入门到精通》是一本介绍Linux编程知识的重要参考书籍。它系统地介绍了Linux操作系统的基本原理和应用,同时深入讲解了Linux下的编程技术。该书的主要内容包括:Linux系统概述、Shell编程、C语言编程、进程和线程管理、网络编程、设备驱动编程等。 《Linux编程从入门到精通》首先介绍了Linux系统的基础知识,包括Linux发行版的选择和安装,文件系统的操作等。然后详细讲解了Shell编程,介绍了Shell脚本的基本语法和常用命令,以及如何编写简单的Shell脚本来完成自动化任务。 接着,《Linux编程从入门到精通》讲解了C语言在Linux下的应用。它介绍了C语言的基本语法和编程技巧,并通过实例演示了如何使用C语言编写Linux下的应用程序。例如,如何编写一个简单的文本编辑器或者一个网络聊天程序。 在进程和线程管理方面,《Linux编程从入门到精通》详细介绍了Linux下的进程管理和线程管理技术。它讲解了进程创建、运行和退出过程,以及进程间通信的各种方法。同时,还介绍了线程的创建和管理方法,并讲解了线程同步和互斥的技术。 此外,《Linux编程从入门到精通》还深入讲解了网络编程和设备驱动编程等高级主题。它介绍了Linux网络编程的基本原理和常用技术,如套接字编程和网络通信协议。在设备驱动编程方面,该书介绍了Linux设备驱动的框架和开发过程,以及如何编写一个简单的字符设备驱动程序。 总之,《Linux编程从入门到精通》是一本常实用的Linux编程指南。通过学习这本书,读者可以系统地了解Linux编程的基本知识和技巧,并能够熟练地编写各种类型的Linux程序。无论是初学者还是有一定经验的开发人员,都可以从这本书中受益匪浅。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值