CVTE嵌入式Linux面试

2019-07-12 20:25发布

2019年cvte春招,嵌入式软件开发面试问题分享。 目录 1.scanf 和 gets 的区别? 2.#define 比较俩个数的大小,输出小的? 3.malloc函数需要注意哪些?  4.进程间通信有哪些技术?  5.进程、线程并发的技术?  6.static关键字 7.怎么知道链表是环状? 8.shell会不会(简单语法) 9.栈、堆、静态内存? 10.二分查找

1.scanf 和 gets 的区别?

scanf是格式化输入,如果只使用最简单的格式化字符串%s,那么scanf只会取一个单词,在输入流中遇到空格/tab/换行,就会结束。比如输入流中有"aa bb cc"的话,那么执行scanf("%s", s),s的值就会是aa,再次执行这句话,s的值就是bb,第三次就是cc。scanf会从输入流中取多少怎么取,取决于格式化字符串,也就是scanf的第一个参数;而gets则肯定会取出完整的一行。也就是说在scanf中,换行符等同于空格/tab,但gets中,换行符为读取结束标志。  gets会将输入回车前所有输入的内容取出来,存放到一个字符数组,包括空格/tab,无论这些内容被空格/tab分成了几段,gets都会将这行输入完完整整的放到数组中,通俗点说就是gets会取一行,读到换行为止。

2.#define 比较俩个数的大小,输出小的?

#define out_min(A,B) ((A<=B)?(A):(B))

3.malloc函数需要注意哪些? 

  1. malloc函数原型为 extern void *malloc(unsigned int num_bytes);使用的时候要进行类型转换;
  2. malloc函数使用后必须free;

4.进程间通信有哪些技术? 

  • 管道

管道分为未名管道和命名管道,使用时要注意以下几点:

1.管道是半双工的,双向通信需要建立俩个管道; 2.管道从尾部写入,从头部读出; 3.管道里的内容被读出以后,会自动删除; 4.多进程对管道操作时要加锁; 5.读写端关闭的管道返回0; 6.写读端关闭的管道出错,产生信号SIGPIPE; 7.管道空了read会阻塞,管道满了write也会阻塞,可以设置为非阻塞;

未名管道操作流程(框架):

/*未名管道只能用于有亲属关系的进程间的通信*/ int fd[2]; pipe(fd[2]); pid_t pid = fork(); if(pid < 0){ //error perror("fork error"); exit(1); }else if(pid > 0){ //parent close(fd[0]); //关闭读端 …… close(fd[1]); wait(0); }else{ //child close(fd[1]); //关闭写端 …… close(fd[0]); }

命名管道操作流程(框架)

/* * 命名管道可以用于任意俩个进程间通信 * fifo的创建: * shell下:mkfifo FIFO * C语言函数:int mkfifo(const char *pathname, mode_t mode); * 如果该程序创建好了一个管道,那么这个管道就会一直存在;该文件如果已经存在,那么创建就会失败; */ //read fifo_fd = open(fifo_name, O_RDONLY | O_NONBLOCK); if(fifo_fd < 0){ perror("open error "); exit(1); }else{ printf("open success "); } char readbuff[512]; memset(readbuff, 0, sizeof(readbuff)); while(read(fifo_fd, readbuff, sizeof(readbuff)) < 0){ perror("read error"); exit(1); } printf("%s ", readbuff); close(fifo_fd); //write fifo_fd = open(fifo_name, O_WRONLY); if(fifo_fd < 0){ perror("open error "); exit(1); }else{ printf("open success "); } char *buf = "123456789"; size_t size = strlen(buf); if(write(fifo_fd, buf, size) != size){ perror("write error "); exit(1); } close(fifo_fd);
  • 消息队列

消息队列特点

  1. 消息队列一直存在于内核之中,除非调用命令删除;
  2. 消息队列可是双向通信
  3. 消息队列的数据块可以有类型

关键函数

//创建、获得消息队列 int msgget(key_t key, int msgflg); key ftok(path,0); //系统创建一个唯一的 IPC_PRIVATE //系统提供一个唯一的 一个大于0的数 //用户提供一个唯一的 msgflag IPC_CREAT如果内核中没有此队列,则创建它。 IPC_EXCL当和IPC_CREAT一起使用时,如果队列已经存在,则失败。 //向消息队列发送消息 msgsnd(int msqid, const void *msgp, size_t msg, int msgflg); msqid 消息队列标识 *msgp 要发送的消息 msg 发送的消息长度,要减去sizeof(long)不包括消息类型的长整形 msgflg 0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列 IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回 IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。 //从消息队列接收消息 msgrcv(int msqid, void* msgp, size_t msgsz, long msgtype, int msgflg); msqid 消息队列标识符 msgp 存放消息的结构体,结构体类型要与msgsnd函数发送的类型相同 msgsz 要接收消息的大小,不含消息类型占用的4个字节 msgtyp 0:接收第一个消息 >0:接收类型等于msgtyp的第一个消息 <0:接收类型等于或者小于msgtyp绝对值的第一个消息 msgflg 0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待 IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息 IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃 //消息队列控制函数 msgctl(int msqid, int cmd, struct msqid_ds *buf); msqid 消息队列id cmd IPC_STAT 获取消息队列的信息 IPC_SET 设置消息队列的信息 IPC_RMID 删除消息队列 msqid_ds 修改时间;修改对象;队列长度;

 

消息队列的使用 

/*向消息队列发送消息*/ #include #include #include #include typedef struct{ long type; //消息类型 int start; //消息数据本身(包括start和end) int end; }MSG; /* * 往消息队列中发送消息 */ int main(int argc, char *argv[]) { if(argc < 2){ printf("usage: %s key ", argv[0]); exit(1); } key_t key = atoi(argv[1]); //手动输入一个唯一的值 //key_t key = IPC_PRIVATE; //系统提供 //key_t key = ftok(argv[1], 0); //系统提供,传参为路径名字 printf("key: %d ", key); //创建消息队列 int msq_id; if((msq_id = msgget(key, //IPC_CREAT|IPC_EXCL表示如果队列已经存在则失败 IPC_CREAT|IPC_EXCL|0777)) < 0){ perror("msgget error"); } printf("msg id: %d ", msq_id); //定义要发送的消息 //{消息类型, 消息内容, 消息内容} MSG m1 = {4, 4, 400}; MSG m2 = {2, 2, 200}; MSG m3 = {1, 1, 100}; MSG m4 = {6, 6, 600}; MSG m5 = {6, 60, 6000}; //发送消息到消息队列 if(msgsnd(msq_id, &m1, sizeof(MSG)-sizeof(long), IPC_NOWAIT) < 0){ //减去数据类型的大小 perror("msgsnd error"); } if(msgsnd(msq_id, &m2, sizeof(MSG)-sizeof(long), IPC_NOWAIT) < 0){ perror("msgsnd error"); } if(msgsnd(msq_id, &m3, sizeof(MSG)-sizeof(long), IPC_NOWAIT) < 0){ perror("msgsnd error"); } if(msgsnd(msq_id, &m4, sizeof(MSG)-sizeof(long), IPC_NOWAIT) < 0){ perror("msgsnd error"); } if(msgsnd(msq_id, &m5, sizeof(MSG)-sizeof(long), IPC_NOWAIT) < 0){ perror("msgsnd error"); } //发送消息队列中消息的总数 struct msqid_ds ds; if(msgctl(msq_id, IPC_STAT, &ds) < 0){ perror("msgctl error"); } printf("msg total: %ld ", ds.msg_qnum); exit(0); } /*从消息队列中读取数据*/ #include #include #include #include typedef struct{ long type; int start; int end; }MSG; int main(int argc, char *argv[]) { if(argc < 3){ printf("usage: %s key type ", argv[0]); exit(1); } key_t key = atoi(argv[1]); long type = atoi(argv[2]); //获得指定的消息队列 int msq_id; if((msq_id = msgget(key, 0777)) < 0){ perror("msgget error"); } printf("msg id: %d ", msq_id); //从消息队列中接受指定类型的消息 MSG m; if(msgrcv(msq_id, &m, sizeof(MSG)-sizeof(long), type, IPC_NOWAIT) < 0){ perror("msgrcv error"); }else{ printf("type: %ld start: %d end: %d ", m.type, m.start, m.end); } exit(0); }  
  • 共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
         
        (1)创建共享内存    int shmget(key_t key, size_t size, int shmflg)
                返回 共享内存标识符
                key    共享内存段的命名
                size    size以字节为单位指定的内存容量
                shmflg    权限标志 与open函数一样
        (2)共享内存映射 void *shmat(int shm_id, const void *shm_addr, int shmflg)
        第一次创建的共享内存还不能被任何进程访问,序用通过shmat映射
        shm_id        共享内存的标识
        shm_addr    连接到当前进程的地址,通常为空,让系统来选择共享内存的地址
        shmflag        通常为0
        调用成功返回共享内存第一个字节的指针         
        ④函数进行分离(本进程对共享存储段操作结束时的步骤,并不是从系统中删除共享内存和结构)
        int shmdt(const void *shmaddr)
        
        ⑤控制共享内存 shmctl(int shm_id, int command, struct shmid_ds *buf)
        shm_id:        内存标识符
        command:    IPC_STAT:读取共享内存的信息
                             IPC_SET: 设置共享内存的信息
                             IPC_RMID:删除共享内存段
  • 信号

信号的基本概念

不可靠信号: 也称为非实时信号,不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值取值区间为1~31; 可靠信号: 也称为实时信号,支持排队, 信号不会丢失, 发多少次, 就可以收到多少次. 信号值取值区间为32~64 在终端可以通过 kill -l 命令查看所有的信号。

信号的产生

(1)用户自己输入,如按下Ctrl+C,产生SIGINT信号; (2)产生硬件异常,如CPU监测到内存的违法访问;
  1. 硬件方式产生
  2. 软件产生方式
1.int kill(pid_t pid, int sig); //用于向任何进程组或进程发送信号 pid: pid > 0: 发送给进程号为pid的的进程 pid == 0: 发送给调用kill()的同组进程 pid == -1: 发送给所有本进程有权限发给的进程 pid < -1: 发送给-pid组的进程 sig: 准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。 返回值: 成功返回0; 失败返回-1,并设定errno的值: errno == EINVAL:指定的sig无效 errno == EPERM:权限不够,无法发送给某进程 errno == ESRCH:参数pid所指定的进程或进程组不存在 2.unsigned int alarm(unsigned int seconds); //用于调用进程指定时间后发出SIGALARM信号; seconds: 表示秒数 函数返回值 成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。 出错:-1 如果用alarm()函数作为定时器,每次处理以后,都要再次调用alarm()函数 4.setitimer():设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大; 5.int raise(int signo):用于向进程自身发送信号;

信号的处理

进程对信号的处理方式: 有3种
  • 默认 接收到信号后按默认的行为处理该信号。 这是多数应用采取的处理方式;
  • 自定义 用自定义的信号处理函数来执行特定的动作;
  • 忽略 接收到信号后不做任何反应;
函数 void (*signal(int sig, void (*func)(int)))(int) 设置一个函数来处理信号; 函数原型的解读: 中间部分signal(int sig, void (*func)(int))俩个参数,第一个参数sig为要登记的信号类型,第二个参数为一个带int参数的函数指针; 外围部分 void (*signel(xxxx)(int))   signal函数返回的是一个函数指针,无返回值,有一个int参数; 参数sig: 要登记处理函数的信号,如SIGINT; 参数 void(*func)(int): 可以是一个函数指针;也可以是SIG_DFL:默认的信号处理程序;SIG_IGN:忽视信号。

例子

#include #include #include #include //定义信号处理函数 //signo: 进程所捕获的信号 void sig_handler(int signo) { printf("%d, %d occrued ", getpid(), signo); } int main(void) { //向内核登记信号处理函数以及信号值 if(signal(SIGTSTP, sig_handler) == SIG_ERR) { perror("signal sigtstp error"); } if(signal(SIGINT, sig_handler) == SIG_ERR) { perror("signal sigint error"); } if(signal(SIGUSR1, sig_handler) == SIG_ERR) { perror("signal sigusr1 error"); } if(signal(SIGUSR2, sig_handler) == SIG_ERR) { perror("signal sigusr2 error"); } int i = 0; while(i < 20) { printf("%d out %d ", getpid(), i++); sleep(1); } raise(SIGUSR1); kill(getpid(), SIGUSR2); return 0; }
  • socket

用于处于不同主机的俩个主机相互通信。

5.进程、线程并发的技术? 

互斥锁

        基本流程:
        1.初始化一个互斥锁:pthread_mutex_init(pthread_mutex_t *mutex, NULL);
        2.加锁    pthread_mutex_lock(pthread_mutex_t *mutex):加锁失败阻塞
                      pthread_mutex_trylock():加锁失败返回错误码
        3.对共享资源进行操作
        4.解锁    pthread_mutex_unlock()
        5.注销互斥锁:pthread_mutex_destroy(pthread_mutex_t *mutex)  

读写锁

读写锁的状态:1.读模式下的加锁;2.写模式下的加锁;3.无锁;
        没有写模式下的加锁,任意线程都可以进行读模式的加锁;
        只有读写不加锁的状态下,才能进行写模式的加锁;
        
        使用步骤:
        1.初始化
        2.设置读写锁属性
        3.使用读写锁
        4.销毁读写锁

信号量

        信号量:特殊的变量,访问具有原子性;
        使用信号量解决进程或者线程之间的共享资源引发的同步问题;
        
        使用步骤:
        1.新建信号量    int semget(key_t key, int num_sems, int sem_flags);
                返回:信号量标识符
                key:信号量标记
                num_sems:信号量的数量,一般为1
                flags:    IPC_CREATE:如果信号量存在,返回标识符
                    IPC_EXCL:如果信号量存在,返回错误-1
        2.修改信号量的值(在PV操作里调用)
                int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
                sem_id:信号量标识符
                sem_opa:指向信号量操作数
                sem_ops:opsptr中元素的个数
        3.信号量的控制    int semctl(int semid, int semnum, int cmd, /*可选参数*/);
                semid:信号量标识
                semnum:指定哪个信号
                cmd:    IPC_STAT:读取信号量结构体
                    IPC_SET:设置信号量结构体
                    IPC_RMID:将信号量从内存中删除
                    SETVAL:设置信号量中一个单独信号量的值
        

6.static关键字

  1. 用于修饰函数里变量时,变量存在静态内存,函数退出后不会释放;
  2. 用于修饰全局变量,该全局变量只能在本文件中使用;
  3. 用于修饰函数的时候,该函数只能在本文件中使用;

7.怎么知道链表是环状?

思路:设置俩个快慢指针(快慢指针即俩个指针的起点相同,慢指针每次走一步,快指针每次走俩步),让他们一直往下走,如果他们相等说明有环;遇到nullptr则说明无环。

8.shell会不会(简单语法)

1.变量: a.定义时没有$ b.变量名和等号之间不能有空格 c.使用变量${变量名} 使用变量的时候$ 不使用的不加$ readonly 只读变量 修改值会报错 unset可以删除变量 单引号里的字符无论如何都会原样输出 双引号里可以有转义字符可以有变量 默认变量: $# 输出命令行参数的个数 $* 输出所有的变量 $0 shell命令本身 $1 输出第一个参数 局部变量 local 只在定义的函数中有效 2.if语句 if [ $a = $b ] then #code fi 3.判断 [-r"$folder"]&&echo"Can read" &&前面为真后边执行 [-f"$folder"]||echo"this is not file" ||前面为假后边执行 4.for for var in [list] var不加$ do # 循环体中var加$ done 5.while until while[] do # done 6.case case "$vsr"in condition1) ;; condirion2) ;; *) ;; esac  

9.栈、堆、静态内存?

1.栈存放局部变量、函数调用上下文、函数参数、函数返回地址 栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,如:局部数组; 2.堆 堆中被程序申请使用的内存在程序主动释放前将一直有效 堆空间通过申请才能获得 3.静态内存存放全局变量和静态变量 静态变量的空间在编译的时候就确认了 静态变量的存储信息会存在可执行程序中 静态变量会一直存在,直到程序结束

10.二分查找

  /* 函数BinarySeach,在包含size个元素的、从小到大排序的int数组a里查找元素p,如果查到了返回元素的下 * 标,如果找不到,返回-1. */ int BinarySearch(int a[], int size, int p) { int L = 0; //左端点 int R = size - 1; //右端点 while(L <= R){ int mid = L+(R-L)/2; //取查找正中元素下标 if( p == a[mid]){ return mid; }else if(p > a[mid]){ L = mid + 1; }else{ R = mid - 1; } } return -1; } /* 函数LowerBound,在包含size个元素的、从小到大排序的int数组a里查找比给定整数p小的,下标最大的元 * 素。找到则返回其下标,找不到返回-1 */ int LowerBound(int a[], int size, int p) { int L = 0; //左端点 int R = size -1; //右端点 int lastPos = -1; //到目前为止找到的最优解 while(L <= R){ int mid = L + (R-L)/2; if(a[mid] > p){ R = mid - 1; }else{ lastPos = mid; L = mid+1; } } return lastPos; }