嵌入式学习之Linux系统编程篇

326 阅读31分钟

嵌入式学习之Linux系统编程篇

以下内容整理自:【北京迅为】嵌入式学习之Linux系统编程篇_哔哩哔哩_bilibili

本章讲述编写 Linux 系统应用层软件常用的一些技术,包括文件 IO,标准 IO,进程 线程操作。这些运行在系统应用层的程序直接与内核和系统核心库进行交互,只能在 Linux 上运行,不能跨平台,也就是不能运行在其他操作系统上(比如 windows)。

学习系统编程可以使用 man 手册查看 API,查找用到的头文件,如“man 2 open”,使用“top”等命 令查看进程状态。本文档主要通过实验例程来说明各系统调用 API 和各种机制。

Linux 根据 UNIX 发展而来,属于类 UNIX 操作 系统,拥有 UNIX 特点,但是 Linux 作为开源软件更专注实用功能,支持更多的系统调用,从而拥有更多的新特性。

1. 什么是Linux系统编程?

Linux 系统编程也叫 Linux 下的高级编程。是介于应用层和驱动层之间的,内核向用户提供的接口

2. 统编程基本程序框架

首先来编写下 Linux 系统编程的基本程序框架,新建 test.c 文件,内容如下所示:

#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
	//argc:表示的是命令行中参数的个数。
	//argv:表示的是命令行中的参数
	int i;
	printf("argc is %d\n",argc);
	for(i=0;i<argc;i++)
	{
		printf("argv[%d] is %s\n",i,argv[i]);
	};
	return 0;
}

我们在学习 Linux 系统编程的时候,大多数 main 函数都是带参数的,因为我们要配合命令行来给我们的程序传参数。大部分情况下,main 函数的参数为 int argc,char *argv[]。 argc 表示的命令行中参数的个数argv 表示的是命令行中的参数

3. 标准IO和文件IO

标准 IO 和文件 IO 常用 API 如下:

文件 IO 是 Linux 系统提供的接口,针对文件和磁盘进行操作,不带缓存机制

标准 IO 是 C 语言函数库里的标准 I/O 模型,在 stdio.h 中定义,通过缓冲区操作文件,带缓存机制。

Linux 系统中一切皆文件,包 括普通文件,目录,设备文件(不包含网络设备),管道,fifio 队列,socket 套接字等,在终端输入“ls -l” 可查看文件类型和权限。

文件 IO 是直接调用内核提供的系统调用函数,头文件是 unistd.h,标准 IO 是间接调用系统调用函数, 头文件是 stdio.h文件 IO 是依赖于 Linux 操作系统的,标准 IO 是不依赖操作系统的,所以在任何的操作系统下,使用标准 IO,也就是 C 库函数操作文件的方法都是相同的。

4. 文件IO函数

文件描述符:对于文件IO来说,一切都是围绕文件描述符来进行的。在Linux系统

中,所有打开的文件都有一个对应的文件描述符。文件描述符的本质是-一个非负整数,当我们打开一个文件时,系统会给我们分配一个文件描述符。当我们对一个文件做读写操作的时候,我们使用open函数返回的这个文件描述符会标识该文件,并将其作为参数传递给read或者write函数

在posix. 1应用程序里面,文件描述符0,1,2分 别对应着标准输入,标准输出,标准出错。

4.1 文件IO—open函数

man 2 open //查看函数信息
函数原型:
int open(const char *pathname, int flags) 
int open(const char *pathname, int flags, mode_t mode)
参数:
const char *pathname:路径和文件名
flag:文件打开方式,可用多个标志位按位或设置
mode:权限掩码,对不同用户和组设置可执行,读,写权限,使用八进制数表示,此参
数可不写

返回值:open()执行成功会返回 int 型文件描述符,出错时返回-1
作用:通过系统调用,可以打开文件,并返回文件描述符
参数 flags可选标志:
O_CREAT: 	要打开的文件名不存在时自动创建改文件。//命令中使用该标志时,需要提供3个参数
O_EXCL:		要和 O_CREAT 一起使用才能生效,如果文件存在则 open()调用失败。

O_RDONLY:	只读模式打开文件。
O_WRONLY:	只写模式打开文件。
O_RDWR:		可读可写模式打开文件。//多个flag可使用逻辑或 | 组合使用

O_APPEND:	以追加模式打开文件。
O_NONBLOCK:	以非阻塞模式打开。

文件的权限:可以使用数字来表示

无权限:0
可执行:1
可写:2
可读:4
可读可写:2+4=6
可读可写可执行:1+2+4=7

eg:

首先使用:man 2 open 命令查看open命令需要包含的头文件

man 2 open  //结果如下

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

程序:

/*test.c*/

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd;//保存文件描述符

    fd = open("a.c", O_CREAT | O_RDWR, 0666);
    if(fd<0){
        printf("open is error\n");
    }
    printf("fd is %d\n", fd);
    return 0;
}

运行结果:

创建的 a.c 文件权限:644 = 666 & (umask的值(022))取反 = 666 & 755 = 644

4.2 文件IO—close函数

函数原型:int close(int fd)
头文件:#include <unistd.h>
参数fd:文件描述符
返回值:成功返回 0;错误返回-1

程序:

/*test.c*/

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;//保存文件描述符

    fd = open("a.c", O_CREAT | O_RDWR, 0666);
    if(fd<0){
        printf("open is error\n");
    }
    printf("fd is %d\n", fd);
    
    close(fd);
    
    return 0;
}

4.3 文件IO—read函数

man 2 read
函数原型:ssize_t read(int fd, void *buf, size_t count)
头文件:#include <unistd.h>
参数 fd:要读的文件描述符
参数 buf:缓冲区,存放读到的内容
参数 count:每次读取的字节数
返回值:返回值大于 0,表示读取到的字节数;等于 0 在阻塞模式下表示到达文件末尾或没有数据可读(EOF),并调用阻塞;等于-1 表示出错,在非阻塞模式下表示没有数据可读。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	int fd;
	char buf[32] = {0};
	ssize_t ret;
	fd = open("a.c", O_CREAT | O_RDWR, 0666);
	if (fd < 0)
	{
		printf("open is error\n");
		return -1;
	}
	printf("fd is %d\n", fd);
	ret = read(fd, buf, 32);  			//read函数
	if (ret < 0)
	{
		printf("read is error\n");
		return -2;
	}
	printf("buf is %s\n", buf);
	printf("ret is %ld\n", ret);
	close(fd);
	return 0;
}

4.4 文件IO—write函数

函数原型:ssize_t write(int fd, const void *buf, size_t count);
头文件:#include <unistd.h>
参数 fd:文件描述符;
参数 buf:缓存区,存放将要写入的数据
参数 count:每次写入的个数
功能:每次从 buf 缓存区拿count 个字节写入 fd 文件
返回值:大于或等于 0 表示执行成功,返回写入的字节数;返回-1 代表出错
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
	int fd;
	char buf[32]={0};
	ssize_t ret;
	fd=open("a.c",O_CREAT|O_RDWR,0666);
	if(fd<0)
	{
		printf("open is error\n");
		return -1;
	}
	write(1,"hello\n",6);
	close(fd);
	return 0;
}

4.5 文件IO—lseek函数

函数原型:off_t lseek(int fd, off_t offset, int whence);
头文件:#include <sys/types.h>
	   #include <unistd.h>
参数 fd:文件描述符;
参数 off_t offset:偏移量,单位是字节的数量,可以正负,
				  如果是负值表示向前移动;如果是正值,表示向后移动。

参数 whence:当前位置的基点,可以使用以下三组值。
			SEEK_SET:相对于文件开头
			SEEK_CUR:相对于当前的文件读写指针位置
			SEEK_END:相对于文件末尾
功能:移动文件读写指针;获取文件长度;拓展文件空间
返回值:成功返回当前位移,失败返回-1
把文件位置指针设置为 100lseek(fd,100,SEEK_SET);
把文件位置设置成文件末尾 : lseek(fd,0,SEEK_END);
确定当前的文件位置:       lseek(fd,0,SEEK_CUR)

5. 目录IO函数

目录IO都是对目录的操作。

5.1 创建目录—mkdir函数

功能:创建一个目录

man 2 mkdir

函数原型:int mkdir(const char *pathname, mode_t mode);
头文件:#include <sys/stat.h>
       #include <sys/types.h>
参数 pathname:路径和文件名
参数 mode:权限掩码umask,对不同用户和组设置可执行、读、写权限,使用八进制数表示,此参数
可不写。
返回值:mkdir()执行成功会返回 0,出错时返回-1
#include <stdio.h>
#include <stdlib.h>

#include <sys/stat.h>
#include <sys/types.h>

int main(int argc, char *argv[])
{
	int ret;
	if (argc != 2)
	{
		printf("Usage:%s <name file>\n", argv[0]);
		return -1;
	}
	ret=mkdir(argv[1],0666);	//创建一个目录
	if(ret<0)
	{
		printf("mkdir is error\n");
	}
	printf("mkdir is ok\n");
	return 0;
}

5.2 打开目录—opendir函数

功能:打开指定的目录,并返回 DIR*形态的目录流

man 3 opendir

函数原型:DIR *opendir(const char *name);
         DIR *fdopendir(int fd);
头文件:#include <sys/types.h>
       #include <dirent.h>
参数 name:路径名字
返回值:成功返回打开的目录流,失败返回 NULL

5.3 关闭目录—closedir函数

功能:关闭目录流

man 3 closedir

函数原型:int closedir(DIR *dirp);

头文件:#include <sys/types.h>
       #include <dirent.h>
参数 drip:要关闭的目录流指针
返回值:成功返回0,失败返回-1
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

#include <sys/types.h>
#include <dirent.h>

int main(int argc, char *argv[])
{
	int ret;
	DIR *dp;
	if (argc != 2)
	{
		printf("Usage:%s <name file>\n", argv[0]);
		return -1;
	}
	dp = opendir(argv[1]);	//打开指定的目录,并返回 **DIR***形态的目录流
	if (dp != NULL)
	{
		printf("opendir is ok\n");
		return -1;
	}
	closedir(dp);	//关闭目录流
	return 0;
}

5.4 读目录—readdir函数

功能:用来读一个目录

man 3 readdir

函数原型:struct dirent *readdir(DIR *dirp);
struct dirent {
               ino_t          d_ino;       /* Inode number */
               off_t          d_off;       /* Not an offset; see below */
               unsigned short d_reclen;    /* Length of this record */
               unsigned char  d_type;      /* Type of file; not supported
                                              by all filesystem types */
               char           d_name[256]; /* 文件名 */
           };
头文件:#include <dirent.h>
参数 DIR *dirp:要读取的目录流指针
返回值:成功返回读取到的目录流指针,失败返回 NULL
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>

int main(int argc, char *argv[])
{
	int ret;
	DIR *dp;
	struct dirent *dir;
	if (argc != 2)
	{
		printf("Usage:%s <name file>\n", argv[0]);
		return -1;
	}
	dp = opendir(argv[1]);
	if (dp == NULL)
	{
		printf("opendir is error\n");
		return -2;
	}
	printf("opendir is ok\n");
	while (1)//文件是通过链表存放的
	{
		dir = readdir(dp);
		if (dir != NULL)
		{
			printf("file name is %s\n", dir->d_name);
		}
		else
			break;
	}
	closedir(dp);
	return 0;
}

5.5 综合实验

/*实验要求
在综合练习 1 的基础上,利用我们本阶段学习的知识,修改综合练习 1 的代码,增加以下需求:
1.打印我们要拷贝的目录下的所有文件名,并拷贝我们需要的文件。
2.通过键盘输入我们要拷贝的文件的路径和文件名等信息*/

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>		//导入所需的头文件

int main(int argc, char *argv[])
{
    int fd_src,fd_obj;  //定义文件描述符
    char buf[32] = {0}; //定义文件数据缓冲区
    ssize_t ret;		//接受read函数返回值

    DIR *dp;			//DIR*形态的目录流
    struct dirent *dir;	//目录流指针

    char file_path[32];	//文件路径
    char file_name[32];	//文件名

    printf("Please enter the file_path:");
    scanf("%s", file_path);

    dp = opendir(file_path);  //打开目录
    if (dp == NULL)
    {
        printf("opendir is error\n");
        return -2;
    }
    printf("opendir is ok\n");

    while (1)  //读取目录中的文件
    {
        dir = readdir(dp);
        if (dir != NULL)
        {
            printf("file name is %s\n", dir->d_name);
        }
        else
            break;
    }

    printf("Please enter the file name you want to copy:");  //读取要复制的文件名
    scanf("%s", file_name);

    fd_src = open(strcat(strcat(file_path, "/"), file_name),O_RDWR);  //打开要复制的文件
    if (fd_src < 0)
    {
        printf("file open error\n");
        return -1;
    }

    fd_obj = open(file_name, O_CREAT | O_RDWR, 0666);  //创建文件副本
    if (fd_src < 0)
    {
        printf("file creat error\n");
        return -1;
    }

    while ((ret = read(fd_src, buf, 32)) != 0)  //读取文件并复制
    {
        write(fd_obj, buf, ret);
    }
    printf("file copy success!\n");
    close(fd_obj);
    close(fd_src);
    closedir(dp);
    return 0;
}

6. 库

6.1 库的基本概念

库是一种可执行的二进制文件,是编译好的代码。使用库可以提高开发效率。在 Linux 下有静态库和动 态库。在 Linux 下有静态库和动态库,两者加载时间不同。

静态库在程序编译的时候会被链接到目标代码里面。所以程序在运行的时候不再需要静态库了。因此编译出来的体积就比较大。以 lib 开头,以 .a 结尾。eg: libmylib.a

动态库(动态库也叫共享库)在程序编译的时候不会被链接到目标代码里面,而是在程序运行的时候被载入的。所以程序在运行的时候需要动态库了。因此编译出来的体积就比较小。以 lib 开头,以 .so 结尾。eg: libmylib.so

多个子程序使用到同一个库时用动态库合适,否则需要多次链接静态库

程序需要移植时,适合用静态库。

6.2 静态库的制作和使用

  1. 编写或准备库的源代码
/*mylib.c*/
#include<stdio.h>
void mylib(void);
void mylib(void)
{
	printf("This is mylib\n");
}
  1. 将源码.c 文件编译生成.o 文件
gcc -c mylib.c  //生成mylib.o文件
  1. 使用 ar 命令创建静态库
ar cr libmylib.a mylib.o  //
参数:c:创建  
	 r:覆盖
	 libmylib.a:库文件名
	 mylib.o:创建库所使用的文件
  1. 测试库文件

编写测试代码 test1.c

#include <stdio.h>
void mylib(void);
int main(void)
{
	mylib();
	return 0;
}

编译测试文件 test1.c

gcc test1.c -l mylib -L .
// -l 后面跟所用的静态库名
// -L 后面跟所用静态库的查找位置

运运行测试 a.out

6.3 动态库的制作和使用

  1. 编写或准备库的源代码
/*mylib.c*/
#include<stdio.h>
void mylib(void);
void mylib(void)
{
	printf("This is mylib\n");
}
  1. 将源码.c 文件编译生成.o 文件
gcc -c -fpic mylib.c  //-fpic:生成位置无关代码
  1. 使用 gcc 命令创建动态库
gcc -shared -o libmylib.so mylib.o  //-shared:生成动态库
  1. 测试库文件

gcc test1.c -l mylib -L .  //生成a.out文件
// -l 后面跟所用的动态库名
// -L 后面跟所用动态库的查找位置

运行a.out文件提示报错:

解决方法:

  1. 第一种方法: 将生成的动态库拷贝到/lib 或者/usr/lib 里面去,因为系统会默认去这俩个路径下寻找。(不建议)
  2. 把我们的动态库所在的路径加到环境变量里面去,比如我们动态库所在的路径为/home/test,我们就可 以这样添加,但是这种方法只在当前设置的窗口有效
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/dlxy/sambashare/嵌入式学习之Linux系统编程篇
  1. 修改 ubuntu 下的配置文件/etc/ld.so.conf,我们在这个配置文件里面加入动态库所在的位置,然后使用 命令 ldconfig 更新目录。
vi /etc/ld.so.conf

ldconfig

7. 进程

7.1 进程的基本知识

进程:指正在运行的程序,是资源分配的最小单位,可以通过“ps ”或“top”等命令查看正在运行的进程。

线程是系统的最小调度单位,一个进程可以拥有多个线程,同一进程里的线程可以共享此进程的同一资源

每个进程都有一个唯一的标识符,既进程 ID,简称 pid。

7.2 进程创建

常用函数:

函数原型:pid_t fork(void);
功能:系统调用,创建一个进程
头文件:#include <unistd.h>
返回值:调用成功父进程返回子进程号,子进程返回 0,失败返回-1
函数原型:pid_t getpid(void);
功能:获取此进程 PID
头文件:#include <sys/types.h>
	   #include <unistd.h>
返回值:PID 号
函数原型:pid_t getppid(void);
功能:获取父进程 PID
头文件:#include <sys/types.h>
	   #include <unistd.h>
返回值:PID 号

实验代码:

#inlcude <stdio.h>
#include <unistd.h>
int main(void)
{
	pid_t pid;
	pid = fork();
	if (pid<0)  //判断子进程创建是否成功
	{
		print("fork is error \n");
	}
	
	if (pid > 0)  //判断是否是父进程
	{
		printf("This is parent, parent pid is %d\n", getpid());
	}
	if (pid == 0)  //判断是否是子进程
	{
		printf("This is child,child pid is %d,parent pid is %d\n", getpid(), getppid());
	}
	return 0;
}

在 Ubuntu 上编译运行,打印进程号如下图所示:

7.3 exec函数族

用 fork 函数创建子进程后,子进程往往要调用一种 exec 函数以执行另一个程序,该子进程被新的程序替换(程序内容发生替换),改变地址空间,进程映像和一些属性,但是 pid 号不变。

函数原型:int execve(const char *filename, char *const argv[], char *const envp[]);
头文件:#include <unistd.h>
参数 filename:路径名,表示载入进程空间的新程序路径
参数 argv[]:命令行参数,argv[0]为命令名
参数:envp[]:新程序的环境变量
返回值:成功时不会返回,使用时不用检查返回值,可以通过errno检查

以下函数都是根据 execve 实现:

int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
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[]);

实验代码:创建子进程,子进程使用 execl 打印 hello world

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
	int i=0;
	pid_t pid;
	pid = fork();
	if (pid < 0)
	{
		printf("fork is error \n");
		return -1;
	}
	//父进程
	if (pid > 0)
	{
		printf("This is parent,parent pid is %d\n", getpid());
	}
	//子进程
	if (pid == 0)
	{
	printf("This is child,child pid is %d\n", getpid(), getppid());
	//改为自己的路径
	execl("/home/samba/linux/15/hello","hello",NULL);
	exit(1);
	}
	i++;
	printf("i is %d\n",i);
	return 0;
}

上述代码24行中的hello可执行文件由hello.c文件编译而成

/*hello.c*/
#include <stdio.h>
#include <unistd.h>
int main(void)
{
	printf("hello world\n");
	return 0;
}

执行结果:

7.4 ps和kill命令

ps命令:ps命令可以列出系统中当前运行的哪些进程。
命令格式:ps [参数]
命令功能:用来显示当前进程的状态
常用参数: aux

ps -aux命令详解 - dion至君 - 博客园 (cnblogs.com)

ps a 	显示现行终端机下的所有程序,包括其他用户的程序。
ps -A 	显示所有程序。
ps u   以用户为主的格式来显示程序状况。
ps x   显示所有程序,不以终端机来区分。
进程的状态:
D: 无法中断的休眠状态 (通常 IO 的进程)
R: 正在执行中
S: 静止状态
T: 暂停执行
Z: 不存在但暂时无法消除
W: 没有足够的记忆体分页可分配
<: 高优先序的行程
N: 低优先序的行程
L: 有记忆体分页分配并锁在记忆体内 (实时系统或捱 A I/O)
kill命令:kill 命令用来杀死进程
eg:kill -9 pid

命令功能:用来显示当前进程的状态
常用参数: aux

如下所示,使用命令“ps aux | grep a.out”查找到**./a.out** 的进程号为 3179,然后输入“kill -9 3179”结束此进程

ps aux | grep a.out
kill -9 3179

7.5 孤儿进程和僵尸进程

孤儿进程:父进程结束以后,子进程还未结束,这个子进程就叫做孤儿进程。

僵尸进程:子进程结束以后,父进程还在运行,但是父进程不去释放进程控制块,这个子进程就叫做僵尸进程。

7.6 wait函数

wait()函数一般用在父进程中等待回收子进程的资源,而防止僵尸进程的产生

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

与 wait 函数的参数有关的两个宏定义

WIFEXITED(status):如果子进程正常退出,则该宏定义为真
WEXITSTATUS(status):如果子进程正常退出,则该宏定义的值为子进程的退出值

实验代码:使用 wait()函数,防止僵尸进程的产生

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(void)
{
	pid_t pid;
	pid = fork();
	if(pid < 0){
		printf("error\n");
	}
	if(pid > 0)
	{
		int status;
		wait(&status);  
		if(WIFEXITED(status)==1)  //如果子进程正常退出,该宏定义的值为子进程的退出值
		{
			printf("return value is %d\n",WEXITSTATUS(status));
		}
		while(1);
	}
	if(pid == 0)
	{
		sleep(2);
		printf("This is child\n");
		exit(6);
	}
	return 0;
}

实验结果:

7.7 守护进程

7.7.1 什么是守护进程:

守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务

  1. 很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭
  2. 另一些只在需要的时候才启动完成任务后就自动结束

用户使守护进程独立于所有终端是因为,在守护进程从一个终端启动的情况下,这同一个终端可能被其他的用户使用。例如,用户从一个终端启动守护进程后退出,然后另外一个人也登录到这个终端。用户不希望后者在使用该终端的过程中,接收到守护进程的任何错误信息。同样,由终端键入的任何信号(例如 中断信号)也不应该影响先前在该终端启动的任何守护进程的运行。虽然让服务器后台运行很容易(只要 shell 命令行以&结尾即可),但用户还应该做些工作,让程序本身能够自动进入后台,且不依赖于任何终端。

守护进程没有控制终端,因此当某些情况发生时,不管是一般的报告性信息,还是需由管理员处理的紧急信息,都需要以某种方式输出。Syslog 函数就是输出这些信息的标准方法,它把信息发送给 syslogd 守护进程。

7.7.2 创建一个守护进程

守护进程的基本要求:

  1. 必须作为我们 init 进程的子进程
  2. 不跟控制终端交互。

步骤:

  1. 使用 fork 函数创建一个新的进程,然后让父进程使用 exit 函数直接退出(必须要的)

  2. 调用 setsid 函数,摆脱控制终端。(必须要的)

  3. 调用 chdir 函数将当前的工作目录改成根目录,增强程序的健壮性。(不是必须要的)

  4. 重设我们 umask 文件掩码,增强程序的健壮性和灵活性(不是必须要的)

  5. 关闭文件描述符,节省资源(不是必须要的)

  6. 执行我们需要执行的代码(必须要的)

实验代码:

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

int main(void)
{
	pid_t pid;
	//步骤一:创建一个新的进程
	pid = fork();
	//父进程直接退出
	if (pid > 0)
	{
		exit(0);
	}
	//子进程
	if (pid == 0)
	{
		//步骤二:调用 setsid 函数摆脱控制终端
		setsid();
		
		//步骤三:更改工作目录
		chdir("/");
		
		//步骤四:重设umask文件掩码
		umask(0);
		
		// 步骤五:关闭0 1 2 三个文件描述
		close(1);
		close(2);
		close(3);
		
		// 步骤六:执行我们要执行的代码
		while (1)
		{
		}
	}
	
}

8. 通信

进程间的通信应用也是很广泛的,比如后台进程和 GUI 界面数据传递,发送信号关机,Ctrl+C 终止正在 运行的程序等。

Linux 进程间通信机制分三类数据交互,同步,信号。理解了这些机制才能灵活运用操作系统提供的 IPC 工具。

本章以常用的管道(包括有名管道和无名管道),System V IPC消息队列,共享内存,信号灯)套接字 (UNIX 域套接字和网络套接字)为例来说明 Linux 进程通信常用的方法,本文档中介绍的只是一小部分, 如果想深入了解可以去翻看专业的书籍

8.1 管道通信之无名管道

无名管道是最古老的进程通信方式,有如下两个特点:

  1. 只能用于有关联的进程间数据交互,如父子进程,兄弟进程,子孙进程,在目录中看不到管道文件节点,读写文件描述符存在一个 int 型数组中。
  2. 只能单向传输数据,即管道创建好后,一个进程只能进行读操作,另一个进程只能进行写操作, 读出来字节顺序和写入的顺序一样。
函数原型:int pipe(int pipefd[2]);
功能:创建无名管道
头文件:#include <unistd.h>
参数 pipefd[2]:一个 int 型数组,表示管道的文件描述符,pipefd[0]为读,pipefd[1]为写,如下图所示:
返回值:成功返回 0,失败返回-1

无名管道使用步骤:

  1. 调用 pipe()创建无名管道;
  2. fork()创建子进程,一个进程读,使用 read(),一个进程写,使用 write()

实验代码:实现子进程和父进程之间的通信,创建无名管道,父进程从终端获取数据,写入管道,子进程从管道读数据并打印出来

#include <stdio.h>
#include <unisted.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(void)
{
	char buf[32] = {0};
	pid_t pid;
	int fd[2];//定义一个变量来保存文件描述符,因为一个读端,一个写端,所以数量为 2 个
	
	pipe(fd);//创建无名管道
	print("fd[0] is %d\n", fd[0]);
	print("fd[1] is %d\n", fd[1]);
	
	pid = fork();//创建子进程
	if (pid < 0)
	{
		printf("fork error\n");
	}
	if (pid > 0)
	{
		int status;
		close(fd[0]);//在父进程中关闭读端
		write(fd[1], "hello", 5);//在父进程中通过写端向管道写数据
		close(fd[1]);//写完数据关闭写端
		wait(&status);//等待子进程退出
		exit(0);
	}
	if (pid == 0)
	{
		close(fd[1]);//关闭写端
		read(fd[0], buf, 32);//从读端读信息
		printf("buf is %s\n", buf);
		close(fd[0]);//关闭读端
		exit(0);
	}
	return 0;
}

结果如下:

8.2 管道通信之有名管道

有名管道中可以很好地解决在无关进程间数据交换的要求,并且由于有名管道是存在于文件系统中的,这也提供了一种比无名管道更持久稳定的通信办法。有名管道在一些专业书籍中叫做命名管道,它的特点是

  1. 可以使无关联的进程通过 fifo 文件描述符进行数据传递;
  2. 单向传输有一个写入端和一个读出端,操作方式和无名管道相同。

创建有名管道:

函数原型:int mkfifo(const char *pathname, mode_t mode);
头文件:#include <sys/types.h>
		#include <sys/stat.h>
参数 pathname: 有名管道的路径
参数 mode: 权限
返回值 成功返回 0,失败返回-1

有名管道使用步骤:

  1. 使用 **mkfifo()**创建 fifo 文件描述符。
  2. 打开管道文件描述符。
  3. 通过读写文件描述符进行单向数据传输。

实验代码

/*ifo_write.c*/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
	int ret;
	char buf[32] = {0};
	int fd;
	if (argc < 2)
	{
		printf("Usage:%s <fifo name>\n", argv[0]);
		return -1;
	}
	
	if (access(argv[1], F_OK) == 1)
	{
		ret = mkfifo(argv[1], 0666);
		if (ret == -1)
		{
			printf("mkfifo error\n");
			return -2;
		}
		printf("mkfifo success\n");
	}
	fd = open(argv[1], O_WRONLY);
	while(1)
	{
		sleep(1);
		write(fd, "hello", 5);
	}
	close(fd);
	return 0;
}
/*fifo_read.c*/

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

int main(int argc, char *argv[])
{
	char buf[32] = {0};
	int fd;
	if (argc < 2)
	{
		printf("Usage:%s <fifo name> \n", argv[0]);
		return -1;
	}
	fd = open(argv[1], O_RDONLY);
	while (1)
	{
		sleep(1);
		read(fd, buf, 32);
		printf("buf is %s\n", buf);
		memset(buf, 0, sizeof(buf));//memset:在一段内存块中填充某个给定的值
	}
	close(fd);
	return 0;
}

也可以在命令行中使用命令创建管道文件,并查看:

mkfifo fifo
ls
ls -al

8.3 信号通信

信号是 Linux 系统响应某些条件而产生的一个事件,接收到该信号的进程会执行相应的操作。

8.3.1 信号发送

信号的产生有三种方式:

  1. 硬件产生,如从键盘输入 Ctrl+C 可以终止当前进程
  2. 其他进程发送,如可在 shell 进程下,使用命令 kill -信号标号 PID,向指定进程发送信号。
  3. 异常,进程异常时会发送信号

在 Ubuntu 终端输入 kill -l,查看所有信号

kill -l

几个常用的函数:

函数原型:int kill(pid_t pid, int sig);
头文件:用于向任何进程组或进程发送信号
参数 pid:大于 0,时为向 PID 为 pid 的进程发送信号
		 等于 0,向同一个进程组的进程发送信号;
		 等于-1,除发送进程自身外,向所有进程 ID 大于 1 的进程发送信号。
		 小于-1,向组 ID 等于该 pid 绝对值的进程组内所有进程发送信号。
参数 sig:设置发送的信号;
		 等于 0 时为空信号,无信号发送。常用来进行错误检查
返回值:执行成功时,返回值为 0;错误时,返回-1,并设置相应的错误代码 errno

函数原型:int raise(int sig);
功能:向进程自身发送信号,相当于 kill(getpid(),sig)
头文件:#include <signal.h>
参数 sig:信号
函数原型:unsigned int alarm(unsigned int seconds);
功能:设定的时间超过后产生 SIGALARM 信号,默认动作是终止进程
头文件:#include <unistd.h>
参数:设定的时间
注意:每个进程只能有一个 alarm()函数,时间到后要想再次使用要重新注册。

实验一代码:在程序中实现自己给自己发送信号。

/*raise.c*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(void)
{
	printf("raise before\n");
	raise(9);//程序执行到此处就退出了。
	printf("raise after\n");
	return 0;
}

实验二代码:杀死特定进程

/*kill.c*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
	pid_t pid;
	int sig;
	if(argc < 3)
	{
		printf("Usage:%s <pid_t> <signal>\n",argv[0]);
		return -1;
	}
	sig = atoi(argv[2]);//把参数 str 所指向的字符串转换为一个整数
	pid = atoi(argv[1]);
	kill(pid,sig);
	return 0;
}

实验二代码:待杀死的目标代码

/*test.c*/

#include <stdio.h>
#include <unistd.h>
void main(void)
{
	while(1)
	{
		sleep(1);
		printf("hello world\n");
	}
}

先编译运行test.c,程序循环打印hello world,查看test进程号,运行kill程序杀死test程序

实验三代码:alarm.c

/*alarm.c*/

int main(int argc,char *argv[])
{
	int i;
	alarm(3);
	while(1)
	{
		sleep(1);
		i++;
		printf("i = %d\n",i);
	}
	return 0;
}

编译运行alarm.c,程序运行3秒后自动终止

8.3.2 信号接收

接收信号:如果要让我们接收信号的进程可以接收到信号,那么这个进程就不能停止。让进程不停止有三种方法:

while();
sleep();
pause();

方法一:while.c

#include <stdio.h>
#include <unistd.h>
void main(void)
{
	while(1)
	{
		sleep(1);
		printf("hello world\n");
	}
}

方法二:sleep.c

#include <stdio.h>
#include <unistd.h>
void main(void)
{
	sleep(60);
}

方法三:使用 pause() 函数,函数详解如下

函数原型:int pause(void);
功能:将进程挂起,等待信号
头文件:#include <unistd.h>
返回值:进程被信号中断后一直返回-1
/*pause.c*/

#include <stdio.h>
#include <unistd.h>
void main(void)
{
	printf("pause before\n");
	pause();
	printf("pause after\n");//不会执行该行代码
}

8.3.3 信号处理

信号是由操作系统来处理的,说明信号的处理在内核态。信号不一定会立即被处理,此时会储存在信号的信号表中。

处理过程示意图:

由上图中可看出信号有三种处理方式

  1. 默认方式(通常是终止进程)
  2. 忽略,不进行任何操作。
  3. 捕捉并处理调用信号处理器(回调函数形式)。
函数原型:sighandler_t signal(int signum, sighandler_t handler);
		可以简化成 signal(参数 1,参数 2)
功能:改变收到信号后的动作。
头文件:#include <unistd.h>
		typedef void (*sighandler_t)(int);
参数1:我们要进行处理的信号,系统的信号我们可以在终端键入 kill -l 查看。
参数2:处理的方式(是系统默认还是忽略还是捕获)
	  忽略该信号,填写“SIG_IGN”;eg:signal(SIGINT,SIG_IGN);
	  采用系统默认方式处理该信号,填写“SIG_DFL”;eg:signal(SIGINT,SIG_DFL);
	  捕获到信号后执行此函数内容,
	  定义格式为“typedef void (*sighandler_t)(int)”,sighandler_t 代表一个函数指针。eg:signal(SIGINT,myfun);
	  
返回值:调用成功返回最后一次注册信号调用 signal()时的 handler 值;失败返回 SIG_ERR。

实验一代码:实现信号忽略

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(void)
{
	signal(SIGINT,SIG_IGN);
	while(1)
	{
		printf("wait signal\n");
		sleep(1);
	}
	return 0;
}
//编译运行程序,当我们按下 ctrl+C 键的时候,信号被忽略。

实验二代码:实现采用系统默认方式处理该信号

#include <signal.h>
#include <unistd.h>
int main(void)
{
	signal(SIGINT,SIG_DFL);
	while(1)
	{
		printf("wait signal\n");
		sleep(1);
	}
	return 0;
}
//编译运行程序,如下图所示,按 ctrl+c 可以终止程序

实验三代码:实现捕获到信号后执行此函数内

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void myfun(int sig)
{
	if(sig == SIGINT)
	{
		printf("get sigint\n");
	}
}

int main(void)
{
	signal(SIGINT,myfun);
	while(1)
	{
		sleep(1);
		printf("wait signal\n");
	}
	return 0;
}
//编译运行程序,当我们按下 ctrl+c 时,显示 myfun 函数里面的打印信息

8.4 共享内存

共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

Linux 操作系统的进程通常使用的是虚拟内存,虚拟内存空间是有由物理内存映射而来的。System V 共享内存能够实现让两个或多个进程访问同一段物理内存空间,达到数据交互的效果。

共享内存和其他进程间数据交互方式相比,有以下几个突出特点:

  1. 速度快,因为共享内存不需要内核控制,所以没有系统调用。而且没有向内核拷贝数据的过程, 所以效率和前面几个相比是最快的,可以用来进行批量数据的传输,比如图片。
  2. 没有同步机制,需要借助 Linux 提供其他工具来进行同步,通常使用信号灯。

使用共享内存的步骤:

  1. 调用 shmget()创建共享内存段 id
  2. 调用 shmat()将 id 标识的共享内存段加到进程的虚拟地址空间
  3. 访问加入到进程的那部分映射后地址空间,可用 IO 操作读写

常用 API 如下:

函数原型:int shmget(key_t key, size_t size, int shmflg)
功能:创建共享内存
头文件:#include <sys/ipc.h>
       #include <sys/shm.h>
参数 key:由 ftok 生成的 key 标识,标识系统的唯一 IPC 资源
参数 size:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是 4k 字节,
		  为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
参数 shmflg:如果要创建新的共享内存,需要使用 IPC_CREAT,IPC_EXCL,
			如果是已经存在的,可以使用 IPC_CREAT 或直接传 
返回值:成功时返回一个新建或已经存在的的共享内存标识符,取决于 shmflg 的参数。
	   失败返回-1 并设置错误码
函数原型:key_t ftok(const char *pathname, int proj_id)
功能:建立 IPC 通讯(如消息队列、共享内存时)必须指定一个 ID 值。
	 通常情况下,该 id 值通过 ftok 函数得到
参数 const char *pathnam:文件路径以及文件名
参数 int proj_id:同一个文件根据此值生成多个 key 值,int 型或字符型,
				 多个若想访问同一 IPC 对象,此值必须相同。
返回值:成功返回 key 值,失败返回-1
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:挂接共享内存
头文件:#include <sys/types.h>
		#include <sys/shm.h>
参数 int shmid:共享内存的标识符,也就是 shmget 函数的返回值
参数 const void *shmaddr:映射到的地址,一般写 NULLNULL 为系统自动帮我们完成映射
参数 int shmflg:通常为 0,表示共享内存可读可写,或者为 SHM_RDONLY,表示共享内存可读可写
返回值:成功返回共享内存映射到进程中的地址,失败返回-1
函数原型:int shmdt(const void *shmaddr);
功能:去关联共享内存
头文件:#include <sys/types.h>
		#include <sys/shm.h>
参数 const void *shmaddr:共享内存映射后的地址
返回值:成功返回 0,失败返回-1
注意:shmdt 函数是将进程中的地址映射删除,
	 也就是说当一个进程不需要共享内存的时候,就可以使用这个函数将他从进程地址空间中脱离,并不会删除内核里面的共享内存对象。
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:销毁共享内存
头文件:#include <sys/ipc.h>
	   #include <sys/shm.h>
参数 int shmid:要删除的共享内存的标识符
参数 cmd:IPC_STAT (获取对象属性) IPC_SET (设置对象属性) IPC_RMID(删除对象)
参数 struct shmid_ds *buf:指定 IPC_STAT (获取对象属性) IPC_SET (设置对象属性) 时用来保存或者设置的属性

实验代码:

/*在程序中,创建共享内存。*/

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
	int shmid;
	
	shmid = shmget(IPC_PRIVATE, 1024, 0777);/*在程序中,创建共享内存。*/
	
	if (shmid < 0)
	{
		printf("shmget is error\n");
		return -1;
	}
	printf("shmget is ok and shmid is %d\n", shmid);
	return 0;
}
/*在程序中,父子进程通过共享内存通信。*/

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
	int shmid;
	key_t key;
	pid_t pid;
	char *s_addr, *p_addr;
	key = ftok("./a.c", 'a');
	shmid = shmget(key, 1024, 0777 | IPC_CREAT);
	if (shmid < 0)
	{
		printf("shmget is error\n");
		return -1;
	}
	printf("shmget is ok and shmid is %d\n", shmid);
	pid = fork();
	if (pid > 0)//父进程
	{
		p_addr = shmat(shmid, NULL, 0);
		strncpy(p_addr, "hello", 5);
		wait(NULL);
		exit(0);
	}
	if (pid == 0)//子进程
	{
		sleep(2);
		s_addr = shmat(shmid, NULL, 0);
		printf("s_addr is %s\n", s_addr);
		exit(0);
	}
	return 0;
}

编译运行程序如下图所示:

优点:我们可以看到使用共享内存进行进程之间的通信是非常方便的,而且函数的接口也比较简单, 数据的共享还使进程间的数据不用传送,而是直接访问内存,加快了程序的效率

缺点:共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借 助其他手段来保证进程之间的同步工作

8.5 消息队列

System V IPC 包含三种进程间通信机制,有消息队列,信号灯(也叫信号量),共享内存。此外还有 System V IPC 的补充版本 POSIX IPC,这两组 IPC 的通信方法基本一致,本章以 System V IPC 为例介绍 Linux 进程通信 机制。

可以用命令 ipcs 查看三种 IPC,ipcrm 删除 IPC 对象。

这些 IPC 对象存在于内核空间,应用层使用 IPC 通信的步骤为:

  1. 获取 key 值,内核会将 key 值映射成 IPC 标识符,获取 key 值常用方法: (1)在 get 调用中将 IPC_PRIVATE 常量作为 key 值。 (2)使用 ftok()生成 key
  2. 执行 IPC get 调用,通过 key 获取整数 IPC 标识符 id,每个 id 表示一个 IPC 对象

  1. 通过 id 访问 IPC 对象

  1. 通过 id 控制 IPC 对象

创建这三种 IPC 对象都要先获取 key 值,然后根据 key 获取 id,用到的函数如下:

函数原型:key_t ftok(const char *pathname, int proj_id)
功能:建立 IPC 通讯(如消息队列、共享内存时)必须指定一个 ID 值。
	 通常情况下,该 id 值通过 ftok 函数得到
参数 const char *pathnam:文件路径以及文件名
参数 int proj_id:同一个文件根据此值生成多个 key 值,int 型或字符型,
				 多个若想访问同一 IPC 对象,此值必须相同。
返回值:成功返回 key 值,失败返回-1

消息队列是类 unix 系统中一种数据传输的机制,其他操作系统中也实现了这种机制,可见这种通信机 制在操作系统中有重要地位。

Linux 内核为每个消息队列对象维护一个 msqid_ds,每个 msqid_ds 对应一个 id,消息以链表形式存储, 并且 msqid_ds 存放着这个链表的信息。

消息队列的特点:

  1. 发出的消息以链表形式存储,相当于一个列表,进程可以根据 id 向对应的“列表”增加和获取消息。
  2. 进程接收数据时可以按照类型从队列中获取数据。

消息队列的使用步骤:

  1. 创建 key;
  2. msgget()通过 key 创建(或打开)消息队列对象 id;
  3. 使用 msgsnd()/msgrcv()进行收发;
  4. 通过 msgctl()删除 ipc 对象

通过 **msgget()**调用获取到 id 后即可使用消息队列访问 IPC 对象,消息队列常用 API 如下:

函数原型:int msgget(key_t key, int msgflg)
功能:获取 IPC 对象唯一标识 i
头文件:#include <sys/types.h>
	   #include <sys/ipc.h>
	   #include <sys/msg.h>
参数 key_t key:和消息队列相关的 key 
参数 int msgflg:访问权限
返回值:成功返回消息队列的 ID,失败返回-1
函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:发送数据
头文件:#include <sys/types.h>
	   #include <sys/ipc.h>
	   #include <sys/msg.h>
参数 int msqid:消息队列 ID
参数 const void *msgp:指向消息类型的指针
参数 size_t msgsz:发送的消息的字节数。
参数 int msgflg:如果为 0,直到发送完成函数才返回,即阻塞发送
				IPC_NOWAIT:消息没有发送完成, 函数也会返回,即非阻塞发
返回值:成功返回 0,失败返回-1
函数原型: int msgctl(int msqid, int cmd, struct msqid_ds *buf)
功能:控制操作,删除消息队列对象等
头文件:#include <sys/types.h>
	   #include <sys/ipc.h>
	   #include <sys/msg.h>
参数 int msqid:消息队列的 ID
参数 int cmd:IPC_STAT:读取消息队列的属性,然后把它保存在 buf 指向的缓冲区。
			 IPC_SET:设置消息队列的属性,这个值取自 buf 参数
			 IPC_RMID:
参数 struct msqid_ds *buf:消息队列的缓冲区
返回值:成功返回 0,失败返回-1
函数原型: ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
功能:接收消息
参数 msqid:IPC 对象对应的 id
参数 msgp:消息指针,消息包含类型和字段
参数 msgsz:消息里的字段大小
参数 msgtyp:消息里的类型
参数 msgflg:位掩码,不止一个
返回值:成功返回接收到的字段大小,错误返回-1

实验代码:

/*a.c 向消息队列里面写*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf
{
	long mtype;
	char mtext[128];
};

int main(void)
{
	int msgid;
	key_t key;
	struct msgbuf msg;
	//获取 key 值
	key = ftok("./a.c", 'a');
	//获取到 id 后即可使用消息队列访问 IPC 对象
	msgid = msgget(key, 0666 | IPC_CREAT);
	if (msgid < 0)
	{
		printf("msgget is error\n");
		return -1;
	}
	printf("msgget is ok and msgid is %d \n", msgid);
	msg.mtype = 1;
	strncpy(msg.mtext, "hello", 5);
	//发送数据
	msgsnd(msgid, &msg, strlen(msg.mtext), 0);
	return 0;
}
/*b.c 从消息队列里面读*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf
{
	long mtype;
	char mtext[128];
};

int main(void)
{
	int msgid;
	key_t key;
	struct msgbuf msg;
	key = ftok("./a.c", 'a');
	//获取到 id 后即可使用消息队列访问 IPC 对象
	msgid = msgget(key, 0666 | IPC_CREAT);
	if (msgid < 0)
	{
		printf("msgget is error\n");
		return -1;
	}
	printf("msgget is ok and msgid is %d \n", msgid);
	//接收数据
	msgrcv(msgid, (void *)&msg, 128, 0, 0);
	printf("msg.mtype is %ld \n", msg.mtype);
	printf("msg.mtext is %s \n", msg.mtext);
	return 0;
}

8.6 信号量

本章节将讲述另一种进程间通信的机制——信号量。注意请不要把它与之前所说的信号混淆起来,信 号与信号量是不同的两种事物。为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我 们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区 域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让 一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即 P(信号变量))和发送(即 V(信号变量))信息操作。最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式, 叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

由于信号量只能进行两种操作等待和发送信号,即 P(sv)和 V(sv),他们的行为是这样的: P(sv):如果 sv 的值大于零,就给它减 1;如果它的值为零,就挂起该进程的执行 V(sv):如果有其他进程因等待 sv 而被挂起,就让它恢复运行,如果没有进程因等待 sv 而挂起,就给它加 1。

举个例子,就是两个进程共享信号量 sv,一旦其中一个进程执行了 P(sv)操作,它将得到信号量,并可 以进入临界区,使 sv 减 1。而第二个进程将被阻止进入临界区,因为当它试图执行 P(sv)时,sv 为 0,它会被挂起以等待第一个进程离开临界区域并执行 V(sv)释放信号量,这时第二个进程就可以恢复执行。

信号灯也叫信号量,它能够用来同步进程的动作,不能传输数据。它的应用场景就像红绿灯,控制各进程使用共享资源的顺序

Posix 无名信号灯用于线程同步, Posix 有名信号灯,System V 信号灯。信号灯 相当于一个值大于或等于 0 计数器,信号灯值大于 0,进程就可以申请资源,信号灯值-1,如果信号灯值为 0,一个进程还想对它进行-1,那么这个进程就会阻塞,直到信号灯值大于 1。

使用 System V 信号灯的步骤如下:

  1. 使用 semget()创建或打开一个信号灯集。
  2. 使用 semctl()初始化信号灯集。
  3. 使用 semop()操作信号灯值,即进行 P/V 操作。 P 操作:申请资源,申清完后信号灯值-1; V 操作:释放资源,释放资源后信号灯值+1;

Linux 提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会 对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件 sys/sem.h 中。

函数原型: int semget(key_t key, int nsems, int semflg)
功能 创建一个新信号量或取得一个已有信号量
头文件:#include <sys/types.h>
		#include <sys/ipc.h>
		#include <sys/sem.h>
参数 key_t key:信号量的键值
参数 int nsems:信号量的数量
参数 int semflg:标识
返回值:成功返回信号量的 ID,失败返回-1
函数原型: int semctl(int semid, int semnum, int cmd, union semun arg)
功能 :初始化信号灯集合
头文件 #include <sys/types.h>
		#include <sys/ipc.h>
		#include <sys/sem.h>
参数 int semid:信号量 ID
参数 int semnum:信号量编号
参数 cmd:IPC_STAT(获取信号量的属性) 
		 IPC_SET(设置信号量的属性)
		 IPC_RMID (删除信号量)
		 SETVAL(设置信号量的值)
参数 arg:union semun 
		 {
		 	int val;
			struct semid_ds *buf;
			unsigned short *array;
			struct seminfo *__buf;
		 }
函数原型: int semop(int semid, struct sembuf *sops, size_t nsops)
功能:在信号量上执行一个或多个操作。
头文件:#include <sys/types.h>
		#include <sys/ipc.h>
		#include <sys/sem.h>
参数 int semid:信号量 ID
参数 struct sembuf *sops:信号量结构体数组
参数 size_t nsops:要操作信号量的数量
					struct sembuf
					{
						unsigned short sem_num; //要操作的信号量的编号
						short sem_op; //P/V 操作,1 为 V 操作,释放资源。-1 为 P操作,分配资源。0 为等待,直到信号量的值变成 0
						short sem_flg; //0 表示阻塞,IPC_NOWAIT 表示非阻塞
					}

实验代码:指定哪个进程运行,可以使用进程间通信的知识,或者使用信号量,这里以使用信号量为例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

union semun
{
	int val;
};

int main(void)
{
	int semid;
	int key;
	pid_t pid;
	struct sembuf sem;
	union semun semun_union;
	key = ftok("./a.c", 0666);
	semid = semget(key, 1, 0666 | IPC_CREAT);
	semun_union.val = 0;
	semctl(semid, 0, SETVAL, semun_union);
	pid = fork();
	if (pid > 0)
	{
		sem.sem_num = 0;
		sem.sem_op = -1;
		sem.sem_flg = 0;
		semop(semid, &sem, 1);
		printf("This is parents\n");
		sem.sem_num = 0;
		sem.sem_op = 1;
		sem.sem_flg = 0;
		semop(semid, &sem, 1);
	}
	if (pid == 0)
	{
		sleep(2);
		sem.sem_num = 0;
		sem.sem_op = 1;
		sem.sem_flg = 0;
		semop(semid, &sem, 1);
		printf("This is son\n");
	}
	return 0;
}

编译运行程序如下图所示:

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即 P(信号变量))和发 送(即 V(信号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的