STM32MP157嵌入式linux/C应用编程

0. 安装交叉编译器

  • 在开发板光盘 A-基础资料->5、开发工具->1、交叉编译器路径下找到 st-example-image-qt
    wayland-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1-snapshot.sh。将它拷贝到 Ubuntu 虚拟机上。 拷贝到 Ubuntu 后,赋予 st-example-image-qtwayland-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1-snapshot.sh 可执行权限。
    *chmod +x st-example-image-qtwayland-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1-snapshot.sh
  • 执行./st*.sh脚本安装
  • 安装完成后,安装的交叉编译工具链都会安装在/opt/目录下。
  • ls /opt/st/
  • 安装完成之后,在使用之前先对交叉编译工具的环境进行设置,使用 source 执行安装目录下的
    environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi 脚本文件即可,如下所示:
    source /opt/st/stm32mp1/3.1-snapshot/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
    在这里插入图片描述

注意:每个终端需要执行上面的source命令之后,才能打印出${CC}.

使用${CC} -o led led.c编译

生成的led可执行文件通过scp命令传输到开发板,./led执行。

1.文件IO

1.1打开/写/读/关闭文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • pathname:字符串类型,用于标识需要打开或创建的文件,可以包含路径信息,如:“./src_file”、"/home/dengtao/hello.c"等;如果 pathname 是一个符号链接,会对其进行解引用。
  • flags:调用 open 函数时需要提供的标志,包括文件访问模式标志以及其它文件相关标志,如O_RDONLY,O_CREAT,O_RDWR,O_RDWR,O_EXCL,O_NOFOLLOW
  • mode:此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)如:S_IRUSR 允许文件所属者读文件,S_IWUSR 允许文件所属者写文件,S_IXUSR 允许文件所属者执行文件
  • 使用:
int fd = open("/home/dengtao/hello", O_RDWR | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH);
if (-1 == fd)//使用 open 函数打开一个指定的文件,如果该文件不存在则创建该文件,
return fd;

int fd = open("./app.c", O_RDWR)//使用 open 函数打开一个已经存在的文件(例如当前目录下的 app.c 文件),使用可读可写方式打开
if (-1 == fd)
return fd;
  1. write写文件
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

调用 write 函数可向打开的文件写入数据

  • fd:文件描述符。关于文件描述符,前面已经给大家进行了简单地讲解,这里不再重述!我们需要将进行写操作的文件所对应的文件描述符传递给 write 函数。
  • buf:指定写入数据对应的缓冲区。
  • count:指定写入的字节数。
  • 返回值:如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。
  1. read 读文件
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  • 返回值:如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节数,也有可能会为 0
  1. close 关闭文件
#include <unistd.h>
int close(int fd);
  1. lseek
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
  • whence:用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):
    ⚫ SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算);

⚫ SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处,offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移;
⚫ SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果是负数则表示往前偏移。

  • 返回值:成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生错误将返回-1。

使用示例

off_t off = lseek(fd, 0, SEEK_SET);//将读写位置移动到文件开头处:
off_t off = lseek(fd, 0, SEEK_END);//将读写位置移动到文件末尾:
off_t off = lseek(fd, 100, SEEK_SET);//将读写位置移动到偏移文件开头 100 个字节处:
off_t off = lseek(fd, 0, SEEK_CUR);//获取当前读写位置偏移量:

1.2 strerror/perror函数

  • 前面说到了 errno 变量仅仅只是一个错误编号,还需要对比源码中对此编号的错误定义。strerror()函数可以将对应的 errno 转换成适合我们查看的字符串信息,其函数原型如下所示(可通过"man 3 strerror"命令查看,注意此函数是 C 库函数,并不是系统调用):
#include <string.h>
char *strerror(int errnum);
  • errnum:错误编号 errno。
  • 返回值:对应错误编号的字符串描述信息。
    使用:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void)
{
	int fd;
	/* 打开文件 */
	fd = open("./test_file", O_RDONLY);
	if (-1 == fd) {
		printf("Error: %s\n", strerror(errno));
		return -1;}
	close(fd);
	return 0;
}
/*
运行结果:Error:No such file or dictory
*/
  1. perror函数
  • 除了 strerror 函数之外,还可以使用 perror 函数来查看错误信息,调用此函数不需要传入 errno,函数内部会自己去获取 errno 变量的值,调用此函数会直接将错误提示字符串打印出来,而不是返回字符串,除此之外还可以在输出的错误提示字符串之前加入自己的打印信息,函数原型如下所示(可通过"man 3 perror"命令查看):
#include <stdio.h>
void perror(const char *s);
  • s:在错误提示字符串信息之前,可加入自己的打印信息,也可不加,不加则传入空字符串即可。
    使用:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
	int fd;
	/* 打开文件 */
	fd = open("./test_file", O_RDONLY);
	if (-1 == fd) {
		perror("open error");
		return -1;}
	close(fd);
	return 0;
}
/*
运行结果:open error: No such file or dictory
*/

1.3 _exit()和_Exit()函数\空洞文件

  • main 函数中使用 return 后返回,return 执行后把控制权交给调用函数,结束该进程。调用_exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统。_exit()函数原型如下所示:
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
  • 调用函数需要传入 status 状态标志,0 表示正常结束、若为其它值则表示程序执行过程中检测到有错误发生。_exit()和_Exit()两者等价,用法作用是一样的,使用示例如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
	int fd;
	/* 打开文件 */
	fd = open("./test_file", O_RDONLY);
	if (-1 == fd) {
		perror("open error");
		_exit(-1);}
	close(fd);
	_exit(0);
}
  1. exit()函数
    exit()函数_exit()函数都是用来终止进程的,exit()是一个标准 C 库函数,而_exit()和_Exit()是系统调用。执行 exit()会执行一些清理工作,最后调用_exit()函数。该函数是一个标准 C 库函数,该函数的用法和_exit()/_Exit()是一样的,exit()函数原型如下:
#include <stdlib.h>
void exit(int status);
  1. 空洞文件
  • 文件如果只有400kb,使用lseek便宜600K开始写也能正常运行,400到600之间就属于文件空洞,改文件叫做空洞文件。但是文件显示的大小是总的大小如620k,如迅雷多线程下载文件时,还未下载成功就占用的全部文件大小,就是在分段下载,其中就是出现文件空洞。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)//新建一个文件把它做成空洞文件
{
 	int fd,ret,i;
	char buffer[1024];c
 	/* 打开文件 */
 	fd = open("./hole_file", O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP |S_IROTH);
 	if (-1 == fd) {
 		perror("open error");
 		exit(-1);}
 	/* 将文件读写位置移动到偏移文件头 4096 个字节(4K)处 */
 	ret = lseek(fd, 4096, SEEK_SET);
	if (-1 == ret) {
 		perror("lseek error");
 		goto err;}
 	/* 初始化 buffer 为 0xFF */
 	memset(buffer, 0xFF, sizeof(buffer));
 	/* 循环写入 4 次,每次写入 1K */
 	for (i = 0; i < 4; i++) {
 		ret = write(fd, buffer, sizeof(buffer));
 		if (-1 == ret) {
 			perror("write error");
 			goto err;}
 	}
 	ret = 0;
	err:
 	/* 关闭文件 */
 	close(fd);
 	exit(ret);
}
/*
示例代码中,我们使用 open 函数新建了一个文件 hole_file,在 Linux 系统中,新建文件大小是 0,也就是没有任何数据写入,此时使用lseek函数将读写偏移量移动到4K字节处,再使用write函数写入数据0xFF,每次写入 1K,一共写入 4 次,也就是写入了 4K 数据,也就意味着该文件前 4K 是文件空洞部分,而后 4K数据才是真正写入的数据。
*/

使用 ls 命令查看到空洞文件的大小是 8K,使用 ls 命令查看到的大小是文件的逻辑大小,自然是包括了空洞部分大小和真实数据部分大小;当使用 du 命令查看空洞文件时,其大小显示为 4K,du 命令查看到的大小是文件实际占用存储块的大小。
在这里插入图片描述

1.4 O_TRUNC、O_APPEND标志\复制文件描述符

  • O_TRUNC 这个标志的作用非常简单,如果使用了这个标志,调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0。
  • 使用:fd = open(“./test_file”, O_WRONLY | O_TRUNC);
  • 如果 open 函数携带了 O_APPEND 标志,调用 open 函数打开文件,当每次使用 write()函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始。
  • 使用:fd = open(“./test_file”, O_RDWR | O_APPEND);
  1. 复制文件描述符
  • 在 Linux 系统中,open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件描述符,使用新的文件描述符和旧的文件描述符都可以对文件进行 IO 操作,复制得到的文件描述符和旧的文件描述符拥有相同的权限,譬如使用旧的文件描述符对文件有读写权限,那么新的文件描述符同样也具有读写权限;在 Linux 系统下,可以使用 dup 或 dup2 这两个系统调用对文件描述符进行复制。
  • dup 函数用于复制文件描述符,此函数原型如下所示(可通过"man 2 dup"命令查看):
#include <unistd.h>
int dup(int oldfd);
  • oldfd:需要被复制的文件描述符。
  • 返回值:成功时将返回一个新的文件描述符,由操作系统分配,分配置原则遵循文件描述符分配原则;如果复制失败将返回-1,并且会设置 errno 值。
  • *由前面的介绍可知,复制得到的文件描述符与原文件描述符都指向同一个文件表,所以它们的文件读写偏移量是一样的,那么是不是可以在不使用O_APPEND标志的情况下,通过文件描述符复制来实现接续写,接下来我们编写一个程序进行测试:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
 	unsigned char buffer1[4], buffer2[4];
 	int fd1, fd2,ret,i;
 	/* 创建新文件 test_file 并打开 */
 	fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
 	if (-1 == fd1) {
 		perror("open error");
 		exit(-1);}
 	/* 复制文件描述符 */
 	fd2 = dup(fd1);
 	if (-1 == fd2) {
 		perror("dup error");
 		ret = -1;
 		goto err1;}
 	printf("fd1: %d\nfd2: %d\n", fd1, fd2);
 	/* buffer 数据初始化 */
 	buffer1[0] = 0x11;
 	buffer1[1] = 0x22;
 	buffer1[2] = 0x33;
 	buffer1[3] = 0x44;
 	buffer2[0] = 0xAA;
 	buffer2[1] = 0xBB;
 	buffer2[2] = 0xCC;
 	buffer2[3] = 0xDD;
 	/* 循环写入数据 */
 	for (i = 0; i < 4; i++) {
 		ret = write(fd1, buffer1, sizeof(buffer1));
 		if (-1 == ret) {
 		perror("write error");
 		goto err2;}
 	ret = write(fd2, buffer2, sizeof(buffer2));
	 if (-1 == ret) {
		 perror("write error");
 		goto err2; }}
 	/* 将读写位置偏移量移动到文件头 */
	 ret = lseek(fd1, 0, SEEK_SET);
 	if (-1 == ret) {
 		perror("lseek error");
 		goto err2;
 	}
 	/* 读取数据 */
	for (i = 0; i < 8; i++) {
 		ret = read(fd1, buffer1, sizeof(buffer1));
 		if (-1 == ret) {
 			perror("read error");
 			goto err2;}
 		printf("%x%x%x%x", buffer1[0], buffer1[1],
 		buffer1[2], buffer1[3]);
 	}
 	printf("\n");
 	ret = 0;
err2:
 	close(fd2);
err1:
 	/* 关闭文件 */
 	close(fd1);
 	exit(ret);
}
/*
运行结果:由打印信息可知,fd1 等于 6,复制得到的新的文件描述符为 7(遵循 fd 分配原则),打印出来的数据显示为接续写,所以可知,通过复制文件描述符可以实现接续写
*/
  • dup 系统调用分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件描述符,这是 dup 系统调用的一个缺陷;而 dup2 系统调用修复了这个缺陷,可以手动指定文件描述符,而不需要遵循文件描述符分配原则,当然在实际的编程工作中,需要根据自己的情况来进行选择。dup2 函数原型如下所示(可以通过"man 2 dup2"命令查看):
#include <unistd.h>
int dup2(int oldfd, int newfd);

1.5 原子操作:pread()、 pwrite()、fcntl和ioctl函数、截断文件

  • pread()和 pwrite()都是系统调用,与 read()、write()函数的作用一样,用于读取和写入数据。区别在于,pread()和 pwrite()可用于实现原子操作,调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数,用于指定文件当前读或写的位置偏移量,所以调用 pread 相当于调用 lseek 后再调用 read;同理,调用 pwrite相当于调用 lseek 后再调用 write。所以可知,使用 pread 或 pwrite 函数不需要使用 lseek 来调整当前位置偏移量,并会将“移动当前位置偏移量、读或写”这两步操作组成一个原子操作。pread、pwrite 函数原型如下所示(可通过"man 2 pread"或"man 2 pwrite"命令来查看):
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
  • fd、buf、count 参数与 read 或 write 函数意义相同。
  • offset:表示当前需要进行读或写的位置偏移量。
  • 返回值:返回值与 read、write 函数返回值意义一样。
  • 虽然 pread(或 pwrite)函数相当于 lseek 与 pread(或 pwrite)函数的集合,但还是有下列区别:
    ⚫ 调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);
    ⚫ 不更新文件表中的当前位置偏移量。
  1. fcntl和ioctl函数
    *fcntl()函数可以对一个已经打开的文件描述符执行一系列控制操作,譬如复制一个文件描述符(与 dup、dup2 作用相同)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。fcntl()函数原型如下所示(可通过"man 2 fcntl"命令查看):
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ )

fd:文件描述符。
cmd:操作命令。此参数表示我们将要对 fd 进行什么操作,cmd 参数支持很多操作命令,大家可以打
开 man 手册查看到这些操作命令的详细介绍,这些命令都是以 F_XXX 开头的,譬如 F_DUPFD、F_GETFD、F_SETFD 等,不同的 cmd 具有不同的作用,cmd 操作命令大致可以分为以下 5 种功能:
⚫ 复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC);
⚫ 获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD);
⚫ 获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL);
⚫ 获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN);
⚫ 获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK);

  • 返回值:执行失败情况下,返回-1,并且会设置 errno;执行成功的情况下,其返回值与 cmd(操作命令)有关,譬如 cmd=F_DUPFD(复制文件描述符)将返回一个新的文件描述符、cmd=F_GETFD(获取文件描述符标志)将返回文件描述符标志、cmd=F_GETFL(获取文件状态标志)将返回文件状态标志等。
    fcntl 使用示例
    (1)复制文件描述符
    当 cmd=F_DUPFD 时,它的作用会根据 fd 复制出一个新的文件描述符,此时需要传入第三个参数,第三个参数用于指出新复制出的文件描述符是一个大于或等于该参数的可用文件描述符(没有使用的文件描述符);如果第三个参数等于一个已经存在的文件描述符,则取一个大于该参数的可用文件描述符。
    测试代码如下所示:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 	int fd1, fd2;
 	int ret;
 	/* 打开文件 test_file */
 	fd1 = open("./test_file", O_RDONLY);
 	if (-1 == fd1) {
 		perror("open error");
 		exit(-1);
 	}
 	/* 使用 fcntl 函数复制一个文件描述符 */
 	fd2 = fcntl(fd1, F_DUPFD, 0);
 	if (-1 == fd2) {
 		perror("fcntl error");
 		ret = -1;
 		goto err;
 	}
 	printf("fd1: %d\nfd2: %d\n", fd1, fd2);
 	ret = 0;
	 close(fd2);
err:
	 /* 关闭文件 */
 	close(fd1);
	 exit(ret);
}
  • cmd=F_GETFL 可用于获取文件状态标志,cmd=F_SETFL 可用于设置文件状态标志,cmd=F_GETFL 时不需要传入第三个参数,返回值成功表示获取到的文件状态标志,cmd=F_SETFL 时,需要传入第三个参数,此参数表示需要设置的文件状态标志。这些标志指的就是我们在调用 open 函数时传入的 flags 标志,可以指定一个或多个(通过位或 | 运算符组合),但是文件权限标志(O_RDONLY、O_WRONLY、O_RDWR)以及文件创建标志(O_CREAT、O_EXCL、O_NOCTTY、O_TRUNC)不能被设置、会被忽略;在 Linux 系统中,只有 O_APPEND、O_ASYNC、O_DIRECT、O_NOATIME 以及 O_NONBLOCK 这些标志可以被修改,这里面有些标志并没有给大家介绍过,后面我们在用到的时候再给大家介绍。
  • ioctl()可以认为是一个文件 IO 操作的杂物箱,可以处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设,此函数将会在进阶篇中使用到,譬如可以通过 ioctl 获取 LCD 相关信息等.
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
  1. 截断文件
    使用系统调用 truncate()或 ftruncate()可将普通文件截断为指定字节长度,其函数原型如下所示:
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

4 标准I/O函数

4.1 fopen()和fclose()

在文件 I/O 中,使用 open()系统调用打开或创建文件,而在标准 I/O 中,使用库函数fopen()打开或创建文件。

#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
  • mode:参数 mode 指定了对该文件的读写权限,是一个字符串;
  • r 以只读方式打开文件。r+ 以可读、可写方式打开文件。w以只写方式打开文件,如果参数 path 指定的文件存在,将文件长度截断为 0;如果指定文件不存在则创建该文件。w+以可读、可写方式打开文件,如果参数 path 指定的文件存在,将文件长度截断为 0;如果指定文件
    不存在则创建该文件。a以只写方式打开文件,打开以进行追加内容(在文件末尾写入),如果文件不存在则创建该文件。a+以可读、可写方式打开文件,以追加方式写入(在文件末尾写入),如果文件不存在则创建该文件。
    *fclose()关闭文件,调用 fclose()库函数可以关闭一个由 fopen()打开的文件,其函数原型如下所示:
#include <stdio.h>
int fclose(FILE *stream);

4.2读文件和写文件

当使用 fopen()库函数打开文件之后,接着我们便可以使用 fread()和 fwrite()库函数对文件进行读、写操
作了,函数原型如下所示:

#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr:fread()将读取到的数据存放在参数 ptr 指向的缓冲区中;
  • size:fread()从文件读取 nmemb 个数据项,每一个数据项的大小为 size 个字节,所以总共读取的数据大小为 nmemb * size 个字节。
  • nmemb:参数 nmemb 指定了读取数据项的个数。
  • stream:FILE 指针。

4.3fseek 定位

库函数 fseek()的作用类似于 2.7 小节所学习的系统调用 lseek(),用于设置文件读写位置偏移量,lseek()用于文件 I/O,而库函数 fseek()则用于标准 I/O,其函数原型如下所示:

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);

函数参数和返回值含义如下:
stream:FILE 指针。
offset:与 lseek()函数的 offset 参数意义相同。
whence:与 lseek()函数的 whence 参数意义相同。

9.进程

  1. 获取本进程的进程号
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
printf("本进程的 PID 为: %d\n", pid);
  1. 获取父进程的进程号
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);

9.2 进程的环境变量

  1. 使用 env 命令查看到 shell 进程的所有环境变量
  2. 使用 export 命令可以添加一个新的环境变量或删除一个环境变量:export LINUX_APP=123456 # 添加 LINUX_APP 环境变量
  3. 删除 LINUX_APP 环境变量。export -n LINUX_APP
  4. 在我们的应用程序当中也可以获取当前进程的环境变量,事实上,进程的环境变量是从其父进程中继承过来的,譬如在 shell 终端下执行一个应用程序,那么该进程的环境变量就是从其父进程(shell 进程)中继承过来的。新的进程在创建之前,会继承其父进程的环境变量副本。环境变量存放在一个字符串数组中,在应用程序中,通过 environ 变量指向它,environ 是一个全局变量,在我们的应用程序中只需申明它即可使用,如下所示:
    extern char **environ; // 申明外部全局变量 environ
    编写应用程序,获取进程的所有环境变量。
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[])
{
	 int i;
	 /* 打印进程的环境变量 */
	 for (i = 0; NULL != environ[i]; i++)//通过字符串数组元素是否等于 NULL 来判断是否已经到了数组的末尾。
	 	puts(environ[i]);
	 exit(0);
}
  1. 获取指定环境变量 getenv()
#include <stdlib.h>
char *getenv(const char *name);

添加/删除/修改环境变量

  1. putenv()函数。putenv()函数可向进程的环境变量数组中添加一个新的环境变量,或者修改一个已经存在的环境变量对应的值,其函数原型如下所示:该函数调用成功之后,参数 string 所指向的字符串就成为了进程环境变量的一部分了,换言之,putenv()函数将设定 environ 变量(字符串数组)中的某个元素(字符串指针)指向该 string 字符串,而不是指向它的复制副本,这里需要注意!因此,不能随意修改参数 string 所指向的内容,这将影响进程的环境变量,出于这种原因,参数 string 不应为自动变量(即在栈中分配的字符数组)。
#include <stdlib.h>
int putenv(char *string);//参数 string 是一个字符串指针,指向 name=value 形式的字符串。成功返回 0;失败将返回非 0 值,并设置 errno。
  1. setenv()函数,可以替代 putenv()函数,用于向进程的环境变量列表中添加一个新的环境变量或修改现有环境变量对应的值,其函数原型如下所示:setenv()函数为形如 name=value 的字符串分配一块内存缓冲区,并将参数 name 和参数 value 所指向的字符串复制到此缓冲区中,
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);//如果参数 overwrite 的值为非 0,若参数 name标识的环境变量已经存在,则覆盖,不存在则表示添加新的环境变量。
  1. 在执行程序的时候,在其路径前面添加环境变量,以 name=value 的形式添加,如果是多个环境变量,则在./app 前面放置多对 name=value 即可,以空格分隔:NAME=value ./app
  2. unsetenv()函数可以从环境变量表中移除参数 name 标识的环境变量,其函数原型如下所示:
#include <stdlib.h>
int unsetenv(const char *name);

清空环境变量

  1. 有时,需要清除环境变量表中的所有变量,然后再进行重建,可以通过将全局变量 environ 赋值为 NULL来清空所有变量。
    environ = NULL;
    也可通过 clearenv()函数来操作,函数原型如下所示:
#include <stdlib.h>
int clearenv(void);

clearenv()函数内部的做法其实就是将environ赋值为NULL。在某些情况下,使用setenv()函数和clearenv()函数可能会导致程序内存泄漏,前面提到过,setenv()函数会为环境变量分配一块内存缓冲区,随之称为进程的一部分;而调用 clearenv()函数时没有释放该缓冲区(clearenv()调用并不知晓该缓冲区的存在,故而也无法将其释放),反复调用者两个函数的程序,会不断产生内存泄漏。

9.3 进程的内存布局

C 语言程序一直都是由以下几部分组成的:

  • 正文段。也可称为代码段,这是 CPU 执行的机器语言指令部分,文本段具有只读属性,以防止程序由于意外而修改其指令;正文段是可以共享的,即使在多个进程间也可同时运行同一段程序。
  • 初始化数据段。通常将此段称为数据段,包含了显式初始化的全局变量和静态变量,当程序加载到内存中时,从可执行文件中读取这些变量的值。
    *未初始化数据段。包含了未进行显式初始化的全局变量和静态变量,通常将此段称为 bss 段,这一名词来源于早期汇编程序中的一个操作符,意思是“由符号开始的块”(block started by symbol),在程序开始执行之前,系统会将本段内所有内存初始化为 0,可执行文件并没有为 bss 段变量分配存储空间,在可执行文件中只需记录 bss 段的位置及其所需大小,直到程序运行时,由加载器来分配这一段内存空间。
  • 栈。函数内的局部变量以及每次函数调用时所需保存的信息都放在此段中,每次调用函数时,函数传递的实参以及函数返回值等也都存放在栈中。栈是一个动态增长和收缩的段,由栈帧组成,系统会为每个当前调用的函数分配一个栈帧,栈帧中存储了函数的局部变量(所谓自动变量)、实参和返回值。
  • 堆。可在运行时动态进行内存分配的一块区域,譬如使用 malloc()分配的内存空间,就是从系统堆内存中申请分配的。
    Linux 下的 size 命令可以查看二进制可执行文件的文本段、数据段、bss 段的段大小:
    在这里插入图片描述
    在这里插入图片描述

9.4 进程的虚拟地址空间

在 Linux 系统中,每一个进程都在自己独立的地址空间中运行,在 32 位系统中,每个进程的逻辑地址空间均为 4GB,这 4GB 的内存空间按照 3:1 的比例进行分配,其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间,如下所示:
在这里插入图片描述
虚拟地址会通过硬件 MMU(内存管理单元)映射到实际的物理地址空间中,建立虚拟地址到物理地址的映射关系后,对虚拟地址的读写操作实际上就是对物理地址的读写操作,MMU 会将物理地址“翻译”为对应的物理地址,其关系如下所示:
在这里插入图片描述
Linux 系统下,应用程序运行在一个虚拟地址空间中,所以程序中读写的内存地址对应也是虚拟地址,并不是真正的物理地址,譬如应用程序中读写 0x80800000 这个地址,实际上并不对应于硬件的 0x80800000这个物理地址。
为什么需要引入虚拟地址呢?
计算机物理内存的大小是固定的,就是计算机的实际物理内存,试想一下,如果操作系统没有虚拟地址机制,所有的应用程序访问的内存地址就是实际的物理地址,所以要将所有应用程序加载到内存中,但是我们实际的物理内存只有 4G,所以就会出现一些问题:
⚫ 当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。
⚫ 内存使用效率低。内存空间不足时,就需要将其它程序暂时拷贝到硬盘中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率就会非常低。
⚫ 进程地址空间不隔离。由于程序是直接访问物理内存的,所以每一个进程都可以修改其它进程的内存数据,甚至修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏,系统不安全、不稳定。
⚫ 无法确定程序的链接地址。程序运行时,链接地址和运行地址必须一致,否则程序无法运行!因为程序代码加载到内存的地址是由系统随机分配的,是无法预知的,所以程序的运行地址在编译程序时是无法确认的。

针对以上的一些问题,就引入了虚拟地址机制,程序访问存储器所使用的逻辑地址就是虚拟地址,通过逻辑地址映射到真正的物理内存上。所有应用程序运行在自己的虚拟地址空间中,使得进程的虚拟地址空间和物理地址空间隔离开来,这样做带来了很多的优点:
⚫ 进程与进程、进程与内核相互隔离。一个进程不能读取或修改另一个进程或内核的内存数据,这是因为每一个进程的虚拟地址空间映射到了不同的物理地址空间。提高了系统的安全性与稳定性。
⚫ 在某些应用场合下,两个或者更多进程能够共享内存。因为每个进程都有自己的映射表,可以让不同进程的虚拟地址空间映射到相同的物理地址空间中。通常,共享内存可用于实现进程间通信。
⚫ 便于实现内存保护机制。譬如在多个进程共享内存时,允许每个进程对内存采取不同的保护措施,例如,一个进程可能以只读方式访问内存,而另一进程则能够以可读可写的方式访问。
⚫ 编译应用程序时,无需关心链接地址。前面提到了,当程序运行时,要求链接地址与运行地址一致,在引入了虚拟地址机制后,便无需关心这个问题。

9.5 fork()创建子进程

  1. 一个现有的进程可以调用 fork()函数创建一个新的进程,调用 fork()函数的进程称为父进程,由 fork()函数创建出来的进程被称为子进程child process
#include <unistd.h>
pid_t fork(void);
  1. 理解 fork()系统调用的关键在于,完成对其调用后将存在两个进程,一个是原进程(父进程)、另一个则是创建出来的子进程,并且每个进程都会从 fork()函数的返回处继续执行,会导致调用 fork()返回两次值,子进程返回一个值、父进程返回一个值。在程序代码中,可通过返回值来区分是子进程还是父进程。fork()调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0;如果调用失败,父进
    程返回值-1,不创建子进程,并设置 errno。
  2. fork()调用成功后,子进程和父进程会继续执行 fork()调用之后的指令,子进程、父进程各自在自己的进程空间中运行。事实上,子进程是父进程的一个副本,譬如子进程拷贝了父进程的数据段、堆、栈以及继承了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间,这是子进程对父进程相应部分存储空间的完全复制,执行 fork()之后,每个进程均可修改各自的栈数据以及堆段中的变量,而并不影响另一个进程。
  3. 虽然子进程是父进程的一个副本,但是对于程序代码段(文本段)来说,两个进程执行相同的代码段,因为代码段是只读的,也就是说父子进程共享代码段,在内存中只存在一份代码段数据。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
	 pid_t pid;
	 pid = fork();
	 switch (pid) {
		 case -1:
			 perror("fork error");
			 exit(-1);
		 case 0:
			 printf("这是子进程打印信息<pid: %d, 父进程 pid: %d>\n",getpid(), getppid());
			 _exit(0); //子进程使用_exit()退出
		 default:
			 printf("这是父进程打印信息<pid: %d, 子进程 pid: %d>\n",getpid(), pid);
			exit(0);
	 }
}
  1. C 库函数 exit()建立在系统调用_exit()之上,这两个函数在 3.3 小节中向大家介绍过,这里我们强调,在调用了 fork()之后,父、子进程中一般只有一个会通过调用 exit()退出进程,而另一个则应使用_exit()退出,具体原因将会在后面章节内容中向大家做进一步说明!
    直接测试运行查看打印结果:
    在这里插入图片描述
  2. fork()函数调用完成之后,父进程、子进程会各自继续执行 fork()之后的指令,它们共享代码段,但并不共享数据段、堆、栈等,而是子进程拥有父进程数据段、堆、栈等副本,所以对于同一个局部变量,它们打印出来的值是不相同的,因为 fork()调用返回值不同,在父、子进程中赋予了 pid 不同的值
  3. 子进程被创建出来之后,便是一个独立的进程,拥有自己独立的进程空间,系统内唯一的进程号,拥有自己独立的 PCB(进程控制块),子进程会被内核同等调度执行,参与到系统的进程调度中。子进程与父进程之间的这种关系被称为父子进程关系,父子进程关系相比于普通的进程间关系多多少少存在一些关联与“羁绊”.
  4. Tips:系统调度。Linux 系统是一个多任务、多进程、多线程的操作系统,一般来说系统启动之后会运行成百甚至上千个不同的进程,那么对于单核 CPU 计算机来说,在某一个时间它只能运行某一个进程的代码指令,那其它进程怎么办呢(多核处理器也是如此,同一时间每个核它只能运行某一个进程的代码)?这里就出现了调度的问题,系统是这样做的,每一个进程(或线程)执行一段固定的时间,时间到了之后切换执行下一个进程或线程,依次轮流执行,这就称为调度,由操作系统负责这件事情,当然系统调度的实现本身是一件非常复杂的事情,需要考虑的因素很多,这里只是让大家有个简单地认识,系统调度的基本单元是线程,关于线程,后面章节内容将会向大家介绍。

9.6 父、子进程间的文件共享

  1. 调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(),这也意味着父、子进程对应的文件描述符均指向相同的文件表,如下图所示:
    在这里插入图片描述
  2. 由此可知,子进程拷贝了父进程的文件描述符表,使得父、子进程中对应的文件描述符指向了相同的文件表,也意味着父、子进程中对应的文件描述符指向了磁盘中相同的文件,因而这些文件在父、子进程间实现了共享,譬如,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置偏移量。
  3. 若父进程 open 打开文件之后,才调用 fork()创建了子进程,所以子进程了继承了父进程打开的文件描述符 fd,此种情况下,父、子进程分别对同一个文件进行写入操作,结果是接续写,不管是父进程,还是子进程,在每次写入时都是从文件的末尾写入,很像使用了O_APPEND 标志的效果。其原因也非常简单,图 9.6.1 中便给出了答案,子进程继承了父进程的文件描述符,两个文件描述符都指向了一个相同的文件表,意味着它们的文件偏移量是同一个、绑定在了一起,相互影响,子进程改变了文件的位置偏移量就会作用到父进程,同理,父进程改变了文件的位置偏移量就会作用到子进程。
  4. 但是父进程在调用 fork()之后,此时父进程和子进程都去打开同一个文件,然后再对文件进行写入操作,这种文件共享方式实现的是一种两个进程分别各自对文件进行写入操作,因为父、子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况。
    fork()函数使用场景
    fork()函数有以下两种用法:
    ⚫ 父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork()创建一个子进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。
    ⚫ 一个进程要执行不同的程序。譬如在程序 app1 中调用 fork()函数创建了子进程,此时子进程是要去执行另一个程序 app2,也就是子进程需要执行的代码是 app2 程序对应的代码,子进程将从 app2程序的 main 函数开始运行。这种情况,通常在子进程从 fork()函数返回之后立即调用 exec 族函数来实现,关于 exec 函数将在后面内容向大家介绍

9.7 系统调用 vfork()

  1. 除了 fork()系统调用之外,Linux 系统还提供了 vfork()系统调用用于创建子进程,vfork()与 fork()函数在功能上是相同的,并且返回值也相同,在一些细节上存在区别
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
  1. 从前面的介绍可知,可以将 fork()认作对父进程的数据段、堆段、栈段以及其它一些数据结构创建拷贝,由此可以看出,使用 fork()系统调用的代价是很大的,它复制了父进程中的数据段和堆栈段中的绝大部分内容,这将会消耗比较多的时间,效率会有所降低,而且太浪费,原因有很多,其中之一在于,fork()函数之后子进程通常会调用 exec 函数,也就是 fork()第二种使用场景下,这使得子进程不再执行父程序中的代码段,而是执行新程序的代码段,从新程序的 main 函数开始执行、并为新程序重新初始化其数据段、堆段、栈段等;那么在这种情况下,子进程并不需要用到父进程的数据段、堆段、栈段(譬如父程序中定义的局部变量、全局变量等)中的数据,此时就会导致浪费时间、效率降低。

  2. vfork()与 fork()函数主要有以下两个区别:
    ⚫ vfork()与 fork()一样都创建了子进程,但 vfork()函数并不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec(或_exit),于是也就不会引用该地址空间的数据。不过在子进程调用 exec 或_exit 之前,它在父进程的空间中运行、子进程共享父进程的内存。这种优化工作方式的实现提高的效率;但如果子进程修改了父进程的数据(除了 vfork 返回值的变量)、进行了函数调用、或者没有调用 exec 或_exit 就返回将可能带来未知的结果。
    ⚫ 另一个区别在于,vfork()保证子进程先运行,子进程调用 exec 之后父进程才可能被调度运行。虽然 vfork()系统调用在效率上要优于 fork(),但是 vfork()可能会导致一些难以察觉的程序 bug,所以尽量避免使用 vfork()来创建子进程,虽然 fork()在效率上并没有 vfork()高,但是现代的 Linux 系统内核已经采用了写时复制技术来实现 fork(),其效率较之于早期的 fork()实现要高出许多,除非速度绝对重要的场合,我们的程序当中应舍弃 vfork()而使用 fork()

  3. 在正式的使用场合下,一般应在子进程中立即调用 exec,如果 exec 调用失败,子进程则应调用_exit()退出(vfork 产生的子进程不应调用 exit 退出,因为这会导致对父进程 stdio 缓冲区的刷新和关闭)。上述示例代码只是一个简单地演示,并不是 vfork()的真正用法,后面学习到 exec 的时候还会再给大家进行介绍。

9.8 fork()之后的竞争条件

  1. 调用 fork()之后,子进程成为了一个独立的进程,可被系统调度运行,而父进程也继续被系统调度运行,这里出现了一个问题,调用 fork 之后,无法确定父、子两个进程谁将率先访问 CPU,也就是说无法确认谁先被系统调用运行(在多核处理器中,它们可能会同时各自访问一个 CPU),这将导致谁先运行、谁后运行这个顺序是不确定的.
  2. 从测试结果可知,虽然绝大部分情况下,父进程会先于子进程被执行,但是并不排除子进程先于父进程被执行的可能性。而对于有些特定的应用程序,它对于执行的顺序有一定要求的,譬如它必须要求父进程先运行,或者必须要求子进程先运行,程序产生正确的结果它依赖于特定的执行顺序,那么将可能因竞争条件而导致失败、无法得到正确的结果。那如何明确保证某一特性执行顺序呢?这个时候可以通过采用采用某种同步技术来实现,譬如前面给大家介绍的信号,如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它,示例代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
static void sig_handler(int sig)
{
	printf("接收到信号\n");
}
int main(void)
{
	 struct sigaction sig = {0};
	 sigset_t wait_mask;
	 /* 初始化信号集 */
	 sigemptyset(&wait_mask);
	 /* 设置信号处理方式 */
	 sig.sa_handler = sig_handler;
	 sig.sa_flags = 0;
	 if (-1 == sigaction(SIGUSR1, &sig, NULL)) {
		 perror("sigaction error");
		 exit(-1);
	 }
	 switch (fork()) {
		 case -1:
			 perror("fork error");
			 exit(-1);
		 case 0:
			 /* 子进程 */
			 printf("子进程开始执行\n");
			 printf("子进程打印信息\n");
			 printf("~~~~~~~~~~~~~~~\n");
			 sleep(2);
			 kill(getppid(), SIGUSR1); //发送信号给父进程、唤醒它
			 _exit(0);
		 default:
			 /* 父进程 */
			 if (-1 != sigsuspend(&wait_mask))//挂起、阻塞
			 exit(-1);
			 printf("父进程开始执行\n");
			 printf("父进程打印信息\n");
		 	exit(0);
	 }
}

这里我们希望子进程先运行打印相应信息,之后再执行父进程打印信息,在父进程分支中,直接调用了 sigsuspend()使父进程进入挂起状态,由子进程通过 kill 命令发送信号唤醒.

9.9 进程的诞生与终止

  1. 一个进程可以通过 fork()或 vfork()等系统调用创建一个子进程,Linux系统下的所有进程都是由其父进程创建而来,譬如在 shell 终端通过命令的方式执行一个程序./app,那么 app进程就是由 shell 终端进程创建出来的,shell 终端就是该进程的父进程。进程号为 1 的进程便是所有进程的父进程,通常称为 init 进程,它是 Linux 系统启动之后运行的第一个进程,它管理着系统上所有其它进程,init 进程是由内核启动,因此理论上说它没有父进程。init 进程的 PID 总是为 1,它是所有子进程的父进程,一切从 1 开始、一切从 init 进程开始!一个进程的生命周期便是从创建开始直至其终止。
  2. 进程的正常终止有多种不同的方式,譬如在 main 函数中使用 return 返回、调用 exit()函数结束进程、调用_exit()或_Exit()函数结束进程等。在我们的程序当中,一般使用 exit()库函数而非_exit()系统调用,原因在于 exit()最终也会通过_exit()终止进程,但在此之前,它将会完成一些其它的工作,exit()函数会执行的动作如下:
    (1)如果程序中注册了进程终止处理函数,那么会调用终止处理函数。在 9.1.2 小节给大家介绍如何注
    册进程的终止处理函数;
    (2)刷新 stdio 流缓冲区。关于 stdio 流缓冲区的问题,稍后编写一个简单地测试程序进行说明;
    (3)执行_exit()系统调用。
  3. 在前面曾提到过,在我们的程序当中,父、子进程不应都使用 exit()终止,只能有一个进程使用 exit()、而另一个则使用_exit()退出,当然一般推荐的是子进程使用_exit()退出、而父进程则使用 exit()退出。其原因就在于调用 exit()函数终止进程时会刷新进程的 stdio 缓冲区

9.10 监视子进程

在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视,本小节我们就来学习下如何通过系统调用 wait()以及其它变体来监视子进程的状态改变。

9.10.1 wait()函数

对于许多需要创建子进程的进程来说,有时设计需要监视子进程的终止时间以及终止时的一些状态信息,在某些设计需求下这是很有必要的。系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息,其函数原型如下所示:

#include <sys/types.h>
#include <sys/wait.h>//若成功则返回终止的子进程对应的进程号;失败则返回-1。
pid_t wait(int *status);//参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。

参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,
可以通过以下宏来检查 status 参数:
⚫ WIFEXITED(status):如果子进程正常终止,则返回 true;
⚫ WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过WEXITSTATUS 宏转换;
⚫ WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
⚫ WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
⚫ WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;
使用示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main(void)
{
	 int status;
	 int ret;
	 int i;
	 /* 循环创建 3 个子进程 */
	 for (i = 1; i <= 3; i++) {
		 switch (fork()) {
			 case -1:
				 perror("fork error");
				 exit(-1);
			 case 0:
				 /* 子进程 */
				 printf("子进程<%d>被创建\n", getpid());
				 sleep(i);
				 _exit(i);
			 default:
				 /* 父进程 */
				 break;
	 		}
	 }
	 sleep(1);
	 printf("~~~~~~~~~~~~~~\n");
	 for (i = 1; i <= 3; i++) {
		 ret = wait(&status);
		 if (-1 == ret) {
			 if (ECHILD == errno) {
				 printf("没有需要等待回收的子进程\n");
				 exit(0);
	 		}
			 else {
				 perror("wait error");
				 exit(-1);
			 }
		}
	 	printf("回收子进程<%d>, 终止状态<%d>\n", ret,
	 	WEXITSTATUS(status));
	 }
	 exit(0);
}

在这里插入图片描述

9.10.2 waitpid()函数
  1. 使用 wait()系统调用存在着一些限制,这些限制包括如下:
    ⚫ 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
    ⚫ 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
    ⚫ 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。而设计 waitpid()则可以突破这些限制,waitpid()系统调用函数原型如下所示:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options)

pid:参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:
⚫ 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
⚫ 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
⚫ 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
⚫ 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。
status:与 wait()函数的 status 参数意义相同。
options:稍后介绍。
返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0,稍后介绍
参数 options 是一个位掩码,可以包括 0 个或多个如下标志:
⚫ WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
⚫ WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
⚫ WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。从以上的介绍可知,waitpid()在功能上要强于 wait()函数,它弥补了 wait()函数所带来的一些限制,具体在实际的编程使用当中,可根据自己的需求进行选择。
waitid()函数
2. 除了以上给大家介绍的 wait()和 waitpid()系统调用之外,还有一个 waitid()系统调用,waitid()与 waitpid()类似,不过 waitid()提供了更多的扩展功能

9.10.4 僵尸进程与孤儿进程
  1. 孤儿进程
    父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程,换言之,某一子进程的父进程结束后,该子进程调用 getppid()将返回 1,init 进程变成了孤儿进程的“养父”;
    事实上,/sbin/upstart 进程与 Ubuntu 系统图形化界面有关系,是图形化界面下的一个后台守护进程,可负责“收养”孤儿进程,所以图形化界面下,upstart 进程就自动成为了孤儿进程的父进程,既然在图形化界面下孤儿进程的父进程不是 init 进程,那么我们进入 Ubuntu 字符界面,按 Ctrl + Alt +F1 进入,从打印结果可以发现,此时孤儿进程的父进程就成了 init 进程,

  2. 僵尸进程
    进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。
    如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。子进程结束后其父进程并没有来得及立马给它“收尸”,子进程处于“曝尸荒野”的状态,在这么一个状态下,我们就将子进程成为僵尸进程;至于名字由来,肯定是对电影情节的一种效仿!当父进程调用 wait()(或其变体,下文不再强调)为子进程“收尸”后,僵尸进程就会被内核彻底删除。另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(),故而从系统中移除僵尸进程。
    如果父进程创建了某一子进程,子进程已经结束,而父进程还在正常运行,但父进程并未调用 wait()回收子进程,此时子进程变成一个僵尸进程。首先来说,这样的程序设计是有问题的,如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!所以,在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用 wait()将其回收,避免僵尸进程。

SIGCHLD 信号

当发生以下两种情况时,父进程会收到该信号:
⚫ 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
⚫ 当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。
子进程的终止属于异步事件,父进程事先是无法预知的,如果父进程有自己需要做的事情,它不能一直wait()阻塞等待子进程终止(或轮训),这样父进程将啥事也做不了,那么有什么办法来解决这样的尴尬情况,当然有办法,那就是通过 SIGCHLD 信号。那既然子进程状态改变时(终止、暂停或恢复),父进程会收到 SIGCHLD 信号,SIGCHLD 信号的系统默认处理方式是将其忽略,所以我们要捕获它、绑定信号处理函数,在信号处理函数中调用 wait()收回子进程,回收完毕之后再回到父进程自己的工作流程中。当调用信号处理函数时,会暂时将引发调用的信号添加到进程的信号掩码中(除非 sigaction()指定了 SA_NODEFER 标志),这样一来,当 SIGCHLD 信号处理函数正在为一个终止的子进程“收尸”时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次 SIGCHLD 信号,结果是,父进程的 SIGCHLD 信号处理函数每次只调用一次 wait(),那么就会导致有些僵尸进程成为“漏网之鱼”。
解决方案就是:在 SIGCHLD 信号处理函数中循环以非阻塞方式来调用 waitpid(),直至再无其它终止的子进程需要处理为止,所以,通常 SIGCHLD 信号处理函数内部代码如下所示:

static void wait_child(int sig)
{
	 /* 替子进程收尸 */
	 printf("父进程回收子进程\n");
	 while (waitpid(-1, NULL, WNOHANG) > 0)
	 	continue;
}

9.11 执行新程序

在前面已经大家提到了 exec 函数,当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码,那么这个时候子进程可以通过 exec 函数来实现运行另一个新的程序。本小节我们就来学习下,如何在程序中运行一个新的程序,从新程序的 main()函数开始运行。

9.11.1 execve()函数

系统调用 execve()可以将新程序加载到某一进程的内存空间,通过调用 execve()函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
//参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
//参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc, char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。argv[0]对应的便是新程序自身路径名。
//envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value。

execve 调用成功将不会返回;失败将返回-1,并设置 errno。系统调用 execve())称为 exec 族函数,所以 exec 函数并不是指某一个函数、而是 exec 族函数

9.11.2 exec 库函数

exec 族函数包括多个不同的函数,这些函数命名都以 exec 为前缀,上一小节给大家介绍的 execve()函数也属于 exec 族函数中的一员,但它属于系统调用;本小节介绍 exec 族函数中的库函数,这些库函数都是基于系统调用 execve()而实现的,虽然参数各异、但功能相同,包括:execl()、execlp()、execle()、execv()、execvp()、execvpe(),它们的函数原型如下所示:

#include <unistd.h>
extern char **environ;
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()和 execv()都是基本的 exec 函数,都可用于执行一个新程序,它们之间的区别在于参数格式不同;参数 path 意义和格式都相同,与系统调用 execve()的 filename 参数相同,指向新程序的路径名,既可以是绝对路径、也可以是相对路径。execl()和 execv()不同的在于第二个参数,execv()的argv 参数与 execve()的 argv 参数相同,也是字符串指针数组;而 execl()把参数列表依次排列,使用可变参数形式传递,本质上也是多个字符串,以 NULL 结尾,如下所示:

// execv 传参
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execv("./newApp", arg_arr);
// execl 传参
execl("./newApp", "./newApp", "Hello", "World", NULL);

⚫ execlp()和 execvp()在 execl()和 execv()基础上加了一个 p,这个 p 其实表示的是 PATH;execl()和execv()要求提供新程序的路径名,而 execlp()和 execvp()则允许只提供新程序文件名,系统会在由环境变量 PATH 所指定的目录列表中寻找相应的可执行文件,如果执行的新程序是一个 Linux 命令,这将很有用;当然,execlp()和 execvp()函数也兼容相对路径和绝对路径的方式。
⚫ execle()和 execvpe()这两个函数在命名上加了一个 e,这个 e 其实表示的是 environment 环境变量,意味着这两个函数可以指定自定义的环境变量列表给新程序,参数envp与系统调用execve()的envp参数相同,也是字符串指针数组,使用方式如下所示

// execvpe 传参
char *env_arr[5] = {"NAME=app", "AGE=25","SEX=man", NULL};
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execvpe("./newApp", arg_arr, env_arr);
// execle 传参
execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);
9.11.3 exec族函数使用示例

使用以上给大家介绍的 6 个 exec 库函数运行 ls 命令,并加入参数-a 和-l。

  1. execl()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
	 execl("/bin/ls", "ls", "-a", "-l", NULL);
	 perror("execl error");
	 exit(-1);
}
  1. execv()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
	 char *arg_arr[5];
	 arg_arr[0] = "ls";
	 arg_arr[1] = "-a";
	 arg_arr[2] = "-l";
	 arg_arr[3] = NULL;
	 execv("/bin/ls", arg_arr);
	 perror("execv error");
	 exit(-1);
}
  1. execlp()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
	 execlp("ls", "ls", "-a", "-l", NULL);
	 perror("execlp error");
	 exit(-1);
}
  1. execvp()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
	 char *arg_arr[5];
	 arg_arr[0] = "ls";
	 arg_arr[1] = "-a";
	 arg_arr[2] = "-l";
	 arg_arr[3] = NULL;
	 execvp("ls", arg_arr);
	 perror("execvp error");
	 exit(-1);
}
  1. execle()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main(void)
{
	 execle("/bin/ls", "ls", "-a", "-l", NULL, environ);
	 perror("execle error");
	 exit(-1);
}
  1. execvpe()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main(void)
{
	 char *arg_arr[5];
	 arg_arr[0] = "ls";
	 arg_arr[1] = "-a";
	 arg_arr[2] = "-l";
	 arg_arr[3] = NULL;
	 execvpe("ls", arg_arr, environ);
	 perror("execvpe error");
	 exit(-1);
}

以上所有的这些示例代码,运行结果都是一样的,与"ls -al"命令效果相同

9.11.4 system()函数
  1. 使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令,本小节来学习下 system()函数的用法
#include <stdlib.h>
int system(const char *command);//参数 command 指向需要执行的 shell 命令,以字符串的形式提供,譬如"ls -al"、"echo HelloWorld"等
  1. system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能,首先 system()会调用 fork()创建一个子进程来运行 shell(可以把这个子进程成为 shell 进程),并通过 shell 执行参数command 所指定的命令。譬如:system(“ls -la”)、system(“echo HelloWorld”)
  2. system()的返回值如下:
    ⚫ 当参数 command 为 NULL,如果 shell 可用则返回一个非 0 值,若不可用则返回 0;针对一些非UNIX 系统,该系统上可能是没有 shell 的,这样就会导致 shell 不可能;如果 command 参数不为NULL,则返回值从以下的各种情况所决定。
    ⚫ 如果无法创建子进程或无法获取子进程的终止状态,那么 system()返回-1;
    ⚫ 如果子进程不能执行 shell,则 system()的返回值就好像是子进程通过调用_exit(127)终止了;
    ⚫ 如果所有的系统调用都成功,system()函数会返回执行 command 的 shell 进程的终止状态。
    system()的主要优点在于使用上方便简单,编程时无需自己处理对 fork()、exec 函数、waitpid()以及 exit()等调用细节,system()内部会代为处理;当然这些优点通常是以牺牲效率为代价的,使用 system()运行 shell命令需要至少创建两个进程,一个进程用于运行 shell、另外一个或多个进程则用于运行参数 command 中解析出来的命令,每一个命令都会调用一次 exec 函数来执行;所以从这里可以看出,使用 system()函数其效率会大打折扣,如果我们的程序对效率或速度有所要求,那么建议大家不是直接使用 system()。

9.12 进程状态与进程关系

9.12.1 进程状态

Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
⚫ 就绪态(Ready):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程;
⚫ 运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
⚫ 僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”;
⚫ 可中断睡眠状态:可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒;
⚫ 不可中断睡眠状态:不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态),表示进程处于一种等待状态,等待某种条件成立之后便会进入到就绪态;所以,处于等待态的进程是无法参与进程系统调度的。
⚫ 暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。
一个新创建的进程会处于就绪态,只要得到 CPU 就能被执行。以下列出了进程各个状态之间的转换关系,如下所示:
在这里插入图片描述

9.12.2 进程关系
  1. 在 Linux 系统下,每个进程都有自己唯一的标识:进程号(进程 ID、PID),也有自己的生命周期,进程都有自己的父进程、而父进程也有父进程,这就形成了一个以 init 进程为根的进程家族树;当子进程终止时,父进程会得到通知并能取得子进程的退出状态。除此之外,进程间还存在着其它一些层次关系,譬如进程组和会话;所以,由此可知,进程间存在着多种不同的关系,主要包括:无关系(相互独立)、父子进程关系、进程组以及会话。
  2. 每个进程除了有一个进程 ID、父进程 ID 之外,还有一个进程组 ID,用于标识该进程属于哪一个进程组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。
  3. Linux 系统设计进程组实质上是为了方便对进程进行管理。假设为了完成一个任务,需要并发运行 100个进程,但当处于某种场景时需要终止这 100 个进程,若没有进程组就需要一个一个去终止,这样非常麻烦且容易出现一些问题;有了进程组的概念之后,就可以将这 100 个进程设置为一个进程组,这些进程共享一个进程组 ID,这样一来,终止这 100 个进程只需要终止该进程组即可。
    关于进程组需要注意以下以下内容:
    ⚫ 每个进程必定属于某一个进程组、且只能属于一个进程组;
    ⚫ 每一个进程组有一个组长进程,组长进程的 ID 就等于进程组 ID;
    ⚫ 在组长进程的 ID 前面加上一个负号即是操作进程组;
    ⚫ 组长进程不能再创建新的进程组;
    ⚫ 只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关;
    ⚫ 一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离开该进程组;
    ⚫ 默认情况下,新创建的进程会继承父进程的进程组 ID。
    通过系统调用 getpgrp()或 getpgid()可以获取进程对应的进程组 ID,其函数原型如下所示:
#include <unistd.h>
pid_t getpgid(pid_t pid);
pid_t getpgrp(void);
  1. 调用系统调用 setpgid()或 setpgrp()可以加入一个现有的进程组或创建一个新的进程组,其函数原型如下所示:
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void);
  1. setpgid()函数将参数 pid 指定的进程的进程组 ID 设置为参数 gpid。如果这两个参数相等(pid==gpid),则由 pid 指定的进程变成为进程组的组长进程,创建了一个新的进程;如果参数 pid 等于 0,则使用调用者的进程 ID;另外,如果参数 gpid 等于 0,则创建一个新的进程组,由参数 pid 指定的进程作为进程组组长进程。setpgrp()函数等价于 setpgid(0, 0)。一个进程只能为它自己或它的子进程设置进程组 ID,在它的子进程调用 exec 函数后,它就不能更改该子进程的进程组 ID 了。
    4、会话
    介绍完进程组之后,再来看下会话,会话是一个或多个进程组的集合,其与进程组、进程之间的关系如下图所示:
    图 9.12.3 会话
  • 一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一个会话首领(leader),即创建会话的进程。一个会话可以有控制终端、也可没有控制终端,在有控制终端的情况下也只能连接一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(譬如通过 SSH 协议网络登录),一个会话中的进程组可被分为一个前台进程组以及一个或多个后台进程组。
  • 会话的首领进程连接一个终端之后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进程被称为控制进程;产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程,譬如 Ctrl + C(产生 SIGINT 信号)、Ctrl + Z(产生 SIGTSTP 信号)、Ctrl + \(产生 SIGQUIT 信号)等等这些由控制终端产生的信号。
  • 当用户在某个终端登录时,一个新的会话就开始了;当我们在 Linux 系统下打开了多个终端窗口时,实际上就是创建了多个终端会话。
    一个进程组由组长进程的 ID 标识,而对于会话来说,会话的首领进程的进程组 ID 将作为该会话的标识,也就是会话 ID(sid),在默认情况下,新创建的进程会继承父进程的会话 ID。通过系统调用 getsid()可以获取进程的会话 ID,其函数原型如下所示:
#include <unistd.h>
pid_t getsid(pid_t pid);
  • 如果参数 pid 为 0,则返回调用者进程的会话 ID;如果参数 pid不为 0,则返回参数 pid 指定的进程对应的会话 ID。成功情况下,该函数返回会话 ID,失败则返回-1、并设置 errno。
    使用系统调用 setsid()可以创建一个会话:
#include <unistd.h>
pid_t setsid(void);

如果调用者进程不是进程组的组长进程,调用 setsid()将创建一个新的会话,调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程,调用 setsid()创建的会话将没有控制终端。setsid()调用成功将返回新会话的会话 ID;失败将返回-1,并设置 errno。

9.13 守护进程

  • 守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。守护进程与终端无任何关联,用户的登录与注销与守护进程无关、不受其影响,守护进程自成进程组、自成会话,即pid=gid=sid。通过命令"ps -ajx"查看系统所有的进程,会打印进程的sid、gid,TTY 一栏是问号?表示该进程没有控制终端,也就是守护进程,其中 COMMAND 一栏使用中括号[]括起来的表示内核线程,这些线程是在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用 k 开头的名字,表示 Kernel。

如何将自己编写的程序运行之后变成一个守护进程呢?本小节就来学习如何编写守护进程程序,编写守护进程一般包含如下几个步骤:

  1. 创建子进程、终止父进程
    父进程调用 fork()创建子进程,然后父进程使用 exit()退出,这样做实现了下面几点。第一,如果该守护进程是作为一条简单地 shell 命令启动,那么父进程终止会让 shell 认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组ID,但它有自己独立的进程ID,这保证了子进程不是一个进程组的组长进程,这是下面将要调用 setsid 函数的先决条件!
  2. 子进程调用 setsid 创建会话
    这步是关键,在子进程中调用上一小节给大家介绍的 setsid()函数创建新的会话,由于之前子进程并不是进程组的组长进程,所以调用 setsid()会使得子进程创建一个新的会话,子进程成为新会话的首领进程,同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。所以这里调用 setsid 有三个作用:让子进程摆脱原会话的控制、让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。在调用 fork 函数时,子进程继承了父进程的会话、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。setsid 函数能够使子进程完全独立出来,从而脱离所有其他进程的控制。
  3. 将工作目录更改为根目录
    子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录,当然也可以指定其它目录来作为守护进程的工作目录。
  4. 重设文件权限掩码 umask
    文件权限掩码 umask 用于对新建文件的权限位进行屏蔽,在 5.5.5 小节中有介绍。由于使用 fork 函数新建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为 0,确保子进程有最大操作权限、这样可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是 umask,通常的使用方法为 umask(0)。
  5. 关闭不再需要的文件描述符
    子进程继承了父进程的所有文件描述符,这些被打开的文件可能永远不会被守护进程(此时守护进程指的就是子进程,父进程退出、子进程成为守护进程)读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载,所以必须关闭这些文件,这使得守护进程不再持有从其父进程继承过来的任何文件描述符。
  6. 将文件描述符号为 0、1、2 定位到/dev/null
    将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,这使得守护进程的输出无处显示、也无处从交互式用户那里接收输入。
  7. 其它:忽略 SIGCHLD 信号
    处理 SIGCHLD 信号不是必须的,但对于某些进程,特别是并发服务器进程往往是特别重要的,服务器进程在接收到客户端请求时会创建子进程去处理该请求,如果子进程结束之后,父进程没有去 wait 回收子进程,则子进程将成为僵尸进程;如果父进程 wait 等待子进程退出,将又会增加父进程的负担、也就是增加服务器的负担,影响服务器进程的并发性能,在 Linux 下,可以将 SIGCHLD 信号的处理方式设置为
    SIG_IGN,也就是忽略该信号,可让内核将僵尸进程转交给 init 进程去处理,这样既不会产生僵尸进程、又省去了服务器进程回收子进程所占用的时间。
    守护进程一般以单例模式运行,我们根据上面的介绍的步骤,来编写一个守护进程程序,示例代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int main(void)
{
	 pid_t pid;
	 int i;
	 /* 创建子进程 */
	 pid = fork();
	 if (0 > pid) {
		 perror("fork error");
		 exit(-1);
	 }
	 else if (0 < pid)//父进程
		exit(0); //直接退出
	 /*
	 *子进程
	 */
	 /* 1.创建新的会话、脱离控制终端 */
	 if (0 > setsid()) {
		 perror("setsid error");
		 exit(-1);
	 }
	 /* 2.设置当前工作目录为根目录 */
	 if (0 > chdir("/")) {
		 perror("chdir error");
		 exit(-1);
	 }
	 /* 3.重设文件权限掩码 umask */
	 umask(0);
	 /* 4.关闭所有文件描述符,调用 sysconf(_SC_OPEN_MAX)用于获取当前系统允许进程打开的最大文件数量 */
	 for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
	 	close(i);
	 /* 5.将文件描述符号为 0、1、2 定位到/dev/null */
	 open("/dev/null", O_RDWR);
	 dup(0);
	 dup(0);
	 /* 6.忽略 SIGCHLD 信号 */
	 signal(SIGCHLD, SIG_IGN);
	 /* 正式进入到守护进程 */
	 for ( ; ; ) {
		 sleep(1);
		 puts("守护进程运行中......");
	 }
	 exit(0);
}

我们在守护进程中添加了死循环,每隔 1 秒钟打印一行字符串信息,运行之后,没有任何打印信息输出,原因在于守护进程已经脱离了控制终端,它的打印信息并不会输出显示到终端,在代码中已经将标准输入、输出以及错误重定位到了/dev/null,/dev/null 是一个黑洞文件,自然是看不到输出信息。使用"ps -ajx"命令查看进程.

9.14 单例模式运行

  • 即程序只可单个运行,不可开多个程序,像vsftpd,sshd等程序。通过一个特定的文件来实现,当程序启动之后,首先打开该文件,调用 open 时一般使用O_WRONLY | O_CREAT 标志,当文件不存在则创建该文件,然后尝试去获取文件锁,若是成功,则将程序的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁),保证进程一直持有该文件锁;若是程序获取锁失败,代表程序已经被运行、则退出本次启动。
    Tips:当程序退出或文件关闭之后,文件锁会自动解锁!
    文件锁属于本书高级 I/O 章节内容,通过系统调用flock()、fcntl()或库函数 lockf()均可实现对文件进行上锁,本小节我们以系统调用flock()为例,系统调用 flock()产生的是咨询锁(建议性锁)、并不能产生强制性锁。
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#define LOCK_FILE "./testApp.pid"
int main(void)
{
	 char str[20] = {0};
	 int fd;
	 /* 打开 lock 文件,如果文件不存在则创建 */
	 fd = open(LOCK_FILE, O_WRONLY | O_CREAT, 0666);
	 if (-1 == fd) {
		 perror("open error");
		 exit(-1);
	 }
	 /* 以非阻塞方式获取文件锁 */
	 if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
		 fputs("不能重复执行该程序!\n", stderr);
		 close(fd);
		 exit(-1);
	 }
	 puts("程序运行中...");
	 ftruncate(fd, 0); //将文件长度截断为 0
	 sprintf(str, "%d\n", getpid());
	 write(fd, str, strlen(str));//写入 pid
	 for ( ; ; )
	 	sleep(1);
	 exit(0);
}

程序启动首先打开一个特定的文件,这里只是举例,以当前目录下的 testApp.pid 文件作为特定文件,以 O_WRONLY | O_CREAT 方式打开,如果文件不存在则创建该文件;打开文件之后使用 flock 尝试获取文件锁,调用 flock()时指定了互斥锁标志 LOCK_NB,意味着同时只能有一个进程拥有该锁,如果获取锁失败,表示该程序已经启动了,无需再次执行,然后退出;如果获取锁成功,将进程的 PID 写入到该文件中,当程序退出时,会自动解锁、关闭文件。
这种机制在一些程序尤其是服务器程序中很常见,服务器程序使用这种方法来保证程序的单例模式运行;在 Linux 系统中/var/run/目录下有很多以.pid 为后缀结尾的文件,这个实际上是为了保证程序以单例模式运行而设计的,作为程序实现单例模式运行所需的特定文件.

15.控制led


15_led.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define LED_TRIGGER "/sys/class/leds/user-led/trigger"
#define LED_BRIGHTNESS "/sys/class/leds/user-led/brightness"
#define USAGE() fprintf(stderr, "usage:\n" \
" %s <on|off>\n" \
" %s <trigger> <type>\n", argv[0], argv[0])
int main(int argc, char *argv[])
{
int fd1, fd2;
/* 校验传参 */
if (2 > argc) {
USAGE();
exit(-1);
/* 打开文件 */
fd1 = open(LED_TRIGGER, O_RDWR);
if (0 > fd1) {
perror("open error");
exit(-1);
}
fd2 = open(LED_BRIGHTNESS, O_RDWR);
if (0 > fd2) {
perror("open error");
exit(-1);
}
/* 根据传参控制 LED */
if (!strcmp(argv[1], "on")) {
write(fd1, "none", 4); //先将触发模式设置为 none
write(fd2, "1", 1);
//点亮 LED
}
else if (!strcmp(argv[1], "off")) {
write(fd1, "none", 4); //先将触发模式设置为 none
write(fd2, "0", 1);
//LED 灭
}
else if (!strcmp(argv[1], "trigger")) {
if (3 != argc) {
USAGE();
exit(-1);
}
if (0 > write(fd1, argv[2], strlen(argv[2])))
perror("write error");
}
else
USAGE();
exit(0);
}

使用${CC} -o 15_led 15_led.c编译

在虚拟机使用ifconfig ens33 192.168.137.4配置ip

开发板ifconfig eth 192.168.137.3配置ip

开发板:scp tao@192.168.137.4:~/linux/c_cpp/15_led ~/将文件传输至开发板。

./15_led on点亮ds1,./15_led off熄灭ds1,./15_led trigger heartbeat使其闪烁。

也可以使用 DS0 进行测试,将 源码中的路径修改一下即可(/sys/class/leds/user-led/修改为/sys/class/leds/sys-led/)

16.操作GPIO

​* 与 LED 设备一样,GPIO 同样也是通过 sysfs 方式进行操控,进入到/sys/class/gpio 目录下,如下所示:
图 16.1.1 /sys/class/gpio 目录
在这里插入图片描述

可以看到该目录下包含两个文件 export、unexport 以及许多个以 gpiochipX(X 等于 0、32、64、96、
128)命名的文件夹。

  • gpiochipX:当前 SoC 所包含的 GPIO 控制器,我们知道 STM32MP157 一共包含了 12 个 GPIO 控制器,分别为 GPIOA、GPIOB、GPIOC…在这里分别对应 gpiochip0、gpiochip16、gpiochip32…以此类推,每一个 gpiochipX 文件夹用来管理一组 GPIO。随便进入到其中某个目录下,可以看到这些目录下包含了如下文件:
    在这里插入图片描述
    图 16.1.2 gpiochip0 目录下的文件,在这个目录我们主要关注的是 base、label、ngpio 这三个属性文件,这三个属性文件均是只读、不可写。
  • base:与 gpiochipX 中的 X 相同,表示该控制器所管理的这组 GPIO 引脚中最小的编号。每一个 GPIO引脚都会有一个对应的编号,Linux 下通过这个编号来操控对应的 GPIO 引脚;label:该组 GPIO 对应的标签,也就是名字;ngpio:该控制器所管理的 GPIO 引脚的数量(所以引脚编号范围是:base ~ base+ngpio-1);可使用cat ngpio查看;
  • 对于给定的一个 GPIO 引脚GPIOB_IO10,那它对应的编号是16 + 10 = 26;同理 GPIOC_IO05对应的编号是 32 + 5 = 37。
  • export:用于将指定编号的 GPIO 引脚导出。
  • 在使用 GPIO 引脚之前,需要将其导出,导出成功之后才能使用它。注意 export 文件是只写文件,不能读取,将一个指定的编号写入到 export 文件中即可将对应的 GPIO 引脚导出,譬如:
    echo 0 > export # 导出编号为 0 的 GPIO 引脚(对于 STM32MP157 来说,也就是 GPIOA_IO0)
    导出成功之后会发现在/sys/class/gpio 目录下生成了一个名为 gpio0 的文件夹(gpioX,X 表示对应的编号)在这里插入图片描述
  • 这个文件夹就是导出来的 GPIO 引脚对应的文件夹,用于管理、控制该 GPIO 引脚,
  • unexport:将导出的 GPIO 引脚删除,也就是取消导出 GPIO。
  • 当使用完 GPIO 引脚之后,我们需要将导出的引脚删除,同样该文件也是只写文件、不可读,譬如:
    echo 0 > unexport # 删除导出的编号为 0 的 GPIO 引脚,取消导出 GPIO

16.2导出的引脚文件夹

  1. direction:配置 GPIO 引脚为输入或输出模式。该文件可读、可写,读表示查看 GPIO 当前是输入还是输出模式,写表示将 GPIO 配置为输入或输出模式;读取或写入操作可取的值为"out"(输出模式)和"in"(输入模式)
  2. value:在 GPIO 配置为输出模式下,向 value 文件写入"0"控制 GPIO 引脚输出低电平,写入"1"则控制 GPIO 引脚输出高电平。在输入模式下,读取 value 文件获取 GPIO 引脚当前的输入电平状态。
  3. 获取 GPIO 引脚的输入电平状态
    echo “in” > direction
    cat value
  4. 控制 GPIO 引脚输出高电平
    echo “out” > direction
    echo “1” > value
  5. active_low:这个属性文件用于控制极性,可读可写,默认情况下为 0,譬如:
  6. active_low 等于 0 时
echo "0" > active_low
echo "out" > direction
echo "1" > value #输出高
echo "0" > value #输出低
#### active_low 等于 1 时
echo "1" > active_low
echo "out" > direction
echo "1" > value #输出低
echo "0" > value #输出高

由此看出,active_low 的作用已经非常明显了,就是用于控制极性,对于输入模式来说也同样适用。

  1. edge:控制中断的触发模式,
  • 该文件可读可写。在配置 GPIO 引脚的中断触发模式之前,需将其设置为输入模式:
    非中断引脚:echo “none” > edge
    上升沿触发:echo “rising” > edge
    下降沿触发:echo “falling” > edge
    边沿触发:echo “both” > edge
    当引脚被配置为中断后可以使用 poll()函数监听引脚的电平状态变化
    输出示例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
 char file_path[100];
 int len;
 int fd;
 sprintf(file_path, "%s/%s", gpio_path, attr);
 if (0 > (fd = open(file_path, O_WRONLY))) {
 perror("open error");
 return fd;
 }
 len = strlen(val);
 if (len != write(fd, val, len)) {
 perror("write error");
 close(fd);
 return -1;
 }
 close(fd); //关闭文件
 return 0;
}
int main(int argc, char *argv[])
{
 /* 校验传参 */
 if (3 != argc) {
 fprintf(stderr, "usage: %s <gpio> <value>\n", argv[0]);
 exit(-1);
 }
 /* 判断指定编号的 GPIO 是否导出 */
 sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
 if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
 int fd;
 int len;
 if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
 perror("open error");
 exit(-1);
 }
 len = strlen(argv[1]);
 if (len != write(fd, argv[1], len)) {//导出 gpio
 perror("write error");
 close(fd);
 exit(-1);
 }
 close(fd); //关闭文件
 }
 /* 配置为输出模式 */
 if (gpio_config("direction", "out"))
 exit(-1);
 /* 极性设置 */
 if (gpio_config("active_low", "0"))
 exit(-1);
 /* 控制 GPIO 输出高低电平 */
 if (gpio_config("value", argv[2]))
 exit(-1);
 /* 退出程序 */
 exit(0);
}

执行程序时需要传入两个参数,argv[1]指定 GPIO 的编号、argv[2]指定输出电平状态(0 表示低电平、1 表示高电平)。
如./gpio 0 1表示gpio0 输出高电平

16.3 gpio输入demo

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
 char file_path[100];
 int len;
 int fd;
 sprintf(file_path, "%s/%s", gpio_path, attr);
 if (0 > (fd = open(file_path, O_WRONLY))) {
 		perror("open error");
 		return fd;
 }
 len = strlen(val);
 if (len != write(fd, val, len)) {
 		perror("write error");
 		close(fd);
 		return -1;
 }
 close(fd); //关闭文件
 return 0;
}
int main(int argc, char *argv[])
{
 char file_path[100];
 char val;
 int fd;
 /* 校验传参 */
 if (2 != argc) {
 		fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
 		exit(-1);
 }
 /* 判断指定编号的 GPIO 是否导出 */
 sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
 if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
 	int len;
 	if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
 		perror("open error");
 		exit(-1);
 	}
 	len = strlen(argv[1]);
	if (len != write(fd, argv[1], len)) {//导出 gpio
 		perror("write error");
 		close(fd);
 		exit(-1);
 	}
 close(fd); //关闭文件
 }
 /* 配置为输入模式 */
 if (gpio_config("direction", "in"))
 	exit(-1);
 /* 极性设置 */
 if (gpio_config("active_low", "0"))
 	exit(-1);
 /* 配置为非中断方式 */
 if (gpio_config("edge", "none"))
 	exit(-1);
 /* 读取 GPIO 电平状态 */
 sprintf(file_path, "%s/%s", gpio_path, "value");
 if (0 > (fd = open(file_path, O_RDONLY))) {
 	perror("open error");
 	exit(-1);
 	}
 if (0 > read(fd, &val, 1)) {
 	perror("read error");
 	close(fd);
 	exit(-1);
 }
 printf("value: %c\n", val);
 /* 退出程序 */
 close(fd);
 exit(0);
}

执行程序时需要传入一个参数,argv[1]指定要读取电平状态的 GPIO 对应的编号。

16.4 gpio 中断demo

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
 	char file_path[100];
 	int len;
 	int fd;
 	sprintf(file_path, "%s/%s", gpio_path, attr);
	 if (0 > (fd = open(file_path, O_WRONLY))) {
 		perror("open error");
 		return fd;
 	}
	len = strlen(val);
 	if (len != write(fd, val, len)) {
 		perror("write error");
 		return -1;
 	}
 	close(fd); //关闭文件
 	return 0;
}
int main(int argc, char *argv[])
{
 	struct pollfd pfd;
 	char file_path[100];
 	int ret;
 	char val;
 	/* 校验传参 */
 	if (2 != argc) {
 		fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
 		exit(-1);
 	}
 	/* 判断指定编号的 GPIO 是否导出 */
 	sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
 	if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
 		int len;
 		int fd;
 		if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
 			perror("open error");
 			exit(-1);
 		}
 		len = strlen(argv[1]);
 		if (len != write(fd, argv[1], len)) {//导出 gpio
 			perror("write error");
 			exit(-1);
 		}
 		close(fd); //关闭文件
 	}
 	/* 配置为输入模式 */
 	if (gpio_config("direction", "in"))
 		exit(-1);
	 /* 极性设置 */
	 if (gpio_config("active_low", "0"))
	 	exit(-1);
	 /* 配置中断触发方式: 上升沿和下降沿 */
	 if (gpio_config("edge", "both"))
	 	exit(-1);
	 /* 打开 value 属性文件 */
	 sprintf(file_path, "%s/%s", gpio_path, "value");
	 if (0 > (pfd.fd = open(file_path, O_RDONLY))) {
	 	perror("open error");
		exit(-1);
	 }
	 /* 调用 poll */
	 pfd.events = POLLPRI; //只关心高优先级数据可读(中断)
	 read(pfd.fd, &val, 1);//先读取一次清除状态
	 for ( ; ; ) {
	 	ret = poll(&pfd, 1, -1); //调用 poll
	 	if (0 > ret) {
	 		perror("poll error");
	 		exit(-1);
	 	}
	 	else if (0 == ret) {
	 		fprintf(stderr, "poll timeout.\n");
	 		continue;
	 	}
	 /* 校验高优先级数据是否可读 */
	 if(pfd.revents & POLLPRI) {
	 	if (0 > lseek(pfd.fd, 0, SEEK_SET)) {//将读位置移动到头部
	 		perror("lseek error");
	 		exit(-1);
	 	}
	 	if (0 > read(pfd.fd, &val, 1)) {
	 		perror("read error");
			 exit(-1);
	 	}
	 	printf("GPIO 中断触发<value=%c>\n", val);
	 	}
	 }
	 /* 退出程序 */
	 exit(0);
}

执行程序时需要传入一个参数,argv[1]指定要读取电平状态的 GPIO 对应的编号。

3.输入设备应用编程

文章目录
Markdown 8363 字数 398 行数 当前行 1, 当前列 0HTML 7914 字数 368 段落

25.v4l2摄像头

25.1 预定义及头文件部分

#include <opencv2/opencv.hpp>  
#include <iostream>  
#include <fstream>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>
#include<assert.h>
#include<limits.h>
//#include<opencv/highgui.h>
//#include <opencv2/opencv.hpp>
//#include<iterator>
#define FB_DEV              "/dev/fb0"      //LCD设备节点
#define FRAMEBUFFER_COUNT   3               //帧缓冲数量
/*** 摄像头像素格式及其描述信息 ***/
typedef struct camera_format {
    unsigned char description[32];  //字符串描述信息
    unsigned int pixelformat;       //像素格式
} cam_fmt;
/*** 描述一个帧缓冲的信息 ***/
typedef struct cam_buf_info {
    unsigned short *start;      //帧缓冲起始地址
    unsigned long length;       //帧缓冲长度
} cam_buf_info;
static int width;                       //LCD宽度
static int height;                      //LCD高度
static int line_length;
static unsigned short *screen_base = NULL;//LCD显存基地址
static int fb_fd = -1;                  //LCD设备文件描述符
static int v4l2_fd = -1;                //摄像头设备文件描述符
static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
static cam_fmt cam_fmts[10];
static int frm_width, frm_height;   //视频帧宽度和高度
  1. LCD缓冲初始化
static int fb_dev_init(void)
{
    struct fb_var_screeninfo fb_var = {0};
    struct fb_fix_screeninfo fb_fix = {0};
    unsigned long screen_size;
    fb_fd = open(FB_DEV, O_RDWR);/* 打开framebuffer设备 */
    if (0 > fb_fd) {
        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
        return -1;
    }
    ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var); /* 获取framebuffer设备信息 */
    ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);
    screen_size = fb_fix.line_length * fb_var.yres;
    width = fb_var.xres;
    height = fb_var.yres;
    line_length = fb_fix.line_length / (fb_var.bits_per_pixel / 8);
    /* 内存映射 */
    screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fb_fd);
        return -1;
    }
    /* LCD背景刷白 */
    memset(screen_base, 0xFF, screen_size);
    return 0;
}

25.2 v4l2设备初始化

static int v4l2_dev_init(const char *device)
{
    struct v4l2_capability cap = {0};
    /* 打开摄像头 */
    v4l2_fd = open(device, O_RDWR);
    if (0 > v4l2_fd) {
        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
        return -1;
    }
    /* 查询设备功能 */
    ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);
    /* 判断是否是视频采集设备 */
    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities)) {
        fprintf(stderr, "Error: %s: No capture video device!\n", device);
        close(v4l2_fd);
        return -1;
    }
    return 0;
}
  1. v4l2列举可用格式

static void v4l2_enum_formats(void)
{
    struct v4l2_fmtdesc fmtdesc = {0};
    /* 枚举摄像头所支持的所有像素格式以及描述信息 */
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
        // 将枚举出来的格式以及描述信息存放在数组中
        cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
        strcpy(cam_fmts[fmtdesc.index].description, fmtdesc.description);
        fmtdesc.index++;
    }
}
  1. v4l2打印可用格式
static void v4l2_print_formats(void)
{
    struct v4l2_frmsizeenum frmsize = {0};
    struct v4l2_frmivalenum frmival = {0};
    int i;
    frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    for (i = 0; cam_fmts[i].pixelformat; i++) {
        printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,cam_fmts[i].description);
        /* 枚举出摄像头所支持的所有视频采集分辨率 */
        frmsize.index = 0;
        frmsize.pixel_format = cam_fmts[i].pixelformat;
        frmival.pixel_format = cam_fmts[i].pixelformat;
        while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {
            printf("size<%d*%d> ",frmsize.discrete.width,frmsize.discrete.height);
			frmsize.index++;
            /* 获取摄像头视频采集帧率 */
            frmival.index = 0;
            frmival.width = frmsize.discrete.width;
            frmival.height = frmsize.discrete.height;
            while (0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {
                printf("<%dfps>", frmival.discrete.denominator /frmival.discrete.numerator);
                frmival.index++;
            }
            printf("\n");
        }
        printf("\n");
    }
}

  1. v4l2设置图像格式
static int v4l2_set_format(void)
{
    struct v4l2_format fmt = {0};
    struct v4l2_streamparm streamparm = {0};
    /* 设置帧格式 */
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//type类型
    fmt.fmt.pix.width =640;//width;  //视频帧宽度
    fmt.fmt.pix.height =480;//视频帧高度
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;  //像素格式 RGB565 -> YUYV
    if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt)) {
       fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
       return -1;
    }
    /*** 判断是否已经设置为我们要求的YUYV像素格式,如果没有设置成功表示该设备不支持的像素格式 */
    if (V4L2_PIX_FMT_YUYV != fmt.fmt.pix.pixelformat) {
        fprintf(stderr, "Error: the device does not support YUYV format!\n");
        return -1;
    }
    frm_width = fmt.fmt.pix.width;  //获取实际的帧宽度
    frm_height = fmt.fmt.pix.height;//获取实际的帧高度
    printf("视频帧大小<W,H>:<%d * %d>\n", frm_width, frm_height);
    /* 获取streamparm */
    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);
    /** 判断是否支持帧率设置 **/
    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
        streamparm.parm.capture.timeperframe.numerator = 1;
        streamparm.parm.capture.timeperframe.denominator = 30;//30fps
        if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {
            fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
            return -1;
        }
    }
    return 0;
}

25.3 v4l2初始化缓冲器

static int v4l2_init_buffer(void)
{
    struct v4l2_requestbuffers reqbuf = {0};
    struct v4l2_buffer buf = {0};
    /* 申请帧缓冲 */
    reqbuf.count = FRAMEBUFFER_COUNT;       //帧缓冲的数量
    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuf.memory = V4L2_MEMORY_MMAP;
    if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf)) {
        fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
        return -1;
    }
    /* 建立内存映射 */
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
        ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
        buf_infos[buf.index].length = buf.length;
        buf_infos[buf.index].start = mmap(NULL, buf.length,PROT_READ | PROT_WRITE, MAP_SHARED,v4l2_fd, buf.m.offset);
        if (MAP_FAILED == buf_infos[buf.index].start) {
            perror("mmap error");
            return -1;
        }
    }
    /* 入队 */
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
        if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf)) {
            fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
            return -1;
        }
    }
    return 0;
}

  1. v4l2开启视频流
static int v4l2_stream_on(void)

{
    /* 打开摄像头、摄像头开始采集数据 */
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type)) {
        fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
        return -1;
    }
    return 0;
}

25.4 yuyv格式数据转换为RGB565

unsigned short yuyv_to_rgb565(unsigned short y, unsigned short u,unsigned short v) {  
   int16_t r, g, b;  
    // 将Y, U, V从8位无符号转换为带符号的16位值  
    int16_t y1 = y;  
     int16_t u1 = u - 128;    
     int16_t v1 = v - 128;  
    r = y1 + 1.042*(v1);
    g = y1- 0.34414*(u1) - 0.71414*v1;
    b = y1+ 1.772*u1;
    // 将r, g, b值限制在0-255的范围内  
    r = r < 0 ? 0 : (r > 255 ? 255 : r);  
    g = g < 0 ? 0 : (g > 255 ? 255 : g);  
    b = b < 0 ? 0 : (b > 255 ? 255 : b);  
    // 将r, g, b值转换为RGB565格式  
    return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);  
}
  1. 读取图像帧用lcd显示
static void v4l2_read_data(void)

{
    struct v4l2_buffer buf = {0};
    unsigned short *base;
    unsigned short *start;
    int min_w, min_h;
    int j;
    if (width > frm_width)//以下判断仅为了适配屏幕大小
        min_w = frm_width;
    else
        min_w = width;
    if (height > frm_height)
        min_h = frm_height;
    else
        min_h = height;
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for ( ; ; ) {
        for(buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) { 
            ioctl(v4l2_fd, VIDIOC_DQBUF, &buf);     //出队
            printf("length:%d\n",buf_infos[buf.index].length);
            //  cv::Mat bgrImage = convertYUYVtoBGR(buf_infos[buf.index].start,frm_width,frm_height);//使用opencv将yuyv格式转化为jpg图像
            // cv::imwrite("img.jpg",bgrImage);
    for (j = 0, base=screen_base, start=buf_infos[buf.index].start;j < min_h; j++) {//base是屏幕基地址,start是帧起始地址
		for (int i = 0; i < min_w; i +=2) {  //unsigned short一次两个字节
            unsigned short  y0 = start[i] & 0x00ff;  //低字节存放的Y
            unsigned short u  = start[i] >> 8;  //高字节存放的U或者V
            unsigned short y1 = start[i + 1]  & 0x00ff;  
            unsigned short v  = start[i + 1] >> 8;  //相邻两个Y共用U和V,YUYV一共4个字节数据
            unsigned short rgb0 = yuyv_to_rgb565(y0, u, v); // 第一个像素  
            unsigned short rgb1 = yuyv_to_rgb565(y1, u, v); // 第二个像素  
            *(unsigned short *)base = rgb0;  //RGB565适用于正点原子4.3寸屏幕显示,一个像素两个字节
            base += 1;  
            *(unsigned short *)base = rgb1;  
            base += 1;     
    }                  
                base = screen_base+(j+1)* line_length;//line_length是屏幕的宽度width
                start += frm_width;//指向下一行数据
            }  
            ioctl(v4l2_fd, VIDIOC_QBUF, &buf);// 数据处理完之后、再入队、往复
        }
    }
}

25.5使用opencv保存图像



cv::Mat convertYUYVtoBGR( unsigned short * yuyvData, int w, int h) {  
    printf("frame:<w,h>:<%d,%d>\n",w,h);//w,h图像帧的宽度和高度
    cv::Mat bgrImage(w, h, CV_8UC3);  //初始化矩阵宽度及高度,CV_8UC3表示8位无符号整形,每个位置存放三个字节元素
	unsigned short *start;
	int j=0;
      for (start=yuyvData;j < h; j++) {
		for (int i = 0; i< w; i +=2) {  
            unsigned short  Y1 = start[i] & 0x00ff;  
            unsigned short U  = start[i] >> 8;  
            unsigned short Y2 = start[i + 1]  & 0x00ff;  
            unsigned short V  = start[i + 1] >> 8;  

            int U_ = U - 128;  
            int V_ = V - 128;  
            int R1 = Y1 + 1.402 * V_;  
            int G1 = Y1 - 0.34414 * U_ - 0.71414 * V_;  
            int B1 = Y1 + 1.772 * U_;  
             int R2 = Y2 + 1.402 * V_;  
            int G2 = Y2 - 0.34414 * U_ - 0.71414 * V_;  
            int B2 = Y2 + 1.772 * U_;  
            
            bgrImage.at<cv::Vec3b>(j, i)[0] = std::min(std::max(B1, 0), 255);  //B
            bgrImage.at<cv::Vec3b>(j, i)[1] = std::min(std::max(G1, 0), 255);  //G
            bgrImage.at<cv::Vec3b>(j, i)[2] = std::min(std::max(R1, 0), 255);  //R
            bgrImage.at<cv::Vec3b>(j, i+1)[0] = std::min(std::max(B2, 0), 255);  
            bgrImage.at<cv::Vec3b>(j, i+1)[1] = std::min(std::max(G2, 0), 255);  
            bgrImage.at<cv::Vec3b>(j, i+1)[2] = std::min(std::max(R2, 0), 255); 
    }               
                start += w;//指向下一行数据
            }
    return bgrImage;  
}  


25.6主函数

int main(int argc, char *argv[])//使用:./gpio /dev/video
{
    if (2 != argc) {//使用时指定视频设备
        fprintf(stderr, "Usage: %s <video_dev>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    /* 初始化LCD */
    if (fb_dev_init())
        exit(EXIT_FAILURE);
    /* 初始化摄像头 */
    if (v4l2_dev_init(argv[1]))
        exit(EXIT_FAILURE);
    /* 枚举所有格式并打印摄像头支持的分辨率及帧率 */
    v4l2_enum_formats();
    v4l2_print_formats();
    /* 设置格式 */
    if (v4l2_set_format())
        exit(EXIT_FAILURE);
    /* 初始化帧缓冲:申请、内存映射、入队 */
    if (v4l2_init_buffer())
        exit(EXIT_FAILURE);
    /* 开启视频采集 */
    if (v4l2_stream_on())
        exit(EXIT_FAILURE);
    /* 读取数据:出队 */
    v4l2_read_data();       //在函数内循环采集数据、将其显示到LCD屏
    exit(EXIT_SUCCESS);
}

Tips

man 2 open查看命令介绍

man 命令后面跟着两个参数,数字 2 表示系统调用,man 命令除了可以查看系统调用的帮助信息
外,还可以查看 Linux 命令(对应数字 1)以及标准 C 库函数(对应数字 3)所对应的帮助信息;最后一个
参数 open 表示需要查看的系统调用函数名。

#include"..\ucos-ii\includes.h" /* uC/OS interface */ #include "..\ucos-ii\add\osaddition.h" #include "..\inc\drv.h" #include <string.h> #include <math.h> #pragma import(__use_no_semihosting_swi) // ensure no functions that use semihosting ///******************任务定义***************/// OS_STK Main_Stack[STACKSIZE*8]={0, }; //Main_Test_Task堆栈 void Main_Task(void *Id); //Main_Test_Task #define Main_Task_Prio 12 /**************已经定义的OS任务************* tcp监控任务 11 以太网物理层监控任务 8 触摸屏任务 9 键盘任务 10 lcd刷新任务 59 系统任务 1 *****************************************************/ ///*****************事件定义*****************/// OS_EVENT *Nand_Rw_Sem; //Nand_Flash读控制权旗语 //and you can use it as folloeing: // Nand_Rw_Sem=OSSemCreate(1); //创建Nand-Flash读控制权旗语,初值为1满足互斥条件// // OSSemPend(Nand_Rw_Sem,0,&amp;err); // OSSemPost(Nand_Rw_Sem); OS_EVENT *Uart_Rw_Sem; //Uart读控制权旗语 //and you can use it as folloeing: // Uart_Rw_Sem=OSSemCreate(1); //创建Uart读控制权旗语,初值为1满足互斥条件// // OSSemPend(Uart_Rw_Sem,0,&amp;err); // OSSemPost(Uart_Rw_Sem); ////////////////////////////////////////////////////////// void initOSGUI() //初始化操作系统的图形界面 { initOSMessage(); initOSList(); initOSDC(); initOSCtrl(); initOSFile(); } ///////////////////////////////////////////////////// // Main function. // ////////////////////////////////////////////////////
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值