【面试心经】——蔚来汽车Linux 岗位开发01

介绍

        本文记录的是2020.5月份应聘蔚来汽车linux 开发岗位的经历。

        BOSS 岗位薪资:30K ~60K

        总结:本次岗位应聘总共经历了4次面试,三次电话面试,一次视频面试。总体感觉难度中等,但是由于个人在最后一面视频面试中比较紧张,并且手写算法比较生疏,导致表现不如意,很遗憾最终没能进入该公司。虽然两个月后HR又请我再次面试,但是我已经入职新公司,并且与新同事相处愉快,就婉拒了。

        希望本文能够帮助有需要的朋友,还请关注、点赞、收藏三连~~~

面试内容

1. 进程间通信有哪些?区别是什么?共享内存为什么效率会更高?

        答:常用的进程间的通信方式有7种。消息队列共享内存管道信号量,socket,FIFO,信号

管道也称为无名管道。是一种半双工的通信方式,数据只能单向流动。而且只能在具有亲缘关系的进程中使用,因此一般结合fork使用。使用流程:

  1.  父进程创建管道,得到两个⽂件描述符指向管道的两端
  2.  父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。
  3. 父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。

#include  
#include  
#include  
#include  
int main()  
{  
    int fd[2];  
    int ret = pipe(fd);  
    //fd[0] 为读打开,fd[1] 为写打开。
    if (ret == -1)  
    {  
        perror(”pipe error\n”);  
        return 1;  
    }  
    pid_t id = fork();  
    if (id == 0)  
    {//child  
        int i = 0;  
        close(fd[0]);  
        char *child = “I am  child!”;  
        while (i<5)  
        {  
            write(fd[1], child, strlen(child) + 1);  
            sleep(2);  
            i++;  
        }  
    }  
    else if (id>0)  
    {//father  
        close(fd[1]);  
        char msg[100];  
        int j = 0;  
        while (j<5)  
        {  
            memset(msg,’\0’,sizeof(msg));  
            ssize_t s = read(fd[0], msg, sizeof(msg));  
            if (s>0)  
            {  
                msg[s - 1] = ’\0’;  
            }  
            printf(”%s\n”, msg);  
            j++;  
        }  
    }  
    else  
    {//error  
        perror(”fork error\n”);  
        return 2;  
    }  
    return  0;  
}  

注意:

  1. 当所有读通道关闭时,所有的写通道都是不合法的,否则向管道写入数据的进程将会收到内核传来的错误SIGPIPE信号
  2. 当所有的写通道关闭时,在所有数据全部读完之后,read 返回0。否则只要有一个写通道未关闭,read就会阻塞。
  3. PIPE_BUFF 规定了内核的管道缓冲区大小。当缓冲区满了之后,写进程就会一直阻塞。
  4. 当有多个读进程时,写入管道的数据是随机被读进程获取的,一旦被读取,其它的读进程就无法读书数据了。

        FIFO也被称为有名管道。它也是一种半双工的通信方式,但它允许无亲缘关系进程间的通信。FIFO其实是一种文件类型,使用步骤:

  1. 创建一个FIFO文件,这个FIFO文件是所有进程都知道。int mkfifo(const char *path, mode_t mode);
  2. 各进程open FIFO文件之后,通过文件操作进行通信。int mkfifoat(int fd, const char *path, mode_t mode);

代码示例:

//fifo_w.c
#include
#include
#include
#include
#include
#include
#include
#include
int main() {
        int fd = 0;
        int len = 0;
        int ret;
        char buf[1024] = { 0 };
        mkfifo("./test",0664);
        //只写方式打开test
        fd = open("./test", O_WRONLY);
        if (fd < 0)
        {
                perror("open error");
                exit(1);
        }
        puts("open fifo write");
        //向FIFO写入数据
        while ((len = read(STDIN_FILENO, buf, sizeof(buf))) > 0)
        {
                write(fd, buf, len);
        }
        close(fd);
        return 0;
}

//fifo_r.c
#include
#include
#include
#include
#include
#include
#include
#include

int main() {
        int fd = 0;
        int len = 0;
        char buf[1024] = { 0 };
       
        //只读打开test
        fd = open("./test", O_RDONLY);
        if (fd < 0)
        {
                perror("open error");
                exit(1);
        }
        puts("open fifo read");

        //从管道中读取数据
        while ((len = read(fd, buf, sizeof(buf))) > 0)
        {
                write(STDOUT_FILENO, buf, len);
        }

        //如果read返回0,说明读到文件末尾或对端已关闭
        if (len == 0)
        {
                puts("peer is close or file end");
        }
        else
        {
                puts("read error");
        }

        close(fd);
        return 0;
}

思考:FIFO文件和普通文件有何不同?

  1.  FIFO 文件,为我们做了同步,阻塞,通知等操作。
  2.  FIFO 管道是在磁盘中有对应的节点,但没有数据块。 无名管道是在内存中的,不可见。

信号用于通知某个事件已经发生。比如截取ctrl+c (SIGINT 终止信号)信号等。

注册信号处理函数
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号//不能是SIGKILL和SIGSTOP
handler:SIG_IGN:忽略该信号。
SIG_DFL:采用系统默认方式处理信号。
   自定义的信号处理函数指针
返回值:成功:设置之前的信号处理方式;失败:SIG_ERR

发送信号函数
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
sig:要发送的信号
返回值:成功 0;失败 -1

消息队列是消息的链接表,存储在内核中,有消息队列标识符。

#include
int msgget(key_t key, int flag);  
msgget用于创建一个新的消息队列或打开一个现有队列

int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);  
msgsnd用于将新消息添加到队列尾端

ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
msgrcv用于从队列中去消息

并不一定以先进先出的顺序取消息,可以按照消息的类型字段取消息。

        共享存储允许两个或多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的IPC

        在多个进程同步访问一个给定存储区时,若服务器进程正在将数据放入存储区,则在它做完之前,客户进程不应该去取这些数据。

#include
int shmget(key_t key, size_t size, int flag);
shmget函数获得一个共享存储标识符:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl函数对共享存储段执行多种操作:

void *shmat(int shmid, const void *addr, int flag);
一旦创建了一个共享存储段,进程就可以调用shmat函数将其连接到它的地址空间中

int shmdt(const void *addr);
当对共享存储段的操作已经结束时,调用shmdt函数与该段分离,但这并不会删除标
识符及其相关的数据结构。直至某个进程(一般是服务器进程)调用shmctl特地删除它为止:

        优点:我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。
        缺点:共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。

        信号量它是一个计数器,用于为多个进程提供共享数据对象的访问.。通常用于共享内存的同步访问,临界资源的同步访问。

#include
int semget(key_t key, int nsems, int flag);
当我们想使用信号量时,首先通过函数semget来获取一个信号量ID

int semctl(int semid, int semnum, int cmd, ... /* union semun arg*/);
semctl函数包含了多种信号量操作

int semop(int semid, struct sembuf semoparray[], size_t nops);
semop函数自动执行信号量集合上的操作数组

1.1 应用场景

  • 如果用户传递的信息较少,或者只是为了出发某些行为。信号是一种简洁有效的通信方式。但若是进程间要求传递的信息量较大或者存在数据交换的要求,就需要考虑别的通信方式了。
  • 无名管道与有名管道的区别在于单向通信以及有关联的进程。
  • 消息队列允许任意进程通过共享队列来进行进程间通信。并由系统调用函数来实现消息发送和接收之间的同步。从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用相对方便。

但是消息队列中信息的复制需要耗费CPU时间,不适宜信息量大或频繁操作的场合。

  • 消息队列与管道方式的区别在于,消息队列可以实现多对多,并需要在内存中实现,而管道可以在内存或磁盘上实现。
  • 共享内存无须复制,信息量大是其最大的优势。但是需要考虑同步问题。
  • 系统间的通信需要使用socket

1.2 为什么共享内存进行两次内存拷贝?其他IPC进行四次拷贝

        共享内存是一种高效的IPC机制。它允许两个或多个进程共享一块内存空间,进程可以直接读写这块内存,而无需调用操作系统的任何数据传输服务
        在共享内存模型中,数据通常只需要经历两次拷贝:
1.
从发送进程的用户空间拷贝到共享内存区域: 这一步涉及将数据从发送进程的用户空间拷贝到共享的内存区域。
2.
从共享内存区域拷贝到接收进程的用户空间: 接收进程从共享内存区域读取数据并拷贝到它自己的用户空间。
        消息队列和管道是另一种形式的IPC,它们允许进程通过操作系统内核进行数据交换
        在消息队列和管道中,数据通常需要经历四次拷贝:
1.
从发送进程的用户空间拷贝到内核空间:首先,数据从发送进程的用户空拷贝到操作系统内核空间。

2. 从内核空间拷贝到内核的缓冲区:接着,数据在内核内部从一个位置拷贝到另一个位置,通常是内核的缓冲区

3. 从内核的缓冲区拷贝回内核空间的另一部分: 当接收进程准备好接收数据时,数据从内核的缓冲区拷贝到内核空间的另一部分,以便传输给接收进程

4. 从内核空间拷贝到接收进程的用户空间: 最后,数据从内核空间拷贝到接收进程的用户空间。

2、线程间通信有哪些?区别是什么?

        线程于进程之间的区别在于同一进程中的所有线程共享地址空间。它并不需要像进程间通信一样需要系统调用。线程间通信,使用全局变量即可。但是要做好共享资源的同步以及互斥,保护好共享资源。

线程同步和互斥的方式

  • 锁机制:互斥锁,条件变量,读写锁

互斥锁提供了以排他方式防止数据结构被并发修改的方法

  1. 初始化锁int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  2. 阻塞加锁int pthread_mutex_lock(pthread_mutex *mutex);若锁已经被占用,挂起等待
  3. 非阻塞加锁int pthread_mutex_trylock( pthread_mutex_t *mutex);该函数语义与 pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待
  4.  解锁int pthread_mutex_unlock(pthread_mutex *mutex);要求锁处于lock状态
  5. 锁销毁int pthread_mutex_destroy(pthread_mutex *mutex);要求锁此时处于unlock状态

#include
#include
#include
#include

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int gn;

void* thread(void *arg)
{
    printf("thread's ID is  %d\n",pthread_self());
    pthread_mutex_lock(&mutex);
    gn = 12;
    printf("Now gn = %d\n",gn);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main()
{
    pthread_t id;
    printf("main thread's ID is %d\n",pthread_self());
    gn = 3;
    printf("In main func, gn = %d\n",gn);
    if (!pthread_create(&id, NULL, thread, NULL)) {
        printf("Create thread success!\n");
    } else {
        printf("Create thread failed!\n");
    }
    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

读写锁允许多个线程同时读共享数据,而对写操作是互斥的。

  • 如果一个线程用读锁锁定了临界区,那么其他线程也可以用读锁来进入临界区,这样就可以多个线程并行操作。但这个时候,如果再进行写锁加锁就会发生阻塞,写锁请求阻塞后,后面如果继续有读锁来请求,这些后来的读锁都会被阻塞!这样避免了读锁长期占用资源,防止写锁饥饿!
  • 如果一个线程用写锁锁住了临界区,那么其他线程不管是读锁还是写锁都会发生阻塞!
  1. 初始化int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
  2. 读写加解锁int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);、int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct   timespec *restrict abs_timeout);int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
  3.  销毁锁int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

#include
#include
#include
#include
#include

/* 初始化读写锁 */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
/* 全局资源 */
int global_num = 10;

void err_exit(const char *err_msg)
{
 printf("error:%s\n", err_msg);
 exit(1);
}

/* 读锁线程函数 */
void *thread_read_lock(void *arg)
{
 char *pthr_name = (char *)arg;

 while (1)
 {
     /* 读加锁 */
     pthread_rwlock_rdlock(&rwlock);

     printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num);
     sleep(1);
     printf("线程%s离开临界区...\n", pthr_name);

     /* 读解锁 */
     pthread_rwlock_unlock(&rwlock);

     sleep(1);
 }

 return NULL;
}

/* 写锁线程函数 */
void *thread_write_lock(void *arg)
{
 char *pthr_name = (char *)arg;

 while (1)
 {
     /* 写加锁 */
     pthread_rwlock_wrlock(&rwlock);

     /* 写操作 */
     global_num++;
     printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num);
     sleep(1);
     printf("线程%s离开临界区...\n", pthr_name);

     /* 写解锁 */
     pthread_rwlock_unlock(&rwlock);

     sleep(2);
 }

 return NULL;
}

int main(void)
{
 pthread_t tid_read_1, tid_read_2, tid_write_1, tid_write_2;

 /* 创建4个线程,2个读,2个写 */
 if (pthread_create(&tid_read_1, NULL, thread_read_lock, "read_1") != 0)
     err_exit("create tid_read_1");

 if (pthread_create(&tid_read_2, NULL, thread_read_lock, "read_2") != 0)
     err_exit("create tid_read_2");

 if (pthread_create(&tid_write_1, NULL, thread_write_lock, "write_1") != 0)
     err_exit("create tid_write_1");

 if (pthread_create(&tid_write_2, NULL, thread_write_lock, "write_2") != 0)
     err_exit("create tid_write_2");

 /* 随便等待一个线程,防止main结束 */
 if (pthread_join(tid_read_1, NULL) != 0)
     err_exit("pthread_join()");

 return 0;
}

条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件变量的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

  1. 初始化条件变量int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
  2. 等待函数int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
  3. 无条件等待int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
  4. 计时等待int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
  5. 激发条件int pthread_cond_signal(pthread_cond_t *cond);
  6. 激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)int pthread_cond_broadcast(pthread_cond_t *cond);激活所有等待线程
  7. 销毁条件变量int pthread_cond_destroy(pthread_cond_t *cond);

#include
#include
#include "stdlib.h"
#include "unistd.h"

pthread_mutex_t mutex;
pthread_cond_t cond;

void hander(void *arg)
{
    free(arg);
    (void)pthread_mutex_unlock(&mutex);
}

void *thread1(void *arg)
{
    pthread_cleanup_push(hander, &mutex);
    while (1) {
        printf("thread1 is running\n");
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        printf("thread1 applied the condition\n");
        pthread_mutex_unlock(&mutex);
        sleep(4);
    }
    pthread_cleanup_pop(0);
}

void *thread2(void *arg)
{
    while (1) {
        printf("thread2 is running\n");
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        printf("thread2 applied the condition\n");
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

int main()
{
    pthread_t thid1,thid2;
    printf("condition variable study!\n");
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_create(&thid1,NULL,thread1,NULL);
    pthread_create(&thid2,NULL,thread2,NULL);

    sleep(1);
    do {
        pthread_cond_signal(&cond);
    } while(1);

    sleep(20);
    pthread_exit(0);
    return 0;
}

3. 同主机中两个进程之间的socket 文件传输,需要几次内存拷贝?不同主机之间的socket传输呢?

不同进程之间socket 通信需要进行四次拷贝。

第一次:文件操作, read 磁盘中的文件,拷贝到进程的用户空间内存中。

第二次:socket发送 ,write 用户空间中内存传到内核空间内存中。

第三次:接收进程socket read。将内核空间中内存,拷贝到用户空间的内存中

第四次:接收进程,通过write 写入磁盘文件。

不同主机之间传输需要6次拷贝,多了两次 内核-> 网卡网卡-> 内核 拷贝。

你可能感兴趣的:(面试心经,汽车,linux,运维,面试)